dcbor_pattern/pattern/value/
date_pattern.rs

1use std::ops::RangeInclusive;
2
3use dcbor::{Date, prelude::*};
4
5use crate::pattern::{Matcher, Path, Pattern, vm::Instr};
6
7/// Pattern for matching date values in dCBOR.
8#[derive(Debug, Clone)]
9pub enum DatePattern {
10    /// Matches any date.
11    Any,
12    /// Matches a specific date.
13    Value(Date),
14    /// Matches dates within a range (inclusive).
15    Range(RangeInclusive<Date>),
16    /// Matches dates that are on or after the specified date.
17    Earliest(Date),
18    /// Matches dates that are on or before the specified date.
19    Latest(Date),
20    /// Matches a date by its ISO-8601 string representation.
21    String(String),
22    /// Matches dates whose ISO-8601 string representation matches the given
23    /// regex pattern.
24    Regex(regex::Regex),
25}
26
27impl PartialEq for DatePattern {
28    fn eq(&self, other: &Self) -> bool {
29        match (self, other) {
30            (DatePattern::Any, DatePattern::Any) => true,
31            (DatePattern::Value(a), DatePattern::Value(b)) => a == b,
32            (DatePattern::Range(a), DatePattern::Range(b)) => a == b,
33            (DatePattern::Earliest(a), DatePattern::Earliest(b)) => a == b,
34            (DatePattern::Latest(a), DatePattern::Latest(b)) => a == b,
35            (DatePattern::String(a), DatePattern::String(b)) => a == b,
36            (DatePattern::Regex(a), DatePattern::Regex(b)) => {
37                a.as_str() == b.as_str()
38            }
39            _ => false,
40        }
41    }
42}
43
44impl Eq for DatePattern {}
45
46impl std::hash::Hash for DatePattern {
47    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
48        match self {
49            DatePattern::Any => {
50                0u8.hash(state);
51            }
52            DatePattern::Value(date) => {
53                1u8.hash(state);
54                date.hash(state);
55            }
56            DatePattern::Range(range) => {
57                2u8.hash(state);
58                range.start().hash(state);
59                range.end().hash(state);
60            }
61            DatePattern::Earliest(date) => {
62                3u8.hash(state);
63                date.hash(state);
64            }
65            DatePattern::Latest(date) => {
66                4u8.hash(state);
67                date.hash(state);
68            }
69            DatePattern::String(iso_string) => {
70                5u8.hash(state);
71                iso_string.hash(state);
72            }
73            DatePattern::Regex(regex) => {
74                6u8.hash(state);
75                // Regex does not implement Hash, so we hash its pattern string.
76                regex.as_str().hash(state);
77            }
78        }
79    }
80}
81
82impl DatePattern {
83    /// Creates a new `DatePattern` that matches any date.
84    pub fn any() -> Self {
85        DatePattern::Any
86    }
87
88    /// Creates a new `DatePattern` that matches a specific date.
89    pub fn value(date: Date) -> Self {
90        DatePattern::Value(date)
91    }
92
93    /// Creates a new `DatePattern` that matches dates within a range
94    /// (inclusive).
95    pub fn range(range: RangeInclusive<Date>) -> Self {
96        DatePattern::Range(range)
97    }
98
99    /// Creates a new `DatePattern` that matches dates that are on or after the
100    /// specified date.
101    pub fn earliest(date: Date) -> Self {
102        DatePattern::Earliest(date)
103    }
104
105    /// Creates a new `DatePattern` that matches dates that are on or before the
106    /// specified date.
107    pub fn latest(date: Date) -> Self {
108        DatePattern::Latest(date)
109    }
110
111    /// Creates a new `DatePattern` that matches a date by its ISO-8601 string
112    /// representation.
113    pub fn string(iso_string: impl Into<String>) -> Self {
114        DatePattern::String(iso_string.into())
115    }
116
117    /// Creates a new `DatePattern` that matches dates whose ISO-8601 string
118    /// representation matches the given regex pattern.
119    pub fn regex(regex: regex::Regex) -> Self {
120        DatePattern::Regex(regex)
121    }
122}
123
124impl Matcher for DatePattern {
125    fn paths(&self, haystack: &CBOR) -> Vec<Path> {
126        // Check if the CBOR is a tagged value with date tag (tag 1)
127        if let CBORCase::Tagged(tag, _) = haystack.as_case() {
128            // Check if this is a date tag (tag 1)
129            if tag.value() == 1 {
130                // Try to extract the date
131                if let Ok(date) = Date::try_from(haystack.clone()) {
132                    let is_hit = match self {
133                        DatePattern::Any => true,
134                        DatePattern::Value(expected_date) => {
135                            date == *expected_date
136                        }
137                        DatePattern::Range(range) => range.contains(&date),
138                        DatePattern::Earliest(earliest) => date >= *earliest,
139                        DatePattern::Latest(latest) => date <= *latest,
140                        DatePattern::String(expected_string) => {
141                            date.to_string() == *expected_string
142                        }
143                        DatePattern::Regex(regex) => {
144                            regex.is_match(&date.to_string())
145                        }
146                    };
147
148                    if is_hit {
149                        vec![vec![haystack.clone()]]
150                    } else {
151                        vec![]
152                    }
153                } else {
154                    // Tagged with date tag but couldn't be parsed as date
155                    vec![]
156                }
157            } else {
158                // Not a date tag
159                vec![]
160            }
161        } else {
162            // Not tagged
163            vec![]
164        }
165    }
166
167    fn compile(
168        &self,
169        code: &mut Vec<Instr>,
170        literals: &mut Vec<Pattern>,
171        _captures: &mut Vec<String>,
172    ) {
173        let idx = literals.len();
174        literals.push(Pattern::Value(crate::pattern::ValuePattern::Date(
175            self.clone(),
176        )));
177        code.push(Instr::MatchPredicate(idx));
178    }
179}
180
181impl std::fmt::Display for DatePattern {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            DatePattern::Any => write!(f, "date"),
185            DatePattern::Value(date) => write!(f, "date'{}'", date),
186            DatePattern::Range(range) => {
187                write!(f, "date'{}...{}'", range.start(), range.end())
188            }
189            DatePattern::Earliest(date) => write!(f, "date'{}...'", date),
190            DatePattern::Latest(date) => write!(f, "date'...{}'", date),
191            DatePattern::String(iso_string) => {
192                write!(f, "date'{}'", iso_string)
193            }
194            DatePattern::Regex(regex) => {
195                write!(f, "date'/{}/'", regex.as_str())
196            }
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_date_pattern_any() {
207        // Create a date CBOR value
208        let date = Date::from_ymd(2023, 12, 25);
209        let cbor = CBOR::from(date);
210
211        let pattern = DatePattern::any();
212        let paths = pattern.paths(&cbor);
213        assert_eq!(paths.len(), 1);
214        assert_eq!(paths[0], vec![cbor.clone()]);
215
216        // Test with non-date CBOR
217        let text_cbor = CBOR::from("test");
218        let paths = pattern.paths(&text_cbor);
219        assert!(paths.is_empty());
220    }
221
222    #[test]
223    fn test_date_pattern_value() {
224        let date = Date::from_ymd(2023, 12, 25);
225        let cbor = CBOR::from(date.clone());
226
227        // Test matching date
228        let pattern = DatePattern::value(date.clone());
229        let paths = pattern.paths(&cbor);
230        assert_eq!(paths.len(), 1);
231        assert_eq!(paths[0], vec![cbor.clone()]);
232
233        // Test non-matching date
234        let other_date = Date::from_ymd(2024, 1, 1);
235        let pattern = DatePattern::value(other_date);
236        let paths = pattern.paths(&cbor);
237        assert!(paths.is_empty());
238    }
239
240    #[test]
241    fn test_date_pattern_range() {
242        let date1 = Date::from_ymd(2023, 12, 20);
243        let date2 = Date::from_ymd(2023, 12, 25);
244        let date3 = Date::from_ymd(2023, 12, 30);
245
246        let test_date = date2.clone();
247        let cbor = CBOR::from(test_date);
248
249        // Test date within range
250        let pattern = DatePattern::range(date1.clone()..=date3.clone());
251        let paths = pattern.paths(&cbor);
252        assert_eq!(paths.len(), 1);
253
254        // Test date outside range
255        let early_date = Date::from_ymd(2023, 12, 10);
256        let _late_date = Date::from_ymd(2024, 1, 10);
257        let pattern = DatePattern::range(early_date..=date1);
258        let paths = pattern.paths(&cbor);
259        assert!(paths.is_empty());
260    }
261
262    #[test]
263    fn test_date_pattern_earliest() {
264        let early_date = Date::from_ymd(2023, 12, 20);
265        let test_date = Date::from_ymd(2023, 12, 25);
266        let cbor = CBOR::from(test_date);
267
268        // Test date after earliest
269        let pattern = DatePattern::earliest(early_date);
270        let paths = pattern.paths(&cbor);
271        assert_eq!(paths.len(), 1);
272
273        // Test date before earliest
274        let late_date = Date::from_ymd(2023, 12, 30);
275        let pattern = DatePattern::earliest(late_date);
276        let paths = pattern.paths(&cbor);
277        assert!(paths.is_empty());
278    }
279
280    #[test]
281    fn test_date_pattern_latest() {
282        let late_date = Date::from_ymd(2023, 12, 30);
283        let test_date = Date::from_ymd(2023, 12, 25);
284        let cbor = CBOR::from(test_date);
285
286        // Test date before latest
287        let pattern = DatePattern::latest(late_date);
288        let paths = pattern.paths(&cbor);
289        assert_eq!(paths.len(), 1);
290
291        // Test date after latest
292        let early_date = Date::from_ymd(2023, 12, 20);
293        let pattern = DatePattern::latest(early_date);
294        let paths = pattern.paths(&cbor);
295        assert!(paths.is_empty());
296    }
297
298    #[test]
299    fn test_date_pattern_iso8601() {
300        let date = Date::from_ymd(2023, 12, 25);
301        let cbor = CBOR::from(date.clone());
302        let iso_string = date.to_string();
303
304        // Test matching ISO string
305        let pattern = DatePattern::string(iso_string.clone());
306        let paths = pattern.paths(&cbor);
307        assert_eq!(paths.len(), 1);
308
309        // Test non-matching ISO string
310        let pattern = DatePattern::string("2024-01-01T00:00:00Z");
311        let paths = pattern.paths(&cbor);
312        assert!(paths.is_empty());
313    }
314
315    #[test]
316    fn test_date_pattern_regex() {
317        let date = Date::from_ymd(2023, 12, 25);
318        let cbor = CBOR::from(date);
319
320        // Test matching regex (year 2023)
321        let regex = regex::Regex::new(r"2023-.*").unwrap();
322        let pattern = DatePattern::regex(regex);
323        let paths = pattern.paths(&cbor);
324        assert_eq!(paths.len(), 1);
325
326        // Test non-matching regex (year 2024)
327        let regex = regex::Regex::new(r"2024-.*").unwrap();
328        let pattern = DatePattern::regex(regex);
329        let paths = pattern.paths(&cbor);
330        assert!(paths.is_empty());
331    }
332
333    #[test]
334    fn test_date_pattern_display() {
335        assert_eq!(DatePattern::any().to_string(), "date");
336
337        let date = Date::from_ymd(2023, 12, 25);
338        assert_eq!(
339            DatePattern::value(date.clone()).to_string(),
340            format!("date'{}'", date)
341        );
342
343        let date1 = Date::from_ymd(2023, 12, 20);
344        let date2 = Date::from_ymd(2023, 12, 30);
345        assert_eq!(
346            DatePattern::range(date1.clone()..=date2.clone()).to_string(),
347            format!("date'{}...{}'", date1, date2)
348        );
349
350        assert_eq!(
351            DatePattern::earliest(date.clone()).to_string(),
352            format!("date'{}...'", date)
353        );
354        assert_eq!(
355            DatePattern::latest(date.clone()).to_string(),
356            format!("date'...{}'", date)
357        );
358
359        assert_eq!(
360            DatePattern::string("2023-12-25T00:00:00Z").to_string(),
361            "date'2023-12-25T00:00:00Z'"
362        );
363
364        let regex = regex::Regex::new(r"2023-.*").unwrap();
365        assert_eq!(DatePattern::regex(regex).to_string(), "date'/2023-.*/'");
366    }
367}