Skip to main content

dinero/models/
currency.rs

1use std::cell::RefCell;
2use std::collections::HashSet;
3use std::fmt;
4use std::fmt::{Display, Formatter};
5use std::hash::{Hash, Hasher};
6
7use super::super::parser::{GrammarParser, Rule};
8use crate::models::{FromDirective, HasAliases, HasName, Origin};
9use pest::Parser;
10use std::cmp::Ordering;
11/// Currency representation
12///
13/// A currency (or commodity) has a name and a list of aliases, we have to make sure that when two commodities are
14/// created, they are the same, like so:
15///
16/// # Examples
17/// ```rust
18/// use dinero::models::{Currency};
19/// use dinero::List;
20///
21/// let usd1 = Currency::from("usd");
22/// let usd2 = Currency::from("usd");
23/// assert_eq!(usd1, usd2);
24///
25/// let eur1 = Currency::from("eur");
26/// assert_ne!(eur1, usd1);
27///
28/// let mut eur2 =  Currency::from("eur");
29/// assert_eq!(eur1, eur2);
30///
31/// let mut currencies = List::<Currency>::new();
32/// currencies.insert(eur1);
33/// currencies.insert(eur2);
34/// currencies.insert(usd1);
35/// currencies.insert(usd2);
36/// assert_eq!(currencies.len_alias(), 2, "Alias len should be 2");
37/// let eur = Currency::from("eur");
38/// currencies.add_alias("euro".to_string(), &eur);
39/// assert_eq!(currencies.len_alias(), 3, "Alias len should be 3");
40/// currencies.add_alias('€'.to_string(), &eur);
41/// assert_eq!(currencies.len(), 2, "List len should be 2");
42/// assert_eq!(currencies.len_alias(), 4, "Alias len should be 4");
43/// assert_eq!(currencies.get("eur").unwrap().as_ref(), &eur);
44/// assert_eq!(currencies.get("€").unwrap().as_ref(), &eur);
45///
46///
47/// assert_eq!(currencies.get("eur").unwrap(), currencies.get("€").unwrap(), "EUR and € should be the same");
48///
49/// ```
50#[derive(Debug, Clone)]
51pub struct Currency {
52    name: String,
53    origin: Origin,
54    note: Option<String>,
55    aliases: HashSet<String>,
56    pub(crate) format: Option<String>,
57    default: bool,
58    pub(crate) display_format: RefCell<CurrencyDisplayFormat>,
59}
60
61/// Definition of how to display a currency
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct CurrencyDisplayFormat {
64    pub symbol_placement: CurrencySymbolPlacement,
65    pub negative_amount_display: NegativeAmountDisplay,
66    pub decimal_separator: Separator,
67    pub digit_grouping: DigitGrouping,
68    pub thousands_separator: Option<Separator>,
69    pub precision: usize,
70    pub max_decimals: Option<usize>,
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum CurrencySymbolPlacement {
75    BeforeAmount,
76    AfterAmount,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum NegativeAmountDisplay {
81    BeforeSymbolAndNumber,      // UK   -£127.54   or Spain  -127,54 €
82    BeforeNumberBehindCurrency, // Denmark	kr-127,54
83    AfterNumber,                // Netherlands € 127,54-
84    Parentheses,                // US	($127.54)
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum DigitGrouping {
89    Thousands,
90    Indian,
91    None,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum Separator {
96    Dot,
97    Comma,
98    Space,
99    Other(char),
100}
101
102impl Currency {
103    pub fn from_directive(name: String) -> Self {
104        Currency {
105            name,
106            origin: Origin::FromDirective,
107            note: None,
108            aliases: HashSet::new(),
109            format: None,
110            default: false,
111            display_format: RefCell::new(DEFAULT_DISPLAY_FORMAT),
112        }
113    }
114
115    pub fn set_note(&mut self, note: String) {
116        self.note = Some(note);
117    }
118    pub fn set_default(&mut self) {
119        self.default = true;
120    }
121    pub fn set_aliases(&mut self, aliases: HashSet<String>) {
122        self.aliases = aliases;
123    }
124    pub fn set_precision(&self, precision: usize) {
125        self.display_format.borrow_mut().set_precision(precision);
126    }
127    pub fn get_precision(&self) -> usize {
128        self.display_format.borrow().precision
129    }
130    pub fn update_precision(&self, precision: usize) {
131        self.display_format.borrow_mut().update_precision(precision);
132    }
133    pub fn set_format(&self, format: &CurrencyDisplayFormat) {
134        let mut current_format = self.display_format.borrow_mut();
135        current_format.symbol_placement = format.symbol_placement;
136        current_format.negative_amount_display = format.negative_amount_display;
137        current_format.decimal_separator = format.decimal_separator;
138        current_format.digit_grouping = format.digit_grouping;
139        current_format.thousands_separator = format.thousands_separator;
140        current_format.precision = format.precision;
141        current_format.max_decimals = format.max_decimals;
142        // dbg!(format);
143    }
144}
145impl CurrencyDisplayFormat {
146    pub fn get_decimal_separator_str(&self) -> char {
147        match self.decimal_separator {
148            Separator::Dot => '.',
149            Separator::Comma => ',',
150            Separator::Space => '\u{202f}',
151            Separator::Other(x) => x,
152        }
153    }
154    pub fn set_decimal_separator(&mut self, separator: char) {
155        self.decimal_separator = match separator {
156            '.' => Separator::Dot,
157            ',' => Separator::Comma,
158            x => Separator::Other(x),
159        };
160    }
161    pub fn get_thousands_separator_str(&self) -> Option<char> {
162        match self.thousands_separator {
163            Some(Separator::Dot) => Some('.'),
164            Some(Separator::Comma) => Some(','),
165            Some(Separator::Space) => Some('\u{202f}'),
166            Some(Separator::Other(x)) => Some(x),
167            None => None,
168        }
169    }
170    pub fn set_thousands_separator(&mut self, separator: char) {
171        self.thousands_separator = match separator {
172            '.' => Some(Separator::Dot),
173            ',' => Some(Separator::Comma),
174            '\u{202f}' => Some(Separator::Space),
175            x => Some(Separator::Other(x)),
176        };
177    }
178    pub fn get_digit_grouping(&self) -> DigitGrouping {
179        self.digit_grouping
180    }
181    pub fn set_digit_grouping(&mut self, grouping: DigitGrouping) {
182        self.digit_grouping = grouping
183    }
184    pub fn update_precision(&mut self, precision: usize) {
185        if precision > self.precision {
186            self.precision = precision;
187        }
188    }
189    pub fn set_precision(&mut self, precision: usize) {
190        self.max_decimals = Some(precision);
191    }
192    pub fn get_precision(&self) -> usize {
193        match self.max_decimals {
194            Some(precision) => precision,
195            None => self.precision,
196        }
197    }
198}
199
200impl From<&str> for CurrencyDisplayFormat {
201    /// Sets the format of the currency representation
202    fn from(format: &str) -> Self {
203        // dbg!(&format);
204        let mut display_format = DEFAULT_DISPLAY_FORMAT;
205        let mut parsed = GrammarParser::parse(Rule::currency_format, format)
206            .unwrap()
207            .next()
208            .unwrap()
209            .into_inner();
210
211        let mut first = parsed.next().unwrap();
212        let integer_format;
213
214        if first.as_rule() == Rule::currency_format_positive {
215            display_format.negative_amount_display = NegativeAmountDisplay::BeforeSymbolAndNumber;
216            if first.as_str().starts_with('(') {
217                display_format.negative_amount_display = NegativeAmountDisplay::Parentheses;
218            }
219            parsed = first.into_inner();
220            first = parsed.next().unwrap();
221        }
222        match first.as_rule() {
223            Rule::integer_part => {
224                integer_format = Some(first);
225                let rule = parsed.next().unwrap();
226                if rule.as_rule() == Rule::space {
227                    parsed.next();
228                }
229                parsed.next();
230            }
231            Rule::currency_string => {
232                let mut rule = parsed.next().unwrap();
233                if rule.as_rule() == Rule::space {
234                    rule = parsed.next().unwrap();
235                }
236                integer_format = Some(rule);
237                display_format.symbol_placement = CurrencySymbolPlacement::BeforeAmount;
238                display_format.negative_amount_display =
239                    NegativeAmountDisplay::BeforeSymbolAndNumber;
240            }
241            other => {
242                panic!("Other: {:?}", other);
243            }
244        }
245
246        // Get thousands separator and type of separation
247        match integer_format {
248            Some(x) => {
249                let mut separators = vec![];
250                let num_str = x.as_str();
251                for sep in x.into_inner() {
252                    separators.push((sep.as_str().chars().next().unwrap(), sep.as_span().start()));
253                }
254                let len = separators.len();
255                display_format.thousands_separator = None;
256                if len == 0 {
257                    display_format.digit_grouping = DigitGrouping::None;
258                } else {
259                    display_format.set_decimal_separator(separators[len - 1].0);
260                    // Get the precision
261                    display_format.max_decimals =
262                        Some(num_str.split(separators[len - 1].0).last().unwrap().len());
263                }
264                if len > 1 {
265                    display_format.set_thousands_separator(separators[len - 2].0);
266                }
267                if len > 2 {
268                    let n = separators[len - 2].1 - separators[len - 3].1;
269                    match n {
270                        2 => display_format.digit_grouping = DigitGrouping::Indian,
271                        3 => display_format.digit_grouping = DigitGrouping::Thousands,
272                        _ => eprintln!("Wrong number format: {}", &format),
273                    }
274                }
275            }
276            None => display_format.digit_grouping = DigitGrouping::None,
277        }
278        display_format
279    }
280}
281
282impl Display for Currency {
283    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
284        write!(f, "{}", self.name)
285    }
286}
287
288impl HasName for Currency {
289    fn get_name(&self) -> &str {
290        self.name.as_str()
291    }
292}
293impl HasAliases for Currency {
294    fn get_aliases(&self) -> &HashSet<String> {
295        &self.aliases
296    }
297}
298impl<'a> From<&'a str> for Currency {
299    fn from(name: &'a str) -> Self {
300        let mut cur = Currency::from_directive(name.to_string());
301        cur.origin = Origin::Other;
302        cur
303    }
304}
305
306impl FromDirective for Currency {
307    fn is_from_directive(&self) -> bool {
308        matches!(self.origin, Origin::FromDirective)
309    }
310}
311
312impl PartialEq for Currency {
313    fn eq(&self, other: &Self) -> bool {
314        self.name == other.name
315    }
316}
317impl Eq for Currency {}
318
319impl Hash for Currency {
320    fn hash<H: Hasher>(&self, state: &mut H) {
321        self.name.hash(state);
322    }
323}
324
325impl Ord for Currency {
326    fn cmp(&self, other: &Self) -> Ordering {
327        self.name.cmp(&other.name)
328    }
329}
330impl PartialOrd for Currency {
331    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
332        self.name.partial_cmp(&other.name)
333    }
334}
335
336const DEFAULT_DISPLAY_FORMAT: CurrencyDisplayFormat = CurrencyDisplayFormat {
337    symbol_placement: CurrencySymbolPlacement::AfterAmount,
338    negative_amount_display: NegativeAmountDisplay::BeforeSymbolAndNumber,
339    decimal_separator: Separator::Dot,
340    digit_grouping: DigitGrouping::Thousands,
341    thousands_separator: Some(Separator::Comma),
342    precision: 0,
343    max_decimals: None,
344};
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    #[test]
350    fn format_1() {
351        let format_str = "-1.234,00 €";
352        let format = CurrencyDisplayFormat::from(format_str);
353
354        assert_eq!(format.get_precision(), 2);
355        assert_eq!(format.get_thousands_separator_str(), Some('.'));
356        assert_eq!(format.get_decimal_separator_str(), ',');
357        assert_eq!(format.get_digit_grouping(), DigitGrouping::Thousands);
358        assert_eq!(
359            format.symbol_placement,
360            CurrencySymbolPlacement::AfterAmount
361        );
362        assert_eq!(
363            format.negative_amount_display,
364            NegativeAmountDisplay::BeforeSymbolAndNumber
365        );
366    }
367    #[test]
368    fn format_2() {
369        let format_str = "($1,234.00)";
370        let format = CurrencyDisplayFormat::from(format_str);
371
372        assert_eq!(format.get_precision(), 2);
373        assert_eq!(format.get_thousands_separator_str(), Some(','));
374        assert_eq!(format.get_decimal_separator_str(), '.');
375        assert_eq!(format.get_digit_grouping(), DigitGrouping::Thousands);
376        assert_eq!(
377            format.symbol_placement,
378            CurrencySymbolPlacement::BeforeAmount
379        );
380        assert_eq!(
381            format.negative_amount_display,
382            NegativeAmountDisplay::BeforeSymbolAndNumber
383        );
384    }
385    #[test]
386    fn format_3() {
387        let format_str = "-1234,00 €";
388        let format = CurrencyDisplayFormat::from(format_str);
389
390        assert_eq!(format.get_precision(), 2);
391        assert_eq!(format.get_thousands_separator_str(), None);
392        assert_eq!(format.get_decimal_separator_str(), ',');
393        // assert_eq!(format.get_digit_grouping(), DigitGrouping::Thousands);
394        assert_eq!(
395            format.symbol_placement,
396            CurrencySymbolPlacement::AfterAmount
397        );
398        assert_eq!(
399            format.negative_amount_display,
400            NegativeAmountDisplay::BeforeSymbolAndNumber
401        );
402    }
403}