zer_blocking/keys/
suffix.rs1use zer_core::{record::Record, schema::Schema};
2
3use super::BlockingKey;
4use crate::normalize::normalize_digits_only;
5
6pub struct SuffixKey {
8 field: String,
9 n: usize,
10}
11
12impl SuffixKey {
13 pub fn new(field: &str, n: usize) -> Self {
14 Self {
15 field: field.into(),
16 n,
17 }
18 }
19}
20
21impl BlockingKey for SuffixKey {
22 fn name(&self) -> &str {
23 "suffix"
24 }
25
26 fn extract(&self, record: &Record, _schema: &Schema) -> Vec<String> {
27 let cow = record.field_as_str(&self.field);
28 let raw = match cow.as_deref() {
29 Some(s) => s,
30 None => return vec![],
31 };
32
33 let digits = normalize_digits_only(raw);
34 if digits.len() < self.n {
35 return vec![];
36 }
37
38 let suffix = digits[digits.len() - self.n..].to_string();
39 vec![suffix]
40 }
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use zer_core::{
47 record::FieldValue,
48 schema::{FieldKind, SchemaBuilder},
49 };
50
51 fn schema() -> Schema {
52 SchemaBuilder::new()
53 .field("phone", FieldKind::Phone)
54 .build()
55 .unwrap()
56 }
57
58 #[test]
59 fn suffix_strips_punctuation_and_takes_last_n() {
60 let k = SuffixKey::new("phone", 7);
61 let r = Record::new(1).insert("phone", FieldValue::Text("555-123-4567".into()));
62 assert_eq!(k.extract(&r, &schema()), vec!["1234567"]);
63 }
64
65 #[test]
66 fn suffix_too_short_returns_empty() {
67 let k = SuffixKey::new("phone", 7);
68 let r = Record::new(1).insert("phone", FieldValue::Text("12345".into()));
69 assert!(k.extract(&r, &schema()).is_empty());
70 }
71
72 #[test]
73 fn same_last_digits_collide() {
74 let k = SuffixKey::new("phone", 4);
75 let s = schema();
76 let r1 = Record::new(1).insert("phone", FieldValue::Text("06-1234".into()));
77 let r2 = Record::new(2).insert("phone", FieldValue::Text("+31-20-001234".into()));
78 assert_eq!(k.extract(&r1, &s), k.extract(&r2, &s));
79 }
80}