lnmp_codec/
equivalence.rs

1//! Semantic equivalence mapping system.
2//!
3//! This module provides equivalence mapping to recognize synonyms and related terms
4//! as semantically equivalent. Mappings are field-specific, allowing different
5//! equivalence rules for different fields.
6//!
7//! # Examples
8//!
9//! ```
10//! use lnmp_codec::EquivalenceMapper;
11//!
12//! let mut mapper = EquivalenceMapper::new();
13//!
14//! // Add mapping for field 7 (is_active)
15//! mapper.add_mapping(7, "yes".to_string(), "1".to_string());
16//! mapper.add_mapping(7, "true".to_string(), "1".to_string());
17//! mapper.add_mapping(7, "no".to_string(), "0".to_string());
18//! mapper.add_mapping(7, "false".to_string(), "0".to_string());
19//!
20//! // Map values to canonical form
21//! assert_eq!(mapper.map(7, "yes"), Some("1".to_string()));
22//! assert_eq!(mapper.map(7, "true"), Some("1".to_string()));
23//! assert_eq!(mapper.map(7, "no"), Some("0".to_string()));
24//! assert_eq!(mapper.map(7, "unmapped"), None);
25//! ```
26
27use lnmp_core::FieldId;
28use std::collections::HashMap;
29
30/// Equivalence mapper for semantic synonym mapping
31///
32/// Maps field values to their canonical forms based on field-specific
33/// equivalence rules. This enables recognition of synonyms like
34/// "admin" → "administrator" or "yes" → "1".
35#[derive(Debug, Clone)]
36pub struct EquivalenceMapper {
37    /// Field-specific mappings: FieldId → (from_value → to_value)
38    mappings: HashMap<FieldId, HashMap<String, String>>,
39}
40
41impl EquivalenceMapper {
42    /// Creates a new empty equivalence mapper
43    pub fn new() -> Self {
44        Self {
45            mappings: HashMap::new(),
46        }
47    }
48
49    /// Creates a mapper with default boolean equivalences
50    ///
51    /// Provides common boolean synonym mappings that can be applied
52    /// to any field by calling `apply_default_bool_mappings(fid)`.
53    pub fn with_defaults() -> Self {
54        Self::new()
55    }
56
57    /// Adds a custom mapping for a specific field
58    ///
59    /// # Arguments
60    ///
61    /// * `fid` - The field ID to add the mapping for
62    /// * `from` - The source value to map from
63    /// * `to` - The canonical value to map to
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use lnmp_codec::EquivalenceMapper;
69    ///
70    /// let mut mapper = EquivalenceMapper::new();
71    /// mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
72    /// mapper.add_mapping(12, "dev".to_string(), "developer".to_string());
73    ///
74    /// assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
75    /// assert_eq!(mapper.map(12, "dev"), Some("developer".to_string()));
76    /// ```
77    pub fn add_mapping(&mut self, fid: FieldId, from: String, to: String) {
78        self.mappings.entry(fid).or_default().insert(from, to);
79    }
80
81    /// Adds multiple mappings for a specific field
82    ///
83    /// # Arguments
84    ///
85    /// * `fid` - The field ID to add mappings for
86    /// * `mappings` - Iterator of (from, to) value pairs
87    pub fn add_mappings<I>(&mut self, fid: FieldId, mappings: I)
88    where
89        I: IntoIterator<Item = (String, String)>,
90    {
91        let field_mappings = self.mappings.entry(fid).or_default();
92        for (from, to) in mappings {
93            field_mappings.insert(from, to);
94        }
95    }
96
97    /// Applies default boolean equivalence mappings to a field
98    ///
99    /// Maps common boolean representations to "1" (true) or "0" (false):
100    /// - "yes", "true" → "1"
101    /// - "no", "false" → "0"
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use lnmp_codec::EquivalenceMapper;
107    ///
108    /// let mut mapper = EquivalenceMapper::new();
109    /// mapper.apply_default_bool_mappings(7);
110    ///
111    /// assert_eq!(mapper.map(7, "yes"), Some("1".to_string()));
112    /// assert_eq!(mapper.map(7, "true"), Some("1".to_string()));
113    /// assert_eq!(mapper.map(7, "no"), Some("0".to_string()));
114    /// assert_eq!(mapper.map(7, "false"), Some("0".to_string()));
115    /// ```
116    pub fn apply_default_bool_mappings(&mut self, fid: FieldId) {
117        let bool_mappings = vec![
118            ("yes".to_string(), "1".to_string()),
119            ("Yes".to_string(), "1".to_string()),
120            ("YES".to_string(), "1".to_string()),
121            ("true".to_string(), "1".to_string()),
122            ("True".to_string(), "1".to_string()),
123            ("TRUE".to_string(), "1".to_string()),
124            ("no".to_string(), "0".to_string()),
125            ("No".to_string(), "0".to_string()),
126            ("NO".to_string(), "0".to_string()),
127            ("false".to_string(), "0".to_string()),
128            ("False".to_string(), "0".to_string()),
129            ("FALSE".to_string(), "0".to_string()),
130        ];
131        self.add_mappings(fid, bool_mappings);
132    }
133
134    /// Maps a value to its canonical form for a specific field
135    ///
136    /// Returns `Some(canonical_value)` if a mapping exists, or `None` if
137    /// the value has no mapping for this field.
138    ///
139    /// # Arguments
140    ///
141    /// * `fid` - The field ID to look up mappings for
142    /// * `value` - The value to map
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use lnmp_codec::EquivalenceMapper;
148    ///
149    /// let mut mapper = EquivalenceMapper::new();
150    /// mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
151    ///
152    /// assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
153    /// assert_eq!(mapper.map(12, "user"), None);
154    /// assert_eq!(mapper.map(99, "admin"), None); // Different field
155    /// ```
156    pub fn map(&self, fid: FieldId, value: &str) -> Option<String> {
157        self.mappings
158            .get(&fid)
159            .and_then(|field_mappings| field_mappings.get(value))
160            .cloned()
161    }
162
163    /// Checks if a mapping exists for a specific field and value
164    ///
165    /// # Arguments
166    ///
167    /// * `fid` - The field ID to check
168    /// * `value` - The value to check for a mapping
169    pub fn has_mapping(&self, fid: FieldId, value: &str) -> bool {
170        self.mappings
171            .get(&fid)
172            .map(|field_mappings| field_mappings.contains_key(value))
173            .unwrap_or(false)
174    }
175
176    /// Returns the number of fields with mappings
177    pub fn field_count(&self) -> usize {
178        self.mappings.len()
179    }
180
181    /// Returns the number of mappings for a specific field
182    ///
183    /// # Arguments
184    ///
185    /// * `fid` - The field ID to count mappings for
186    pub fn mapping_count(&self, fid: FieldId) -> usize {
187        self.mappings
188            .get(&fid)
189            .map(|field_mappings| field_mappings.len())
190            .unwrap_or(0)
191    }
192
193    /// Clears all mappings for a specific field
194    ///
195    /// # Arguments
196    ///
197    /// * `fid` - The field ID to clear mappings for
198    pub fn clear_field(&mut self, fid: FieldId) {
199        self.mappings.remove(&fid);
200    }
201
202    /// Clears all mappings
203    pub fn clear(&mut self) {
204        self.mappings.clear();
205    }
206}
207
208impl Default for EquivalenceMapper {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_new_mapper_is_empty() {
220        let mapper = EquivalenceMapper::new();
221        assert_eq!(mapper.field_count(), 0);
222    }
223
224    #[test]
225    fn test_add_single_mapping() {
226        let mut mapper = EquivalenceMapper::new();
227        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
228
229        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
230        assert_eq!(mapper.map(12, "user"), None);
231        assert_eq!(mapper.field_count(), 1);
232        assert_eq!(mapper.mapping_count(12), 1);
233    }
234
235    #[test]
236    fn test_add_multiple_mappings_same_field() {
237        let mut mapper = EquivalenceMapper::new();
238        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
239        mapper.add_mapping(12, "dev".to_string(), "developer".to_string());
240        mapper.add_mapping(12, "qa".to_string(), "quality_assurance".to_string());
241
242        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
243        assert_eq!(mapper.map(12, "dev"), Some("developer".to_string()));
244        assert_eq!(mapper.map(12, "qa"), Some("quality_assurance".to_string()));
245        assert_eq!(mapper.field_count(), 1);
246        assert_eq!(mapper.mapping_count(12), 3);
247    }
248
249    #[test]
250    fn test_add_mappings_different_fields() {
251        let mut mapper = EquivalenceMapper::new();
252        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
253        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
254
255        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
256        assert_eq!(mapper.map(7, "yes"), Some("1".to_string()));
257        assert_eq!(mapper.map(12, "yes"), None); // Different field
258        assert_eq!(mapper.map(7, "admin"), None); // Different field
259        assert_eq!(mapper.field_count(), 2);
260    }
261
262    #[test]
263    fn test_add_mappings_bulk() {
264        let mut mapper = EquivalenceMapper::new();
265        let mappings = vec![
266            ("admin".to_string(), "administrator".to_string()),
267            ("dev".to_string(), "developer".to_string()),
268            ("qa".to_string(), "quality_assurance".to_string()),
269        ];
270        mapper.add_mappings(12, mappings);
271
272        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
273        assert_eq!(mapper.map(12, "dev"), Some("developer".to_string()));
274        assert_eq!(mapper.map(12, "qa"), Some("quality_assurance".to_string()));
275        assert_eq!(mapper.mapping_count(12), 3);
276    }
277
278    #[test]
279    fn test_apply_default_bool_mappings() {
280        let mut mapper = EquivalenceMapper::new();
281        mapper.apply_default_bool_mappings(7);
282
283        // Test true variants
284        assert_eq!(mapper.map(7, "yes"), Some("1".to_string()));
285        assert_eq!(mapper.map(7, "Yes"), Some("1".to_string()));
286        assert_eq!(mapper.map(7, "YES"), Some("1".to_string()));
287        assert_eq!(mapper.map(7, "true"), Some("1".to_string()));
288        assert_eq!(mapper.map(7, "True"), Some("1".to_string()));
289        assert_eq!(mapper.map(7, "TRUE"), Some("1".to_string()));
290
291        // Test false variants
292        assert_eq!(mapper.map(7, "no"), Some("0".to_string()));
293        assert_eq!(mapper.map(7, "No"), Some("0".to_string()));
294        assert_eq!(mapper.map(7, "NO"), Some("0".to_string()));
295        assert_eq!(mapper.map(7, "false"), Some("0".to_string()));
296        assert_eq!(mapper.map(7, "False"), Some("0".to_string()));
297        assert_eq!(mapper.map(7, "FALSE"), Some("0".to_string()));
298
299        // Test unmapped values
300        assert_eq!(mapper.map(7, "maybe"), None);
301        assert_eq!(mapper.map(7, "1"), None);
302        assert_eq!(mapper.map(7, "0"), None);
303    }
304
305    #[test]
306    fn test_has_mapping() {
307        let mut mapper = EquivalenceMapper::new();
308        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
309
310        assert!(mapper.has_mapping(12, "admin"));
311        assert!(!mapper.has_mapping(12, "user"));
312        assert!(!mapper.has_mapping(7, "admin"));
313    }
314
315    #[test]
316    fn test_mapping_count() {
317        let mut mapper = EquivalenceMapper::new();
318        assert_eq!(mapper.mapping_count(12), 0);
319
320        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
321        assert_eq!(mapper.mapping_count(12), 1);
322
323        mapper.add_mapping(12, "dev".to_string(), "developer".to_string());
324        assert_eq!(mapper.mapping_count(12), 2);
325
326        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
327        assert_eq!(mapper.mapping_count(12), 2);
328        assert_eq!(mapper.mapping_count(7), 1);
329    }
330
331    #[test]
332    fn test_field_count() {
333        let mut mapper = EquivalenceMapper::new();
334        assert_eq!(mapper.field_count(), 0);
335
336        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
337        assert_eq!(mapper.field_count(), 1);
338
339        mapper.add_mapping(12, "dev".to_string(), "developer".to_string());
340        assert_eq!(mapper.field_count(), 1); // Same field
341
342        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
343        assert_eq!(mapper.field_count(), 2); // Different field
344    }
345
346    #[test]
347    fn test_clear_field() {
348        let mut mapper = EquivalenceMapper::new();
349        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
350        mapper.add_mapping(12, "dev".to_string(), "developer".to_string());
351        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
352
353        assert_eq!(mapper.field_count(), 2);
354        assert_eq!(mapper.mapping_count(12), 2);
355
356        mapper.clear_field(12);
357        assert_eq!(mapper.field_count(), 1);
358        assert_eq!(mapper.mapping_count(12), 0);
359        assert_eq!(mapper.mapping_count(7), 1);
360        assert_eq!(mapper.map(12, "admin"), None);
361        assert_eq!(mapper.map(7, "yes"), Some("1".to_string()));
362    }
363
364    #[test]
365    fn test_clear_all() {
366        let mut mapper = EquivalenceMapper::new();
367        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
368        mapper.add_mapping(7, "yes".to_string(), "1".to_string());
369
370        assert_eq!(mapper.field_count(), 2);
371
372        mapper.clear();
373        assert_eq!(mapper.field_count(), 0);
374        assert_eq!(mapper.map(12, "admin"), None);
375        assert_eq!(mapper.map(7, "yes"), None);
376    }
377
378    #[test]
379    fn test_overwrite_mapping() {
380        let mut mapper = EquivalenceMapper::new();
381        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
382        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
383
384        // Overwrite with new mapping
385        mapper.add_mapping(12, "admin".to_string(), "superuser".to_string());
386        assert_eq!(mapper.map(12, "admin"), Some("superuser".to_string()));
387        assert_eq!(mapper.mapping_count(12), 1);
388    }
389
390    #[test]
391    fn test_case_sensitive_mapping() {
392        let mut mapper = EquivalenceMapper::new();
393        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
394        mapper.add_mapping(12, "Admin".to_string(), "Administrator".to_string());
395
396        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
397        assert_eq!(mapper.map(12, "Admin"), Some("Administrator".to_string()));
398        assert_eq!(mapper.map(12, "ADMIN"), None);
399    }
400
401    #[test]
402    fn test_empty_string_mapping() {
403        let mut mapper = EquivalenceMapper::new();
404        mapper.add_mapping(12, "".to_string(), "empty".to_string());
405
406        assert_eq!(mapper.map(12, ""), Some("empty".to_string()));
407    }
408
409    #[test]
410    fn test_with_defaults() {
411        let mapper = EquivalenceMapper::with_defaults();
412        assert_eq!(mapper.field_count(), 0);
413    }
414
415    #[test]
416    fn test_default_trait() {
417        let mapper = EquivalenceMapper::default();
418        assert_eq!(mapper.field_count(), 0);
419    }
420
421    #[test]
422    fn test_clone() {
423        let mut mapper = EquivalenceMapper::new();
424        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
425
426        let cloned = mapper.clone();
427        assert_eq!(cloned.map(12, "admin"), Some("administrator".to_string()));
428        assert_eq!(cloned.field_count(), 1);
429    }
430
431    #[test]
432    fn test_multiple_fields_with_same_mapping_value() {
433        let mut mapper = EquivalenceMapper::new();
434        mapper.add_mapping(12, "admin".to_string(), "administrator".to_string());
435        mapper.add_mapping(15, "admin".to_string(), "admin_user".to_string());
436
437        // Same source value, different canonical values per field
438        assert_eq!(mapper.map(12, "admin"), Some("administrator".to_string()));
439        assert_eq!(mapper.map(15, "admin"), Some("admin_user".to_string()));
440    }
441
442    #[test]
443    fn test_unicode_mapping() {
444        let mut mapper = EquivalenceMapper::new();
445        mapper.add_mapping(12, "café".to_string(), "coffee_shop".to_string());
446        mapper.add_mapping(12, "日本".to_string(), "japan".to_string());
447
448        assert_eq!(mapper.map(12, "café"), Some("coffee_shop".to_string()));
449        assert_eq!(mapper.map(12, "日本"), Some("japan".to_string()));
450    }
451}