1use chrono::NaiveDate;
4use crate::rules::{check, RuleContext};
5use shaum_types::{FastingAnalysis, FastingType};
6use shaum_types::ShaumError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FilterMode {
11 All,
12 Wajib,
13 Sunnah,
14 Haram,
15 Makruh,
16 Mubah,
17}
18
19#[derive(Debug, Clone)]
21pub struct FastingQuery {
22 current: NaiveDate,
23 end: Option<NaiveDate>,
24 context: RuleContext,
25 filter: FilterMode,
26 exclude_haram: bool,
27 exclude_makruh: bool,
28 require_type: Option<FastingType>,
29}
30
31impl FastingQuery {
32 pub fn starting_from(date: NaiveDate) -> Self {
34 Self {
35 current: date,
36 end: None,
37 context: RuleContext::default(),
38 filter: FilterMode::All,
39 exclude_haram: false,
40 exclude_makruh: false,
41 require_type: None,
42 }
43 }
44
45 pub fn until(mut self, date: NaiveDate) -> Self { self.end = Some(date); self }
47
48 pub fn with_context(mut self, ctx: RuleContext) -> Self { self.context = ctx; self }
50
51 pub fn wajib(mut self) -> Self { self.filter = FilterMode::Wajib; self }
53
54 pub fn sunnah(mut self) -> Self { self.filter = FilterMode::Sunnah; self }
56
57 pub fn haram(mut self) -> Self { self.filter = FilterMode::Haram; self }
59
60 pub fn makruh(mut self) -> Self { self.filter = FilterMode::Makruh; self }
62
63 pub fn exclude_haram(mut self) -> Self { self.exclude_haram = true; self }
65
66 pub fn exclude_makruh(mut self) -> Self { self.exclude_makruh = true; self }
68
69 pub fn with_type(mut self, ftype: FastingType) -> Self { self.require_type = Some(ftype); self }
71
72 fn matches(&self, analysis: &FastingAnalysis) -> bool {
73 if self.exclude_haram && analysis.primary_status.is_haram() { return false; }
74 if self.exclude_makruh && analysis.primary_status.is_makruh() { return false; }
75 if let Some(ref t) = self.require_type { if !analysis.has_reason(t) { return false; } }
76
77 match self.filter {
78 FilterMode::All => true,
79 FilterMode::Wajib => analysis.primary_status.is_wajib(),
80 FilterMode::Sunnah => analysis.primary_status.is_sunnah(),
81 FilterMode::Haram => analysis.primary_status.is_haram(),
82 FilterMode::Makruh => analysis.primary_status.is_makruh(),
83 FilterMode::Mubah => analysis.primary_status.is_mubah(),
84 }
85 }
86}
87
88impl Iterator for FastingQuery {
89 type Item = Result<FastingAnalysis, ShaumError>;
90
91 fn next(&mut self) -> Option<Self::Item> {
92 loop {
93 if let Some(end) = self.end { if self.current > end { return None; } }
94 let date = self.current;
95 self.current = self.current.succ_opt()?;
96
97 let analysis = match check(date, &self.context) {
99 Ok(a) => a,
100 Err(e) => return Some(Err(e)),
101 };
102
103 if self.matches(&analysis) {
104 return Some(Ok(analysis));
105 }
106 }
107 }
108}
109
110pub trait QueryExt {
112 fn upcoming_fasts(&self) -> FastingQuery;
114}
115
116impl QueryExt for NaiveDate {
117 fn upcoming_fasts(&self) -> FastingQuery { FastingQuery::starting_from(*self) }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_basic_query() {
126 let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
127 let results: Vec<_> = FastingQuery::starting_from(start).take(5).collect();
128 assert_eq!(results.len(), 5);
129 assert!(results.iter().all(|r| r.is_ok()));
130 }
131
132 #[test]
133 fn test_sunnah_filter() {
134 let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
135 let results: Vec<_> = FastingQuery::starting_from(start).sunnah().take(3).collect();
136 for r in &results {
137 assert!(r.as_ref().unwrap().primary_status.is_sunnah());
138 }
139 }
140
141 #[test]
142 fn test_until_bound() {
143 let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
144 let end = NaiveDate::from_ymd_opt(2024, 3, 5).unwrap();
145 let results: Vec<_> = FastingQuery::starting_from(start).until(end).collect();
146 assert!(results.len() <= 5);
147 }
148
149 #[test]
150 fn test_query_ext_trait() {
151 let date = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
152 let results: Vec<_> = date.upcoming_fasts().take(3).collect();
153 assert_eq!(results.len(), 3);
154 }
155
156 #[test]
157 fn test_error_propagation() {
158 let start = NaiveDate::from_ymd_opt(2077, 1, 1).unwrap();
160 let mut query = FastingQuery::starting_from(start);
161 let result = query.next();
162 assert!(result.is_some());
163 assert!(result.unwrap().is_err());
164 }
165}