layered_amount/
amounts.rs

1use layered_nlp::{x, LLCursorAssignment, LLSelection, Resolver, TextTag};
2use rust_decimal::Decimal;
3
4#[derive(Clone, Debug)]
5pub struct Amount(Decimal);
6
7impl Amount {
8    pub fn get_decimal(&self) -> &Decimal {
9        &self.0
10    }
11    pub fn new<T: Into<Decimal>>(from: T) -> Self {
12        Self(from.into())
13    }
14}
15
16impl From<Decimal> for Amount {
17    fn from(dec: Decimal) -> Self {
18        Amount(dec)
19    }
20}
21
22pub struct AmountResolver {
23    /// Configure for localization
24    delimiters: Vec<char>,
25    decimal: char,
26}
27
28impl AmountResolver {
29    pub fn new(delimiters: Vec<char>, decimal: char) -> Self {
30        Self {
31            delimiters,
32            decimal,
33        }
34    }
35    /// Default comma `,` and apostrophe `'` delimeters, with period `.` decimal point.
36    ///  * `1'000'240.00` = `1000240.00`
37    ///  * `1,000,000` = `1000000`
38    ///  * `1,00,00` = `10000`
39    ///  * `1.00,00` = `1.0000`
40    pub fn english() -> Self {
41        Self {
42            delimiters: vec![',', '\''],
43            decimal: '.',
44        }
45    }
46    /// Default period `.`, apostrophe `'`, and space ` ` delimeters, with comma `,` decimal point.
47    ///  * `1'000'240,00` = `1000240.00`
48    ///  * `1 000 000` = `1000000`
49    ///  * `1,00` = `1.00`
50    ///  * `1.00,00` = `100.00`
51    pub fn french() -> Self {
52        Self {
53            delimiters: vec![' ', '.', '\''],
54            decimal: '.',
55        }
56    }
57}
58
59impl Resolver for AmountResolver {
60    type Attr = Amount;
61
62    fn go(&self, mut search_range_sel: LLSelection) -> Vec<LLCursorAssignment<Self::Attr>> {
63        let mut attrs = vec![];
64
65        while let Some((mut selection, (_, text))) =
66            search_range_sel.find_first_by(&x::all((x::attr_eq(&TextTag::NATN), x::token_text())))
67        {
68            let mut number_string = String::from(text);
69            let mut last_valid_selection = None;
70
71            // Avoid trailing delimeters
72            loop {
73                if let Some((delimeter_sel, _)) =
74                    selection.match_first_forwards(&x::token_has_any(self.delimiters.as_slice()))
75                {
76                    last_valid_selection = Some(selection);
77                    selection = delimeter_sel;
78                } else {
79                    break;
80                }
81
82                if let Some((following_delimeter_sel, (_, text))) = selection
83                    .match_first_forwards(&x::all((x::attr_eq(&TextTag::NATN), x::token_text())))
84                {
85                    number_string.push_str(text);
86                    last_valid_selection = None;
87                    selection = following_delimeter_sel;
88                } else {
89                    break;
90                }
91            }
92
93            if last_valid_selection.is_none() {
94                // 100,,20
95                if let Some((with_decimal_sel, _)) =
96                    selection.match_first_forwards(&x::token_has_any(&[self.decimal]))
97                {
98                    last_valid_selection = Some(selection);
99                    number_string.push('.');
100                    selection = with_decimal_sel;
101                }
102
103                if let Some((following_decimal_sel, ((), text))) = selection
104                    .match_first_forwards(&x::all((x::attr_eq(&TextTag::NATN), x::token_text())))
105                {
106                    number_string.push_str(text);
107                    last_valid_selection = None;
108                    selection = following_decimal_sel;
109                }
110            }
111
112            attrs.push(
113                last_valid_selection
114                    .unwrap_or_else(|| selection.clone())
115                    .finish_with_attr(Amount(number_string.parse::<Decimal>().unwrap())),
116            );
117
118            if let [_, Some(right_sel)] = search_range_sel.split_with(&selection) {
119                search_range_sel = right_sel;
120            } else {
121                break;
122            }
123        }
124
125        attrs
126    }
127}