null_kane/
lib.rs

1pub mod calculation;
2pub mod constructor;
3
4use std::{cmp::Ordering, num::ParseIntError};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// A trait for defining currency localization methods.
10pub trait CurrencyLocale {
11    /// Retrieves the separator used for the currency.
12    fn separator(&self) -> char;
13    /// Retrieves the thousand separator used for the currency.
14    fn thousand_separator(&self) -> char;
15    /// Retrieves the currency symbol.
16    fn currency_symbol(&self) -> &'static str;
17}
18
19/// Represents a currency value with specified localization.
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
21#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
22pub struct Currency<L: CurrencyLocale> {
23    negative: bool,
24    amount: usize,
25    locale: L,
26}
27
28impl<L: CurrencyLocale + PartialEq> PartialOrd for Currency<L> {
29    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
30        if self.locale != other.locale {
31            return None;
32        }
33
34        match (
35            self.negative,
36            other.negative,
37            self.amount.cmp(&other.amount),
38        ) {
39            (true, false, Ordering::Less)
40            | (true, false, Ordering::Equal)
41            | (true, false, Ordering::Greater)
42            | (false, false, Ordering::Less)
43            | (true, true, Ordering::Greater) => Some(Ordering::Less),
44
45            (false, false, Ordering::Equal) | (true, true, Ordering::Equal) => {
46                Some(Ordering::Equal)
47            }
48
49            (false, true, Ordering::Less)
50            | (false, true, Ordering::Equal)
51            | (false, false, Ordering::Greater)
52            | (false, true, Ordering::Greater)
53            | (true, true, Ordering::Less) => Some(Ordering::Greater),
54        }
55    }
56}
57
58impl<L> std::fmt::Display for Currency<L>
59where
60    L: CurrencyLocale,
61{
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        let mut buffer = self.full().to_string();
64        if buffer.len() > 3 {
65            let len = buffer.len() - 2;
66            for idx in (1..len).rev().step_by(3) {
67                buffer.insert(idx, self.locale.thousand_separator());
68            }
69        }
70        if self.negative {
71            write!(f, "-")?;
72        }
73        write!(
74            f,
75            "{}{}{:02} {}",
76            buffer,
77            self.locale.separator(),
78            self.part(),
79            self.locale.currency_symbol()
80        )
81    }
82}
83
84impl<L: CurrencyLocale> Currency<L> {
85    /// Constructs a new Currency instance.
86    ///
87    /// # Arguments
88    ///
89    /// * `negative` - Indicates if the currency value is negative.
90    /// * `value` - Indicates the 100th of a curreny, for example 1099 would be 10.99
91    /// * `locale` - The localization information for the currency.
92    ///
93    /// # Returns
94    ///
95    /// A new `Currency` instance.
96    #[must_use]
97    pub fn new(negative: bool, amount: usize, locale: L) -> Self {
98        Self {
99            negative,
100            amount,
101            locale,
102        }
103    }
104
105    /// Updates the localization information of the currency.
106    ///
107    /// # Arguments
108    ///
109    /// * `locale` - The updated localization information.
110    ///
111    /// # Returns
112    ///
113    /// The updated `Currency` instance with the new localization.
114    #[must_use]
115    pub fn with_locale(mut self, locale: L) -> Self {
116        self.locale = locale;
117        self
118    }
119
120    /// returns the full value of the currency
121    #[inline]
122    pub fn full(&self) -> usize {
123        self.amount / 100
124    }
125
126    /// returns the fraction value of the currency
127    #[inline]
128    pub fn part(&self) -> usize {
129        self.amount % 100
130    }
131
132    /// returns the current value as integer
133    #[inline]
134    pub fn amount(&self) -> isize {
135        let amount = self.amount as isize;
136        if self.negative {
137            -amount
138        } else {
139            amount
140        }
141    }
142
143    /// Parses a currency given a local.
144    ///
145    /// # Panics
146    ///
147    /// This panics if the seperators or the currency symbold don't match the given local or if the
148    /// number could not parsed to a isizes
149    pub fn parse(value: impl AsRef<str>, locale: L) -> Result<Self, CurrencyParseError>
150    where
151        L: Default,
152    {
153        let val = value.as_ref();
154        if !val.contains(locale.currency_symbol()) {
155            Err(CurrencyParseError::LocaleNotMatching)
156        } else {
157            let value = val
158                .chars()
159                .filter(|&c| {
160                    !(locale.currency_symbol().contains(c)
161                        || c == locale.separator()
162                        || c == locale.thousand_separator()
163                        || c.is_whitespace())
164                })
165                .collect::<String>()
166                .parse::<isize>()
167                .map_err(|e| CurrencyParseError::ParseValue(e))?;
168
169            Ok(Currency::from(value).with_locale(locale))
170        }
171    }
172}
173
174#[derive(Clone, Debug)]
175pub enum CurrencyParseError {
176    LocaleNotMatching,
177    ParseValue(ParseIntError),
178}
179
180#[cfg(test)]
181mod test {
182    use super::*;
183
184    #[derive(Clone, Copy, Default, Debug, PartialEq)]
185    enum CurrencyL {
186        #[default]
187        Eu,
188        Us,
189    }
190
191    impl CurrencyLocale for CurrencyL {
192        fn separator(&self) -> char {
193            match self {
194                CurrencyL::Eu => ',',
195                CurrencyL::Us => '.',
196            }
197        }
198
199        fn thousand_separator(&self) -> char {
200            match self {
201                CurrencyL::Eu => '.',
202                CurrencyL::Us => ',',
203            }
204        }
205
206        fn currency_symbol(&self) -> &'static str {
207            match self {
208                CurrencyL::Eu => "€",
209                CurrencyL::Us => "$",
210            }
211        }
212    }
213
214    #[test]
215    fn parse_currency() {
216        let curr = Currency::parse("22.000,44 €", CurrencyL::Eu).unwrap();
217
218        assert_eq!(
219            curr,
220            Currency {
221                negative: false,
222                amount: 2200044,
223                locale: CurrencyL::Eu
224            }
225        )
226    }
227
228    #[test]
229    fn parse_currency_non_utf8_whitespace() {
230        let curr = Currency::parse("22.000,44\u{a0}€", CurrencyL::Eu).unwrap();
231
232        assert_eq!(
233            curr,
234            Currency {
235                negative: false,
236                amount: 2200044,
237                locale: CurrencyL::Eu
238            }
239        )
240    }
241
242    #[test]
243    fn parse_currency_prefix_notation() {
244        let curr = Currency::parse("€22,44", CurrencyL::Eu).unwrap();
245
246        assert_eq!(
247            curr,
248            Currency {
249                negative: false,
250                amount: 2244,
251                locale: CurrencyL::Eu
252            }
253        )
254    }
255
256    #[test]
257    fn parse_currency_prefix_notation_other() {
258        let curr = Currency::parse("$22,44", CurrencyL::Us).unwrap();
259
260        assert_eq!(
261            curr,
262            Currency {
263                negative: false,
264                amount: 2244,
265                locale: CurrencyL::Us
266            }
267        );
268    }
269
270    #[test]
271    #[should_panic]
272    fn parse_currency_wrong_currency_symbol() {
273        Currency::parse("$22,44", CurrencyL::Eu).unwrap();
274    }
275
276    #[test]
277    fn print_currency() {
278        let mut curr = Currency::new(false, 0, CurrencyL::Eu);
279        for (full, full_string) in [
280            (2_00, "2"),
281            (20_00, "20"),
282            (200_00, "200"),
283            (2000_00, "2.000"),
284            (20_000_00, "20.000"),
285            (200_000_00, "200.000"),
286            (2_000_000_00, "2.000.000"),
287            (20_000_000_00, "20.000.000"),
288            (200_000_000_00, "200.000.000"),
289        ] {
290            curr.amount = full;
291            assert_eq!(format!("{full_string},00 €"), curr.to_string());
292        }
293
294        let curr = Currency::new(false, 202, CurrencyL::Eu);
295        assert_eq!("2,02 €", &curr.to_string());
296    }
297
298    #[test]
299    fn construct_f32() {
300        let first_val = 100.8_f32;
301        let second_val = 191.0_f32;
302
303        let expected = Currency::<CurrencyL>::from(first_val + second_val);
304        assert_eq!(expected, Currency::new(false, 291_80, CurrencyL::Eu));
305    }
306
307    #[test]
308    fn compare_both_negative_equal() {
309        let curr1 = Currency::new(true, 2_22, CurrencyL::Eu);
310        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
311        assert!(curr1 == curr2);
312    }
313
314    #[test]
315    fn compare_both_negative_equal_full_diff_part() {
316        let curr1 = Currency::new(true, 2_21, CurrencyL::Eu);
317        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
318        assert!(curr1 > curr2);
319    }
320
321    #[test]
322    fn compare_both_negative_diff_full_equal_part() {
323        let curr1 = Currency::new(true, 1_22, CurrencyL::Eu);
324        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
325        assert!(curr1 > curr2);
326    }
327
328    #[test]
329    fn compare_both_negative_diff_full_greater_part() {
330        let curr1 = Currency::new(true, 1_89, CurrencyL::Eu);
331        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
332        assert!(curr1 > curr2);
333    }
334
335    #[test]
336    fn compare_diff_negative_equal() {
337        let curr1 = Currency::new(false, 2_22, CurrencyL::Eu);
338        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
339        assert!(curr1 > curr2);
340    }
341
342    #[test]
343    fn compare_diff_negative_equal_full_diff_part() {
344        let curr1 = Currency::new(false, 2_24, CurrencyL::Eu);
345        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
346        assert!(curr1 > curr2);
347    }
348
349    #[test]
350    fn compare_diff_negative_greater_values() {
351        let curr1 = Currency::new(false, 1_11, CurrencyL::Eu);
352        let curr2 = Currency::new(true, 2_22, CurrencyL::Eu);
353        assert!(curr1 > curr2);
354    }
355
356    #[test]
357    fn compare_equal_full_less_part() {
358        let curr1 = Currency::new(false, 2_21, CurrencyL::Eu);
359        let curr2 = Currency::new(false, 2_22, CurrencyL::Eu);
360        assert!(curr1 < curr2);
361    }
362
363    #[test]
364    fn compare_equal_full_greater_part() {
365        let curr1 = Currency::new(false, 2_23, CurrencyL::Eu);
366        let curr2 = Currency::new(false, 2_22, CurrencyL::Eu);
367        assert!(curr1 > curr2);
368    }
369
370    #[test]
371    fn compare_diff_full_equal_part() {
372        let curr1 = Currency::new(false, 3_22, CurrencyL::Eu);
373        let curr2 = Currency::new(false, 2_22, CurrencyL::Eu);
374        assert!(curr1 > curr2);
375    }
376
377    #[test]
378    fn compare_diff_full_greater_part() {
379        let curr1 = Currency::new(false, 3_22, CurrencyL::Eu);
380        let curr2 = Currency::new(false, 2_89, CurrencyL::Eu);
381        assert!(curr1 > curr2);
382    }
383}