Skip to main content

zer_blocking/keys/
date.rs

1use zer_core::{record::Record, schema::Schema};
2use super::BlockingKey;
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 { dob_field: dob_field.into(), granularity }
21    }
22
23    fn fragment_len(granularity: DateGranularity) -> usize {
24        match granularity {
25            DateGranularity::Year        => 4,
26            DateGranularity::YearMonth   => 7,
27            DateGranularity::YearMonthDay => 10,
28        }
29    }
30}
31
32impl BlockingKey for DateFragmentKey {
33    fn name(&self) -> &str {
34        "date_fragment"
35    }
36
37    fn extract(&self, record: &Record, _schema: &Schema) -> Vec<String> {
38        let cow = record.field_as_str(&self.dob_field);
39        let raw = match cow.as_deref() {
40            Some(s) => s.trim(),
41            None    => return vec![],
42        };
43
44        let len = Self::fragment_len(self.granularity);
45        if raw.len() < len {
46            return vec![];
47        }
48
49        let fragment = &raw[..len];
50        if !fragment.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
51            return vec![];
52        }
53
54        vec![fragment.to_string()]
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use zer_core::{record::FieldValue, schema::{SchemaBuilder, FieldKind}};
62
63    fn schema() -> Schema {
64        SchemaBuilder::new().field("dob", FieldKind::Date).build().unwrap()
65    }
66
67    #[test]
68    fn year_fragment() {
69        let k = DateFragmentKey::new("dob", DateGranularity::Year);
70        let r = Record::new(1).insert("dob", FieldValue::Text("1985-06-23".into()));
71        assert_eq!(k.extract(&r, &schema()), vec!["1985"]);
72    }
73
74    #[test]
75    fn year_month_fragment() {
76        let k = DateFragmentKey::new("dob", DateGranularity::YearMonth);
77        let r = Record::new(1).insert("dob", FieldValue::Text("1985-06-23".into()));
78        assert_eq!(k.extract(&r, &schema()), vec!["1985-06"]);
79    }
80
81    #[test]
82    fn year_month_day_fragment() {
83        let k = DateFragmentKey::new("dob", DateGranularity::YearMonthDay);
84        let r = Record::new(1).insert("dob", FieldValue::Text("1985-06-23".into()));
85        assert_eq!(k.extract(&r, &schema()), vec!["1985-06-23"]);
86    }
87
88    #[test]
89    fn missing_field_returns_empty() {
90        let k = DateFragmentKey::new("dob", DateGranularity::Year);
91        let r = Record::new(1);
92        assert!(k.extract(&r, &schema()).is_empty());
93    }
94
95    #[test]
96    fn short_value_returns_empty() {
97        let k = DateFragmentKey::new("dob", DateGranularity::YearMonth);
98        let r = Record::new(1).insert("dob", FieldValue::Text("1985".into()));
99        assert!(k.extract(&r, &schema()).is_empty());
100    }
101}