shaum_types/
analysis.rs

1//! Fasting analysis result and related types.
2
3use serde::{Serialize, Deserialize};
4use smallvec::SmallVec;
5use std::borrow::Cow;
6use std::fmt;
7
8use super::status::FastingStatus;
9
10/// Extensible fasting type/reason.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct FastingType(pub Cow<'static, str>);
13
14impl FastingType {
15    /// Creates a new custom fasting type.
16    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    // Standard fasting types
20    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    // Legacy constructors
36    #[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/// Machine-readable trace codes for rules.
66#[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/// Payload for deferred trace formatting.
79#[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/// Rule trace event for explainability.
97#[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
108/// Returns Hijri month name (inline for pure types crate).
109fn 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/// Fasting analysis result.
119#[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}