dinero/
list.rs

1use regex::Regex;
2use std::collections::hash_map::{Iter, Values};
3use std::collections::HashMap;
4use std::fmt::Debug;
5use std::hash::Hash;
6use std::rc::Rc;
7
8use crate::error::LedgerError;
9use crate::models::{FromDirective, HasAliases, HasName};
10
11/// A generic container with some search capabilities
12///
13/// This structure is used to hold master elements of the ledger than can be aliases such as
14/// commodities or accounts
15///
16/// It provides methods for:
17/// - Adding new elements to the list
18/// - Adding new aliases to existing elements
19/// - Retrieving elements
20/// - Retrieving elements with a regular expression
21#[derive(Debug, Clone)]
22pub struct List<T> {
23    aliases: HashMap<String, String>,
24    list: HashMap<String, Rc<T>>,
25    matches: HashMap<String, Option<String>>,
26}
27impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> Default for List<T> {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32impl<'a, T: Eq + Hash + HasName + Clone + FromDirective + HasAliases + Debug> List<T> {
33    pub fn new() -> Self {
34        let aliases: HashMap<String, String> = HashMap::new();
35        let list: HashMap<String, Rc<T>> = HashMap::new();
36        let matches: HashMap<String, Option<String>> = HashMap::new();
37        List {
38            aliases,
39            list,
40            matches,
41        }
42    }
43
44    /// Inserts an ```element``` in the list
45    pub fn insert(&mut self, element: T) {
46        let found = self.list.get(&element.get_name().to_lowercase());
47        match found {
48            Some(_) => eprintln!("Duplicate element: {:?}", element), // do nothing
49            None => {
50                // Change the name which will be used as key to lowercase
51                let name = element.get_name().to_string().to_lowercase();
52                for alias in element.get_aliases().iter() {
53                    self.aliases.insert(alias.to_lowercase(), name.clone());
54                }
55                self.list.insert(name, Rc::new(element));
56            }
57        }
58    }
59    /// Removes an ```element``` in the list
60    pub fn remove(&mut self, element: &T) {
61        let found = self.list.get(&element.get_name().to_lowercase());
62        match found {
63            Some(x) => {
64                for alias in x.get_aliases() {
65                    self.aliases.remove(&alias.to_lowercase());
66                }
67                self.list.remove(&element.get_name().to_lowercase());
68            }
69            None => {
70                for alias in element.get_aliases() {
71                    let value = self.aliases.remove(&alias.to_lowercase());
72                    if let Some(x) = value {
73                        self.list.remove(&x);
74                    }
75                    self.list.remove(&alias.to_lowercase());
76                }
77            }
78        }
79    }
80    /// Add an alias
81    pub fn add_alias(&mut self, alias: String, for_element: &'a T) {
82        let element = self.aliases.get(&alias.to_lowercase());
83        match element {
84            Some(x) => panic!(
85                "Repeated alias {} for {} and {}",
86                alias,
87                for_element.get_name(),
88                x
89            ),
90            None => {
91                self.aliases
92                    .insert(alias.to_lowercase(), for_element.get_name().to_lowercase());
93            }
94        }
95    }
96
97    pub fn get(&self, index: &str) -> Result<&Rc<T>, LedgerError> {
98        match self.list.get(&index.to_lowercase()) {
99            None => match self.aliases.get(&index.to_lowercase()) {
100                None => Err(LedgerError::AliasNotInList(format!(
101                    "{} {:?} not found",
102                    std::any::type_name::<T>(),
103                    index
104                ))),
105                Some(x) => Ok(self.list.get(x).unwrap()),
106            },
107            Some(x) => Ok(x),
108        }
109    }
110    /// Gets an element from the regex
111    pub fn get_regex(&mut self, regex: Regex) -> Option<&Rc<T>> {
112        let alias = self.matches.get(regex.as_str());
113        match alias {
114            Some(x) => match x {
115                Some(alias) => Some(self.get(alias).unwrap()),
116                None => None,
117            },
118            None => {
119                // cache miss
120                for (_alias, value) in self.list.iter() {
121                    if regex.is_match(value.get_name()) {
122                        self.matches
123                            .insert(regex.as_str().to_string(), Some(_alias.clone()));
124                        return Some(value);
125                    }
126                }
127                for (alias, value) in self.aliases.iter() {
128                    if regex.is_match(alias) {
129                        self.matches
130                            .insert(regex.as_str().to_string(), Some(value.clone()));
131                        return self.list.get(value);
132                    }
133                }
134                self.matches.insert(regex.as_str().to_string(), None);
135                None
136            }
137        }
138        // // Try the list
139
140        // None
141    }
142
143    pub fn iter(&self) -> Iter<'_, String, Rc<T>> {
144        self.list.iter()
145    }
146    pub fn values(&self) -> Values<'_, String, Rc<T>> {
147        self.list.values()
148    }
149    pub fn len(&self) -> usize {
150        self.list.len()
151    }
152    pub fn is_empty(&self) -> bool {
153        self.list.is_empty()
154    }
155    pub fn len_alias(&self) -> usize {
156        self.aliases.len() + self.len()
157    }
158}
159
160impl<T: Clone + FromDirective + HasAliases + Debug + Eq + Hash + HasName> List<T> {
161    pub fn append(&mut self, other: &List<T>) {
162        for (key, value) in other.list.iter() {
163            if value.is_from_directive() {
164                self.list.insert(key.clone(), value.clone());
165                for alias in value.get_aliases().iter() {
166                    self.aliases.insert(alias.to_lowercase(), key.clone());
167                }
168            } else if self.get(key).is_err() {
169                self.list.insert(key.clone(), value.clone());
170            }
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::models::Payee;
179    use regex::Regex;
180    #[test]
181    fn list() {
182        let name = "ACME Inc.";
183        let payee = Payee::from(name);
184        let mut list: List<Payee> = List::new();
185        list.insert(payee.clone());
186
187        // Get ACME from the list, using a regex
188        let pattern = Regex::new("ACME").unwrap();
189        let retrieved = list.get_regex(pattern);
190
191        assert!(retrieved.is_some());
192        assert_eq!(retrieved.unwrap().get_name(), "ACME Inc.");
193        assert_eq!(list.len_alias(), 1);
194
195        // Now add and alias
196        list.add_alias("ACME is awesome".to_string(), &payee);
197        assert_eq!(list.len_alias(), 2);
198
199        // Retrieve an element that is not in the list
200        assert!(list.get_regex(Regex::new("Warner").unwrap()).is_none());
201        assert!(list.get("Warner").is_err());
202        assert!(list.get_regex(Regex::new("awesome").unwrap()).is_some());
203    }
204    #[test]
205    #[should_panic]
206    fn list_repeated_alias() {
207        let mut list: List<Payee> = List::new();
208        list.insert(Payee::from("ACME"));
209        for _ in 0..2 {
210            let retrieved = list.get("ACME").unwrap().clone();
211            list.add_alias("ACME, Inc.".to_string(), &retrieved)
212        }
213    }
214}