zer_compare/similarity/
numeric.rs1use zer_core::{record::FieldValue, schema::FieldKind};
2
3use crate::similarity::SimilarityFn;
4
5pub struct NumericBucketedSimilarity;
18
19fn extract_numeric(v: &FieldValue) -> Option<f64> {
20 match v {
21 FieldValue::Float(f) => Some(*f),
22 FieldValue::Int(i) => Some(*i as f64),
23 FieldValue::Text(s) => s.trim().parse::<f64>().ok(),
24 _ => None,
25 }
26}
27
28fn numeric_score(va: f64, vb: f64) -> f32 {
29 let diff = (va - vb).abs();
30 if diff == 0.0 {
31 return 1.0;
32 }
33 let denom = va.abs().max(vb.abs()).max(1.0);
34 let rel_diff = diff / denom;
35 if rel_diff <= 0.05 {
36 0.85
37 } else if rel_diff <= 0.20 {
38 0.6
39 } else if rel_diff <= 0.50 {
40 0.3
41 } else {
42 0.0
43 }
44}
45
46impl SimilarityFn for NumericBucketedSimilarity {
47 fn similarity(&self, a: &FieldValue, b: &FieldValue) -> f32 {
48 match (extract_numeric(a), extract_numeric(b)) {
49 (Some(va), Some(vb)) => numeric_score(va, vb),
50 _ => 0.0,
51 }
52 }
53 fn similarity_str(&self, a: &str, b: &str) -> f32 {
54 match (a.trim().parse::<f64>(), b.trim().parse::<f64>()) {
55 (Ok(va), Ok(vb)) => numeric_score(va, vb),
56 _ => 0.0,
57 }
58 }
59 fn field_kind(&self) -> FieldKind {
60 FieldKind::Numeric
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 fn ti(n: i64) -> FieldValue {
69 FieldValue::Int(n)
70 }
71 fn tf(f: f64) -> FieldValue {
72 FieldValue::Float(f)
73 }
74 fn tv(s: &str) -> FieldValue {
75 FieldValue::Text(s.into())
76 }
77
78 #[test]
79 fn exact_int_match() {
80 let sim = NumericBucketedSimilarity;
81 assert_eq!(sim.similarity(&ti(180), &ti(180)), 1.0);
82 }
83
84 #[test]
85 fn close_within_5_percent() {
86 let sim = NumericBucketedSimilarity;
87 assert_eq!(sim.similarity(&ti(180), &ti(183)), 0.85);
89 }
90
91 #[test]
92 fn medium_within_20_percent() {
93 let sim = NumericBucketedSimilarity;
94 assert_eq!(sim.similarity(&ti(100), &ti(115)), 0.6);
96 }
97
98 #[test]
99 fn large_within_50_percent() {
100 let sim = NumericBucketedSimilarity;
101 assert_eq!(sim.similarity(&ti(100), &ti(140)), 0.3);
103 }
104
105 #[test]
106 fn very_different() {
107 let sim = NumericBucketedSimilarity;
108 assert_eq!(sim.similarity(&ti(100), &ti(300)), 0.0);
109 }
110
111 #[test]
112 fn float_parsing_from_text() {
113 let sim = NumericBucketedSimilarity;
114 assert_eq!(sim.similarity(&tv("52.345"), &tv("52.346")), 0.85);
116 }
117
118 #[test]
119 fn mixed_int_float() {
120 let sim = NumericBucketedSimilarity;
121 assert_eq!(sim.similarity(&ti(100), &tf(100.0)), 1.0);
122 }
123
124 #[test]
125 fn null_returns_zero() {
126 let sim = NumericBucketedSimilarity;
127 assert_eq!(sim.similarity(&FieldValue::Null, &ti(100)), 0.0);
128 }
129}