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
26pub trait BaseMoney: Sized + Debug + Display + Clone + PartialOrd + PartialEq + FromStr {
28 fn currency(&self) -> Currency;
32
33 fn amount(&self) -> Decimal;
35
36 fn round(self) -> Self;
38
39 #[inline]
43 fn name(&self) -> &str {
44 self.currency().name()
45 }
46
47 #[inline]
49 fn symbol(&self) -> &str {
50 self.currency().symbol()
51 }
52
53 #[inline]
55 fn code(&self) -> &str {
56 self.currency().code()
57 }
58
59 #[inline]
61 fn numeric_code(&self) -> i32 {
62 self.currency().numeric_code()
63 }
64
65 #[inline]
67 fn minor_unit(&self) -> u16 {
68 self.currency().minor_unit()
69 }
70
71 #[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 #[inline]
83 fn thousand_separator(&self) -> &'static str {
84 self.currency().thousand_separator()
85 }
86
87 #[inline]
89 fn decimal_separator(&self) -> &'static str {
90 self.currency().decimal_separator()
91 }
92
93 #[inline]
95 fn is_zero(&self) -> bool {
96 self.amount().is_zero()
97 }
98
99 #[inline]
101 fn is_positive(&self) -> bool {
102 self.amount().is_sign_positive()
103 }
104
105 #[inline]
107 fn is_negative(&self) -> bool {
108 self.amount().is_sign_negative()
109 }
110
111 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 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 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 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 fn display(&self) -> String {
186 self.format_code()
187 }
188
189 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 fn abs(&self) -> Self;
212
213 fn min(&self, rhs: Self) -> Self;
214
215 fn max(&self, rhs: Self) -> Self;
216
217 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}