1pub mod calculation;
2pub mod constructor;
3
4use std::{cmp::Ordering, num::ParseIntError};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9pub trait CurrencyLocale {
11 fn separator(&self) -> char;
13 fn thousand_separator(&self) -> char;
15 fn currency_symbol(&self) -> &'static str;
17}
18
19#[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 #[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 #[must_use]
115 pub fn with_locale(mut self, locale: L) -> Self {
116 self.locale = locale;
117 self
118 }
119
120 #[inline]
122 pub fn full(&self) -> usize {
123 self.amount / 100
124 }
125
126 #[inline]
128 pub fn part(&self) -> usize {
129 self.amount % 100
130 }
131
132 #[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 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}