Skip to main content

zer_blocking/keys/
date.rs

1use super::BlockingKey;
2use zer_core::{record::Record, schema::Schema};
3
4/// Controls how much of an ISO 8601 date is used as a blocking key.
5#[derive(Debug, Clone, Copy)]
6pub enum DateGranularity {
7    Year,
8    YearMonth,
9    YearMonthDay,
10}
11
12/// Blocking key that extracts the leading date fragment at a given granularity.
13pub struct DateFragmentKey {
14    dob_field: String,
15    granularity: DateGranularity,
16}
17
18impl DateFragmentKey {
19    pub fn new(dob_field: &str, granularity: DateGranularity) -> Self {
20        Self {
21            dob_field: dob_field.into(),
22            granularity,
23        }
24    }
25
26    fn fragment_len(granularity: DateGranularity) -> usize {
27        match granularity {
28            DateGranularity::Year => 4,
29            DateGranularity::YearMonth => 7,
30            DateGranularity::YearMonthDay => 10,
31        }
32    }
33}
34
35impl BlockingKey for DateFragmentKey {
36    fn name(&self) -> &str {
37        "date_fragment"
38    }
39
40    fn extract(&self, record: &Record, _schema: &Schema) -> Vec<String> {
41        let cow = record.field_as_str(&self.dob_field);
42        let raw = match cow.as_deref() {
43            Some(s) => s.trim(),
44            None => return vec![],
45        };
46
47        let len = Self::fragment_len(self.granularity);
48        if raw.len() < len {
49            return vec![];
50        }
51
52        let fragment = &raw[..len];
53        if !fragment
54            .chars()
55            .next()
56            .map(|c| c.is_ascii_digit())
57            .unwrap_or(false)
58        {
59            return vec![];
60        }
61
62        vec![fragment.to_string()]
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use zer_core::{
70        record::FieldValue,
71        schema::{FieldKind, SchemaBuilder},
72    };
73
74    fn schema() -> Schema {
75        SchemaBuilder::new()
76            .field("dob", FieldKind::Date)
77            .build()
78            .unwrap()
79    }
80
81    #[test]
82    fn year_fragment() {
83        let k = DateFragmentKey::new("dob", DateGranularity::Year);
84        let r = Record::new(1).insert("dob", FieldValue::Text("1985-06-23".into()));
85        assert_eq!(k.extract(&r, &schema()), vec!["1985"]);
86    }
87
88    #[test]
89    fn year_month_fragment() {
90        let k = DateFragmentKey::new("dob", DateGranularity::YearMonth);
91        let r = Record::new(1).insert("dob", FieldValue::Text("1985-06-23".into()));
92        assert_eq!(k.extract(&r, &schema()), vec!["1985-06"]);
93    }
94
95    #[test]
96    fn year_month_day_fragment() {
97        let k = DateFragmentKey::new("dob", DateGranularity::YearMonthDay);
98        let r = Record::new(1).insert("dob", FieldValue::Text("1985-06-23".into()));
99        assert_eq!(k.extract(&r, &schema()), vec!["1985-06-23"]);
100    }
101
102    #[test]
103    fn missing_field_returns_empty() {
104        let k = DateFragmentKey::new("dob", DateGranularity::Year);
105        let r = Record::new(1);
106        assert!(k.extract(&r, &schema()).is_empty());
107    }
108
109    #[test]
110    fn short_value_returns_empty() {
111        let k = DateFragmentKey::new("dob", DateGranularity::YearMonth);
112        let r = Record::new(1).insert("dob", FieldValue::Text("1985".into()));
113        assert!(k.extract(&r, &schema()).is_empty());
114    }
115}