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 { DatePattern::Any }
85
86    /// Creates a new `DatePattern` that matches a specific date.
87    pub fn value(date: Date) -> Self { DatePattern::Value(date) }
88
89    /// Creates a new `DatePattern` that matches dates within a range
90    /// (inclusive).
91    pub fn range(range: RangeInclusive<Date>) -> Self {
92        DatePattern::Range(range)
93    }
94
95    /// Creates a new `DatePattern` that matches dates that are on or after the
96    /// specified date.
97    pub fn earliest(date: Date) -> Self { DatePattern::Earliest(date) }
98
99    /// Creates a new `DatePattern` that matches dates that are on or before the
100    /// specified date.
101    pub fn latest(date: Date) -> Self { DatePattern::Latest(date) }
102
103    /// Creates a new `DatePattern` that matches a date by its ISO-8601 string
104    /// representation.
105    pub fn string(iso_string: impl Into<String>) -> Self {
106        DatePattern::String(iso_string.into())
107    }
108
109    /// Creates a new `DatePattern` that matches dates whose ISO-8601 string
110    /// representation matches the given regex pattern.
111    pub fn regex(regex: regex::Regex) -> Self { DatePattern::Regex(regex) }
112}
113
114impl Matcher for DatePattern {
115    fn paths(&self, haystack: &CBOR) -> Vec<Path> {
116        // Check if the CBOR is a tagged value with date tag (tag 1)
117        if let CBORCase::Tagged(tag, _) = haystack.as_case() {
118            // Check if this is a date tag (tag 1)
119            if tag.value() == 1 {
120                // Try to extract the date
121                if let Ok(date) = Date::try_from(haystack.clone()) {
122                    let is_hit = match self {
123                        DatePattern::Any => true,
124                        DatePattern::Value(expected_date) => {
125                            date == *expected_date
126                        }
127                        DatePattern::Range(range) => range.contains(&date),
128                        DatePattern::Earliest(earliest) => date >= *earliest,
129                        DatePattern::Latest(latest) => date <= *latest,
130                        DatePattern::String(expected_string) => {
131                            date.to_string() == *expected_string
132                        }
133                        DatePattern::Regex(regex) => {
134                            regex.is_match(&date.to_string())
135                        }
136                    };
137
138                    if is_hit {
139                        vec![vec![haystack.clone()]]
140                    } else {
141                        vec![]
142                    }
143                } else {
144                    // Tagged with date tag but couldn't be parsed as date
145                    vec![]
146                }
147            } else {
148                // Not a date tag
149                vec![]
150            }
151        } else {
152            // Not tagged
153            vec![]
154        }
155    }
156
157    fn compile(
158        &self,
159        code: &mut Vec<Instr>,
160        literals: &mut Vec<Pattern>,
161        _captures: &mut Vec<String>,
162    ) {
163        let idx = literals.len();
164        literals.push(Pattern::Value(crate::pattern::ValuePattern::Date(
165            self.clone(),
166        )));
167        code.push(Instr::MatchPredicate(idx));
168    }
169}
170
171impl std::fmt::Display for DatePattern {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        match self {
174            DatePattern::Any => write!(f, "date"),
175            DatePattern::Value(date) => write!(f, "date'{}'", date),
176            DatePattern::Range(range) => {
177                write!(f, "date'{}...{}'", range.start(), range.end())
178            }
179            DatePattern::Earliest(date) => write!(f, "date'{}...'", date),
180            DatePattern::Latest(date) => write!(f, "date'...{}'", date),
181            DatePattern::String(iso_string) => {
182                write!(f, "date'{}'", iso_string)
183            }
184            DatePattern::Regex(regex) => {
185                write!(f, "date'/{}/'", regex.as_str())
186            }
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_date_pattern_any() {
197        // Create a date CBOR value
198        let date = Date::from_ymd(2023, 12, 25);
199        let cbor = CBOR::from(date);
200
201        let pattern = DatePattern::any();
202        let paths = pattern.paths(&cbor);
203        assert_eq!(paths.len(), 1);
204        assert_eq!(paths[0], vec![cbor.clone()]);
205
206        // Test with non-date CBOR
207        let text_cbor = CBOR::from("test");
208        let paths = pattern.paths(&text_cbor);
209        assert!(paths.is_empty());
210    }
211
212    #[test]
213    fn test_date_pattern_value() {
214        let date = Date::from_ymd(2023, 12, 25);
215        let cbor = CBOR::from(date.clone());
216
217        // Test matching date
218        let pattern = DatePattern::value(date.clone());
219        let paths = pattern.paths(&cbor);
220        assert_eq!(paths.len(), 1);
221        assert_eq!(paths[0], vec![cbor.clone()]);
222
223        // Test non-matching date
224        let other_date = Date::from_ymd(2024, 1, 1);
225        let pattern = DatePattern::value(other_date);
226        let paths = pattern.paths(&cbor);
227        assert!(paths.is_empty());
228    }
229
230    #[test]
231    fn test_date_pattern_range() {
232        let date1 = Date::from_ymd(2023, 12, 20);
233        let date2 = Date::from_ymd(2023, 12, 25);
234        let date3 = Date::from_ymd(2023, 12, 30);
235
236        let test_date = date2.clone();
237        let cbor = CBOR::from(test_date);
238
239        // Test date within range
240        let pattern = DatePattern::range(date1.clone()..=date3.clone());
241        let paths = pattern.paths(&cbor);
242        assert_eq!(paths.len(), 1);
243
244        // Test date outside range
245        let early_date = Date::from_ymd(2023, 12, 10);
246        let _late_date = Date::from_ymd(2024, 1, 10);
247        let pattern = DatePattern::range(early_date..=date1);
248        let paths = pattern.paths(&cbor);
249        assert!(paths.is_empty());
250    }
251
252    #[test]
253    fn test_date_pattern_earliest() {
254        let early_date = Date::from_ymd(2023, 12, 20);
255        let test_date = Date::from_ymd(2023, 12, 25);
256        let cbor = CBOR::from(test_date);
257
258        // Test date after earliest
259        let pattern = DatePattern::earliest(early_date);
260        let paths = pattern.paths(&cbor);
261        assert_eq!(paths.len(), 1);
262
263        // Test date before earliest
264        let late_date = Date::from_ymd(2023, 12, 30);
265        let pattern = DatePattern::earliest(late_date);
266        let paths = pattern.paths(&cbor);
267        assert!(paths.is_empty());
268    }
269
270    #[test]
271    fn test_date_pattern_latest() {
272        let late_date = Date::from_ymd(2023, 12, 30);
273        let test_date = Date::from_ymd(2023, 12, 25);
274        let cbor = CBOR::from(test_date);
275
276        // Test date before latest
277        let pattern = DatePattern::latest(late_date);
278        let paths = pattern.paths(&cbor);
279        assert_eq!(paths.len(), 1);
280
281        // Test date after latest
282        let early_date = Date::from_ymd(2023, 12, 20);
283        let pattern = DatePattern::latest(early_date);
284        let paths = pattern.paths(&cbor);
285        assert!(paths.is_empty());
286    }
287
288    #[test]
289    fn test_date_pattern_iso8601() {
290        let date = Date::from_ymd(2023, 12, 25);
291        let cbor = CBOR::from(date.clone());
292        let iso_string = date.to_string();
293
294        // Test matching ISO string
295        let pattern = DatePattern::string(iso_string.clone());
296        let paths = pattern.paths(&cbor);
297        assert_eq!(paths.len(), 1);
298
299        // Test non-matching ISO string
300        let pattern = DatePattern::string("2024-01-01T00:00:00Z");
301        let paths = pattern.paths(&cbor);
302        assert!(paths.is_empty());
303    }
304
305    #[test]
306    fn test_date_pattern_regex() {
307        let date = Date::from_ymd(2023, 12, 25);
308        let cbor = CBOR::from(date);
309
310        // Test matching regex (year 2023)
311        let regex = regex::Regex::new(r"2023-.*").unwrap();
312        let pattern = DatePattern::regex(regex);
313        let paths = pattern.paths(&cbor);
314        assert_eq!(paths.len(), 1);
315
316        // Test non-matching regex (year 2024)
317        let regex = regex::Regex::new(r"2024-.*").unwrap();
318        let pattern = DatePattern::regex(regex);
319        let paths = pattern.paths(&cbor);
320        assert!(paths.is_empty());
321    }
322
323    #[test]
324    fn test_date_pattern_display() {
325        assert_eq!(DatePattern::any().to_string(), "date");
326
327        let date = Date::from_ymd(2023, 12, 25);
328        assert_eq!(
329            DatePattern::value(date.clone()).to_string(),
330            format!("date'{}'", date)
331        );
332
333        let date1 = Date::from_ymd(2023, 12, 20);
334        let date2 = Date::from_ymd(2023, 12, 30);
335        assert_eq!(
336            DatePattern::range(date1.clone()..=date2.clone()).to_string(),
337            format!("date'{}...{}'", date1, date2)
338        );
339
340        assert_eq!(
341            DatePattern::earliest(date.clone()).to_string(),
342            format!("date'{}...'", date)
343        );
344        assert_eq!(
345            DatePattern::latest(date.clone()).to_string(),
346            format!("date'...{}'", date)
347        );
348
349        assert_eq!(
350            DatePattern::string("2023-12-25T00:00:00Z").to_string(),
351            "date'2023-12-25T00:00:00Z'"
352        );
353
354        let regex = regex::Regex::new(r"2023-.*").unwrap();
355        assert_eq!(DatePattern::regex(regex).to_string(), "date'/2023-.*/'");
356    }
357}