Skip to main content

moneylib/
base.rs

1use crate::money_macros::dec;
2use crate::{Country, Currency, MoneyError};
3use crate::{Decimal, MoneyResult};
4use accounting::Accounting;
5use regex::Regex;
6use rust_decimal::RoundingStrategy as DecimalRoundingStrategy;
7use rust_decimal::{MathematicalOps, prelude::ToPrimitive};
8use std::fmt::Display;
9use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
10use std::{fmt::Debug, str::FromStr, sync::LazyLock};
11
12pub(crate) const COMMA_SEPARATOR: &str = ",";
13
14pub(crate) const DOT_SEPARATOR: &str = ".";
15
16pub static COMMA_THOUSANDS_SEPARATOR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
17    regex::Regex::new(r"^([A-Za-z]{3})\s+((?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?)$")
18        .expect("failed compiling money format regex: comma thousands separator")
19});
20
21pub static DOT_THOUSANDS_SEPARATOR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
22    regex::Regex::new(r"^([A-Za-z]{3})\s+((?:\d{1,3}(?:.\d{3})*|\d+)(?:\,\d+)?)$")
23        .expect("failed compiling money format regex: dot thousands separator")
24});
25
26/// BaseMoney is the base trait for dealing with money type.
27pub trait BaseMoney: Sized + Debug + Display + Clone + PartialOrd + PartialEq + FromStr {
28    // REQUIRED
29
30    /// Get currency of money
31    fn currency(&self) -> Currency;
32
33    /// Get amount of money
34    fn amount(&self) -> Decimal;
35
36    /// Round money using `Currency`'s rounding strategy to the scale of currency's minor unit
37    fn round(self) -> Self;
38
39    // PROVIDED
40
41    /// Get currency name
42    #[inline]
43    fn name(&self) -> &str {
44        self.currency().name()
45    }
46
47    /// Get money symbol
48    #[inline]
49    fn symbol(&self) -> &str {
50        self.currency().symbol()
51    }
52
53    /// Get money ISO 4217 code
54    #[inline]
55    fn code(&self) -> &str {
56        self.currency().code()
57    }
58
59    /// Get currency ISO 4217 numeric code
60    #[inline]
61    fn numeric_code(&self) -> i32 {
62        self.currency().numeric_code()
63    }
64
65    /// Get money minor unit
66    #[inline]
67    fn minor_unit(&self) -> u16 {
68        self.currency().minor_unit()
69    }
70
71    /// Get money amount in its smallest unit
72    #[inline]
73    fn minor_amount(&self) -> MoneyResult<i128> {
74        self.amount()
75            .checked_mul(dec!(10).powu(self.minor_unit() as u64))
76            .ok_or(MoneyError::ArithmeticOverflow)?
77            .to_i128()
78            .ok_or(MoneyError::DecimalToInteger)
79    }
80
81    /// Get money thousands separator
82    #[inline]
83    fn thousand_separator(&self) -> &'static str {
84        self.currency().thousand_separator()
85    }
86
87    /// Get money decimal separator
88    #[inline]
89    fn decimal_separator(&self) -> &'static str {
90        self.currency().decimal_separator()
91    }
92
93    /// Check if amount is 0
94    #[inline]
95    fn is_zero(&self) -> bool {
96        self.amount().is_zero()
97    }
98
99    /// Check if sign is +
100    #[inline]
101    fn is_positive(&self) -> bool {
102        self.amount().is_sign_positive()
103    }
104
105    /// Check if sign is -
106    #[inline]
107    fn is_negative(&self) -> bool {
108        self.amount().is_sign_negative()
109    }
110
111    /// Format money with code along with thousands and decimal separators.
112    /// Example: USD 1,234.45
113    fn format_code(&self) -> String {
114        let mut fmt = Accounting::new_from_seperator(
115            self.code(),
116            self.minor_unit() as usize,
117            self.thousand_separator(),
118            self.decimal_separator(),
119        );
120        if self.amount().is_sign_negative() {
121            let abs = self.amount().abs();
122            fmt.set_format("{s} -{v}");
123            fmt.format_money(abs)
124        } else {
125            fmt.set_format("{s} {v}");
126            fmt.format_money(self.amount())
127        }
128    }
129
130    /// Format money with symbol along with thousands and decimal separators.
131    /// Example: $1,234.45
132    fn format_symbol(&self) -> String {
133        let mut fmt = Accounting::new_from_seperator(
134            self.symbol(),
135            self.minor_unit() as usize,
136            self.thousand_separator(),
137            self.decimal_separator(),
138        );
139        fmt.set_format("{s}{v}");
140        fmt.format_money(self.amount())
141    }
142
143    /// Format money with code in the smallest unit along with thousands separators.
144    /// Example USD 1,234.45 --> USD 123,445 ¢
145    /// If the currency has no minor unit symbol, it defaults to "minor".
146    /// You can set the minor unit symbol in `Currency` type's setter.
147    fn format_code_minor(&self) -> MoneyResult<String> {
148        let minor_amount = self.minor_amount()?;
149        let mut fmt = Accounting::new_from_seperator(
150            self.code(),
151            0,
152            self.thousand_separator(),
153            self.decimal_separator(),
154        );
155        if minor_amount.is_negative() {
156            let abs = minor_amount.abs();
157            let f = format!("{{s}} -{{v}} {}", self.currency().minor_symbol());
158            fmt.set_format(&f);
159            Ok(fmt.format_money(abs))
160        } else {
161            let f = format!("{{s}} {{v}} {}", self.currency().minor_symbol());
162            fmt.set_format(&f);
163            Ok(fmt.format_money(minor_amount))
164        }
165    }
166
167    /// Format money with code in the smallest unit along with thousands separators.
168    /// Example $1,234.45 --> $123,445 ¢
169    /// If the currency has no minor unit symbol, it defaults to "minor".
170    /// You can set the minor unit symbol in `Currency` type's setter.
171    fn format_symbol_minor(&self) -> MoneyResult<String> {
172        let minor_amount = self.minor_amount()?;
173        let mut fmt = Accounting::new_from_seperator(
174            self.symbol(),
175            0,
176            self.thousand_separator(),
177            self.decimal_separator(),
178        );
179        let f = format!("{{s}}{{v}} {}", self.currency().minor_symbol());
180        fmt.set_format(&f);
181        Ok(fmt.format_money(minor_amount))
182    }
183
184    /// Default display of money
185    fn display(&self) -> String {
186        self.format_code()
187    }
188
189    /// Get countries using this currency
190    fn countries(&self) -> Option<Vec<Country>> {
191        self.currency().countries()
192    }
193}
194
195pub trait BaseOps:
196    Sized
197    + BaseMoney
198    + Add<Output = Self>
199    + Sub<Output = Self>
200    + Mul<Output = Self>
201    + Div<Output = Self>
202    + AddAssign
203    + SubAssign
204    + MulAssign
205    + DivAssign
206    + Neg<Output = Self>
207{
208    // REQUIRED
209
210    /// make money positive
211    fn abs(&self) -> Self;
212
213    fn min(&self, rhs: Self) -> Self;
214
215    fn max(&self, rhs: Self) -> Self;
216
217    /// clamp the money amount between `from` and `to` inclusively.
218    fn clamp(&self, from: Decimal, to: Decimal) -> Self;
219
220    fn add(&self, rhs: Decimal) -> MoneyResult<Self>;
221
222    fn sub(&self, rhs: Decimal) -> MoneyResult<Self>;
223
224    fn mul(&self, rhs: Decimal) -> MoneyResult<Self>;
225
226    fn div(&self, rhs: Decimal) -> MoneyResult<Self>;
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
230pub enum RoundingStrategy {
231    #[default]
232    BankersRounding,
233
234    HalfUp,
235
236    HalfDown,
237
238    Ceil,
239
240    Floor,
241}
242
243impl From<RoundingStrategy> for DecimalRoundingStrategy {
244    fn from(value: RoundingStrategy) -> Self {
245        match value {
246            RoundingStrategy::BankersRounding => DecimalRoundingStrategy::MidpointNearestEven,
247            RoundingStrategy::HalfUp => DecimalRoundingStrategy::MidpointAwayFromZero,
248            RoundingStrategy::HalfDown => DecimalRoundingStrategy::MidpointTowardZero,
249            RoundingStrategy::Ceil => DecimalRoundingStrategy::AwayFromZero,
250            RoundingStrategy::Floor => DecimalRoundingStrategy::ToZero,
251        }
252    }
253}
254
255pub trait CustomMoney: Sized + BaseMoney {
256    fn set_thousand_separator(&mut self, separator: &'static str);
257
258    fn set_decimal_separator(&mut self, separator: &'static str);
259
260    fn round_with(self, decimal_points: u32, strategy: RoundingStrategy) -> Self;
261}