1use serde::{Serialize, Deserialize};
4use smallvec::SmallVec;
5use std::borrow::Cow;
6use std::fmt;
7
8use super::status::FastingStatus;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct FastingType(pub Cow<'static, str>);
13
14impl FastingType {
15 pub fn new(name: impl Into<Cow<'static, str>>) -> Self { Self(name.into()) }
17 pub fn custom(name: &str) -> Self { Self(Cow::Owned(name.to_string())) }
18
19 pub const RAMADHAN: Self = Self(Cow::Borrowed("Ramadhan"));
21 pub const ARAFAH: Self = Self(Cow::Borrowed("Arafah"));
22 pub const TASUA: Self = Self(Cow::Borrowed("Tasua"));
23 pub const ASHURA: Self = Self(Cow::Borrowed("Ashura"));
24 pub const AYYAMUL_BIDH: Self = Self(Cow::Borrowed("AyyamulBidh"));
25 pub const MONDAY: Self = Self(Cow::Borrowed("Monday"));
26 pub const THURSDAY: Self = Self(Cow::Borrowed("Thursday"));
27 pub const SHAWWAL: Self = Self(Cow::Borrowed("Shawwal"));
28 pub const DAUD: Self = Self(Cow::Borrowed("Daud"));
29 pub const EID_AL_FITR: Self = Self(Cow::Borrowed("EidAlFitr"));
30 pub const EID_AL_ADHA: Self = Self(Cow::Borrowed("EidAlAdha"));
31 pub const TASHRIQ: Self = Self(Cow::Borrowed("Tashriq"));
32 pub const FRIDAY_EXCLUSIVE: Self = Self(Cow::Borrowed("FridayExclusive"));
33 pub const SATURDAY_EXCLUSIVE: Self = Self(Cow::Borrowed("SaturdayExclusive"));
34
35 #[allow(non_snake_case)] pub fn Ramadhan() -> Self { Self::RAMADHAN }
37 #[allow(non_snake_case)] pub fn Arafah() -> Self { Self::ARAFAH }
38 #[allow(non_snake_case)] pub fn Tasua() -> Self { Self::TASUA }
39 #[allow(non_snake_case)] pub fn Ashura() -> Self { Self::ASHURA }
40 #[allow(non_snake_case)] pub fn AyyamulBidh() -> Self { Self::AYYAMUL_BIDH }
41 #[allow(non_snake_case)] pub fn Monday() -> Self { Self::MONDAY }
42 #[allow(non_snake_case)] pub fn Thursday() -> Self { Self::THURSDAY }
43 #[allow(non_snake_case)] pub fn Shawwal() -> Self { Self::SHAWWAL }
44 #[allow(non_snake_case)] pub fn Daud() -> Self { Self::DAUD }
45 #[allow(non_snake_case)] pub fn EidAlFitr() -> Self { Self::EID_AL_FITR }
46 #[allow(non_snake_case)] pub fn EidAlAdha() -> Self { Self::EID_AL_ADHA }
47 #[allow(non_snake_case)] pub fn Tashriq() -> Self { Self::TASHRIQ }
48 #[allow(non_snake_case)] pub fn FridayExclusive() -> Self { Self::FRIDAY_EXCLUSIVE }
49 #[allow(non_snake_case)] pub fn SaturdayExclusive() -> Self { Self::SATURDAY_EXCLUSIVE }
50
51 pub fn is_haram_type(&self) -> bool {
52 matches!(self.0.as_ref(), "EidAlFitr" | "EidAlAdha" | "Tashriq")
53 }
54
55 pub fn is_sunnah_type(&self) -> bool {
56 matches!(self.0.as_ref(), "Arafah" | "Tasua" | "Ashura" | "AyyamulBidh" |
57 "Monday" | "Thursday" | "Shawwal" | "Daud")
58 }
59}
60
61impl fmt::Display for FastingType {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub enum TraceCode {
68 EidAlFitr, EidAlAdha, Tashriq, FridaySingledOut, SaturdaySingledOut,
69 Ramadhan, Arafah, Tasua, Ashura, AyyamulBidh,
70 Monday, Thursday, Shawwal, Daud,
71 Custom, Debug,
72}
73
74impl fmt::Display for TraceCode {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub enum TracePayload {
81 None,
82 PostMaghribOffset,
83 CustomReason(String),
84}
85
86impl fmt::Display for TracePayload {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::None => Ok(()),
90 Self::PostMaghribOffset => write!(f, "Post-Maghrib: Effective date +1"),
91 Self::CustomReason(s) => write!(f, "{}", s),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RuleTrace {
99 pub code: TraceCode,
100 pub payload: TracePayload,
101}
102
103impl RuleTrace {
104 pub fn new(code: TraceCode, payload: TracePayload) -> Self { Self { code, payload } }
105 #[inline] pub fn simple(code: TraceCode) -> Self { Self { code, payload: TracePayload::None } }
106}
107
108fn get_hijri_month_name(month: usize) -> &'static str {
110 match month {
111 1 => "Muharram", 2 => "Safar", 3 => "Rabi' al-Awwal", 4 => "Rabi' al-Thani",
112 5 => "Jumada al-Ula", 6 => "Jumada al-Akhirah", 7 => "Rajab", 8 => "Sha'ban",
113 9 => "Ramadhan", 10 => "Shawwal", 11 => "Dhu al-Qi'dah", 12 => "Dhu al-Hijjah",
114 _ => "Unknown",
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct FastingAnalysis {
121 pub date: chrono::DateTime<chrono::Utc>,
122 pub primary_status: FastingStatus,
123 pub hijri_year: usize,
124 pub hijri_month: usize,
125 pub hijri_day: usize,
126 reasons: SmallVec<[FastingType; 2]>,
127 traces: SmallVec<[RuleTrace; 2]>,
128}
129
130impl FastingAnalysis {
131 pub fn new(
132 date: chrono::DateTime<chrono::Utc>,
133 status: FastingStatus,
134 types: SmallVec<[FastingType; 2]>,
135 hijri: (usize, usize, usize),
136 ) -> Self {
137 Self {
138 date, primary_status: status, reasons: types,
139 hijri_year: hijri.0, hijri_month: hijri.1, hijri_day: hijri.2,
140 traces: SmallVec::new(),
141 }
142 }
143
144 pub fn with_traces(
145 date: chrono::DateTime<chrono::Utc>,
146 status: FastingStatus,
147 types: SmallVec<[FastingType; 2]>,
148 hijri: (usize, usize, usize),
149 traces: SmallVec<[RuleTrace; 2]>,
150 ) -> Self {
151 Self {
152 date, primary_status: status, reasons: types,
153 hijri_year: hijri.0, hijri_month: hijri.1, hijri_day: hijri.2,
154 traces,
155 }
156 }
157
158 pub fn reasons(&self) -> impl Iterator<Item = &FastingType> { self.reasons.iter() }
159 pub fn has_reason(&self, ftype: &FastingType) -> bool { self.reasons.contains(ftype) }
160 pub fn reason_count(&self) -> usize { self.reasons.len() }
161
162 pub fn is_ramadhan(&self) -> bool { self.has_reason(&FastingType::RAMADHAN) }
163 pub fn is_white_day(&self) -> bool { self.has_reason(&FastingType::AYYAMUL_BIDH) }
164 pub fn is_eid(&self) -> bool { self.has_reason(&FastingType::EID_AL_FITR) || self.has_reason(&FastingType::EID_AL_ADHA) }
165 pub fn is_tashriq(&self) -> bool { self.has_reason(&FastingType::TASHRIQ) }
166 pub fn is_arafah(&self) -> bool { self.has_reason(&FastingType::ARAFAH) }
167 pub fn is_ashura(&self) -> bool { self.has_reason(&FastingType::ASHURA) }
168
169 pub fn explain(&self) -> String {
170 if self.traces.is_empty() {
171 self.generate_explanation()
172 } else {
173 self.traces.iter()
174 .map(|t| match &t.payload {
175 TracePayload::None => t.code.to_string(),
176 payload => format!("{}: {}", t.code, payload),
177 })
178 .collect::<Vec<_>>()
179 .join("; ")
180 }
181 }
182
183 pub fn traces(&self) -> impl Iterator<Item = &RuleTrace> { self.traces.iter() }
184
185 #[allow(dead_code)]
186 pub(crate) fn add_trace(&mut self, trace: RuleTrace) { self.traces.push(trace); }
187
188 fn generate_explanation(&self) -> String {
189 let hijri_str = format!(
190 "{} {} {}",
191 self.hijri_day,
192 get_hijri_month_name(self.hijri_month),
193 self.hijri_year
194 );
195
196 let status_str = match self.primary_status {
197 FastingStatus::Haram => "Haram",
198 FastingStatus::Wajib => "Wajib",
199 FastingStatus::SunnahMuakkadah => "Sunnah Muakkadah",
200 FastingStatus::Sunnah => "Sunnah",
201 FastingStatus::Makruh => "Makruh",
202 FastingStatus::Mubah => "Mubah",
203 };
204
205 if self.reasons.is_empty() {
206 format!("{} - {}", hijri_str, status_str)
207 } else {
208 let reasons: Vec<String> = self.reasons.iter().map(|r| r.to_string()).collect();
209 format!("{} - {} because: {}", hijri_str, status_str, reasons.join(", "))
210 }
211 }
212}
213
214impl fmt::Display for FastingAnalysis {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.explain()) }
216}