Skip to main content

haystack_core/ontology/
conjunct.rs

1// ConjunctIndex -- decomposition of compound tag names.
2
3use std::collections::HashMap;
4
5/// Maps conjunct def names to their component parts.
6///
7/// A conjunct like `"hot-water"` decomposes into `["hot", "water"]`.
8/// Components are the individual marker tags separated by `"-"`.
9pub struct ConjunctIndex {
10    /// conjunct name -> component tag list
11    parts: HashMap<String, Vec<String>>,
12}
13
14impl ConjunctIndex {
15    /// Create an empty conjunct index.
16    pub fn new() -> Self {
17        Self {
18            parts: HashMap::new(),
19        }
20    }
21
22    /// Register a conjunct decomposition.
23    pub fn register(&mut self, conjunct: &str, parts: Vec<String>) {
24        self.parts.insert(conjunct.to_string(), parts);
25    }
26
27    /// Get component tags for a conjunct.
28    ///
29    /// Returns `None` if not a registered conjunct.
30    pub fn decompose(&self, name: &str) -> Option<&[String]> {
31        self.parts.get(name).map(|v| v.as_slice())
32    }
33
34    /// Check if a name is a registered conjunct.
35    pub fn contains(&self, name: &str) -> bool {
36        self.parts.contains_key(name)
37    }
38
39    /// Number of registered conjuncts.
40    pub fn len(&self) -> usize {
41        self.parts.len()
42    }
43
44    /// Returns true if no conjuncts are registered.
45    pub fn is_empty(&self) -> bool {
46        self.parts.is_empty()
47    }
48}
49
50impl Default for ConjunctIndex {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn register_and_decompose() {
62        let mut idx = ConjunctIndex::new();
63        idx.register("hot-water", vec!["hot".to_string(), "water".to_string()]);
64
65        let parts = idx.decompose("hot-water").unwrap();
66        assert_eq!(parts, &["hot", "water"]);
67    }
68
69    #[test]
70    fn contains_check() {
71        let mut idx = ConjunctIndex::new();
72        idx.register("hot-water", vec!["hot".to_string(), "water".to_string()]);
73
74        assert!(idx.contains("hot-water"));
75        assert!(!idx.contains("cold-water"));
76    }
77
78    #[test]
79    fn unknown_returns_none() {
80        let idx = ConjunctIndex::new();
81        assert!(idx.decompose("nonexistent").is_none());
82    }
83
84    #[test]
85    fn len_and_empty() {
86        let mut idx = ConjunctIndex::new();
87        assert!(idx.is_empty());
88        assert_eq!(idx.len(), 0);
89
90        idx.register("hot-water", vec!["hot".to_string(), "water".to_string()]);
91        assert!(!idx.is_empty());
92        assert_eq!(idx.len(), 1);
93    }
94
95    #[test]
96    fn three_part_conjunct() {
97        let mut idx = ConjunctIndex::new();
98        idx.register(
99            "ac-elec-meter",
100            vec!["ac".to_string(), "elec".to_string(), "meter".to_string()],
101        );
102
103        let parts = idx.decompose("ac-elec-meter").unwrap();
104        assert_eq!(parts, &["ac", "elec", "meter"]);
105    }
106}