proto_types/common/
money.rs1use std::cmp::Ordering;
5
6use thiserror::Error;
7
8use crate::common::Money;
9
10const NANO_FACTOR: i32 = 1_000_000_000;
11
12#[derive(Debug, Error, PartialEq, Eq, Clone)]
14pub enum MoneyError {
15 #[error("Currency mismatch: Expected '{expected}', found '{found}'")]
16 CurrencyMismatch { expected: String, found: String },
17 #[error("Money arithmetic overflow")]
18 Overflow,
19 #[error("Money arithmetic underflow")]
20 Underflow,
21 #[error("Money amount resulted in an invalid state")]
22 InvalidAmount,
23 #[error("Floating point operation resulted in a non-finite number (NaN or Infinity)")]
24 NonFiniteResult,
25}
26
27fn normalize_money_fields_checked(
28 mut units: i64,
29 mut nanos: i32,
30) -> Result<(i64, i32), MoneyError> {
31 if nanos.abs() >= NANO_FACTOR {
32 let units_carry = (nanos / (NANO_FACTOR)) as i64;
33 units = units.checked_add(units_carry).ok_or(MoneyError::Overflow)?;
34 nanos %= NANO_FACTOR;
35 }
36
37 if units > 0 && nanos < 0 {
38 units = units.checked_sub(1).ok_or(MoneyError::Underflow)?;
39 nanos = nanos.checked_add(NANO_FACTOR).ok_or(MoneyError::Overflow)?;
40 } else if units < 0 && nanos > 0 {
41 units = units.checked_add(1).ok_or(MoneyError::Overflow)?;
42 nanos = nanos
43 .checked_sub(NANO_FACTOR)
44 .ok_or(MoneyError::Underflow)?;
45 }
46
47 Ok((units, nanos))
48}
49
50impl PartialOrd for Money {
51 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52 if self.currency_code != other.currency_code {
53 return None; }
55
56 let self_total_nanos = self.units as i128 * 1_000_000_000i128 + self.nanos as i128;
58 let other_total_nanos = other.units as i128 * 1_000_000_000i128 + other.nanos as i128;
59
60 self_total_nanos.partial_cmp(&other_total_nanos)
61 }
62}
63
64impl Money {
65 pub fn to_formatted_string(&self, symbol: &str, decimal_places: u32) -> String {
67 let decimal_places = u32::min(9, decimal_places);
68
69 let mut current_units: i128 = self.units as i128;
70 let mut current_nanos: i128 = self.nanos as i128;
71
72 let ten_pow_9 = NANO_FACTOR as i128;
73 if current_nanos >= ten_pow_9 || current_nanos <= -ten_pow_9 {
74 current_units += current_nanos / ten_pow_9;
75 current_nanos %= ten_pow_9;
76 }
77
78 if current_units > 0 && current_nanos < 0 {
79 current_units -= 1;
80 current_nanos += ten_pow_9;
81 } else if current_units < 0 && current_nanos > 0 {
82 current_units += 1;
83 current_nanos -= ten_pow_9;
84 }
85
86 let mut rounded_nanos = 0;
87 let mut units_carry = 0;
88
89 if decimal_places > 0 {
90 let power_of_10_for_display = 10_i128.pow(decimal_places);
91 let rounding_power = 10_i128.pow(9 - decimal_places);
92
93 let abs_nanos = current_nanos.abs();
94
95 let remainder_for_rounding = abs_nanos % rounding_power;
96 rounded_nanos = abs_nanos / rounding_power;
97
98 if remainder_for_rounding >= rounding_power / 2 {
99 rounded_nanos += 1;
100 }
101
102 if rounded_nanos >= power_of_10_for_display {
104 units_carry = 1;
105 rounded_nanos = 0;
106 }
107 }
108
109 let is_negative = current_units < 0 || (current_units == 0 && current_nanos < 0);
110
111 let final_units_abs = current_units.abs() + units_carry;
112
113 let mut formatted_string = String::new();
114
115 if is_negative {
116 formatted_string.push('-');
117 }
118 formatted_string.push_str(symbol);
119 formatted_string.push_str(&final_units_abs.to_string());
120
121 if decimal_places > 0 {
122 formatted_string.push('.');
123 formatted_string.push_str(&format!(
125 "{:0width$}",
126 rounded_nanos,
127 width = decimal_places as usize
128 ));
129 }
130
131 formatted_string
132 }
133
134 pub fn normalize(mut self) -> Result<Self, MoneyError> {
136 let (normalized_units, normalized_nanos) =
137 normalize_money_fields_checked(self.units, self.nanos)?;
138 self.units = normalized_units;
139 self.nanos = normalized_nanos;
140
141 Ok(self)
142 }
143
144 pub fn new(currency_code: String, units: i64, nanos: i32) -> Result<Self, MoneyError> {
146 let (normalized_units, normalized_nanos) = normalize_money_fields_checked(units, nanos)?;
147 Ok(Money {
148 currency_code,
149 units: normalized_units,
150 nanos: normalized_nanos,
151 })
152 }
153
154 pub fn to_rounded_imprecise_f64(&self, decimal_places: u32) -> Result<f64, MoneyError> {
163 if decimal_places > i32::MAX as u32 {
164 return Err(MoneyError::Overflow);
165 }
166
167 let full_amount = self.as_imprecise_f64();
168
169 let factor_exponent = decimal_places as i32;
170 let factor = 10.0f64.powi(factor_exponent);
171
172 if !factor.is_finite() {
173 return Err(MoneyError::NonFiniteResult);
174 }
175
176 let result = (full_amount * factor).round() / factor;
177
178 if !result.is_finite() {
179 return Err(MoneyError::NonFiniteResult);
180 }
181
182 Ok(result)
183 }
184
185 pub fn as_imprecise_f64(&self) -> f64 {
189 self.units as f64 + (self.nanos as f64 / 1_000_000_000.0)
190 }
191
192 pub fn from_imprecise_f64(currency_code: String, amount: f64) -> Result<Self, MoneyError> {
199 if !amount.is_finite() {
200 return Err(MoneyError::NonFiniteResult);
201 }
202
203 let truncated_amount = amount.trunc();
204
205 if truncated_amount > i64::MAX as f64 {
206 return Err(MoneyError::Overflow);
207 } else if truncated_amount < i64::MIN as f64 {
208 return Err(MoneyError::Underflow);
209 }
210
211 let units = truncated_amount as i64; let raw_nanos_f64 = amount.fract().abs() * NANO_FACTOR as f64;
214 let nanos: i32 = raw_nanos_f64.round() as i32;
215
216 let final_nanos = if units < 0 && nanos > 0 {
217 -nanos
218 } else if units == 0 && amount < 0.0 && nanos > 0 {
219 -nanos
221 } else {
222 nanos
223 };
224
225 Money::new(currency_code, units, final_nanos)
226 }
227
228 pub fn try_add(&self, other: &Self) -> Result<Self, MoneyError> {
231 if self.currency_code != other.currency_code {
232 return Err(MoneyError::CurrencyMismatch {
233 expected: self.currency_code.clone(),
234 found: other.currency_code.clone(),
235 });
236 }
237
238 let sum_units = self
239 .units
240 .checked_add(other.units)
241 .ok_or(MoneyError::Overflow)?;
242 let sum_nanos = self
243 .nanos
244 .checked_add(other.nanos)
245 .ok_or(MoneyError::Overflow)?;
246
247 Money::new(self.currency_code.clone(), sum_units, sum_nanos)
248 }
249
250 pub fn try_add_assign(&mut self, other: &Self) -> Result<(), MoneyError> {
253 if self.currency_code != other.currency_code {
254 return Err(MoneyError::CurrencyMismatch {
255 expected: self.currency_code.clone(),
256 found: other.currency_code.clone(),
257 });
258 }
259
260 self.units = self
261 .units
262 .checked_add(other.units)
263 .ok_or(MoneyError::Overflow)?;
264 self.nanos = self
265 .nanos
266 .checked_add(other.nanos)
267 .ok_or(MoneyError::Overflow)?;
268
269 let (final_units, final_nanos) = normalize_money_fields_checked(self.units, self.nanos)?;
270 self.units = final_units;
271 self.nanos = final_nanos;
272 Ok(())
273 }
274
275 pub fn try_sub(&self, other: &Self) -> Result<Self, MoneyError> {
278 if self.currency_code != other.currency_code {
279 return Err(MoneyError::CurrencyMismatch {
280 expected: self.currency_code.clone(),
281 found: other.currency_code.clone(),
282 });
283 }
284
285 let diff_units = self
286 .units
287 .checked_sub(other.units)
288 .ok_or(MoneyError::Underflow)?;
289 let diff_nanos = self
290 .nanos
291 .checked_sub(other.nanos)
292 .ok_or(MoneyError::Underflow)?;
293
294 Money::new(self.currency_code.clone(), diff_units, diff_nanos)
295 }
296
297 pub fn try_sub_assign(&mut self, other: &Self) -> Result<(), MoneyError> {
300 if self.currency_code != other.currency_code {
301 return Err(MoneyError::CurrencyMismatch {
302 expected: self.currency_code.clone(),
303 found: other.currency_code.clone(),
304 });
305 }
306
307 self.units = self
308 .units
309 .checked_sub(other.units)
310 .ok_or(MoneyError::Underflow)?;
311 self.nanos = self
312 .nanos
313 .checked_sub(other.nanos)
314 .ok_or(MoneyError::Underflow)?;
315
316 let (final_units, final_nanos) = normalize_money_fields_checked(self.units, self.nanos)?;
317 self.units = final_units;
318 self.nanos = final_nanos;
319 Ok(())
320 }
321
322 pub fn try_mul_i64(&self, rhs: i64) -> Result<Self, MoneyError> {
325 let mul_units = self.units.checked_mul(rhs).ok_or(MoneyError::Overflow)?;
326 let mul_nanos_i64 = (self.nanos as i64)
327 .checked_mul(rhs)
328 .ok_or(MoneyError::Overflow)?;
329
330 let final_nanos_for_new: i32 = mul_nanos_i64.try_into().map_err(|_| MoneyError::Overflow)?;
331
332 Money::new(self.currency_code.clone(), mul_units, final_nanos_for_new)
333 }
334
335 pub fn try_mul_f64(&self, rhs: f64) -> Result<Self, MoneyError> {
339 if !rhs.is_finite() {
340 return Err(MoneyError::NonFiniteResult);
341 }
342
343 let decimal_amount = self.as_imprecise_f64();
344 let result_decimal = decimal_amount * rhs;
345
346 if !result_decimal.is_finite() {
347 return Err(MoneyError::NonFiniteResult);
348 }
349
350 Money::from_imprecise_f64(self.currency_code.clone(), result_decimal)
352 }
353
354 pub fn try_div_i64(&self, rhs: i64) -> Result<Self, MoneyError> {
357 if rhs == 0 {
358 return Err(MoneyError::InvalidAmount); }
360
361 let total_nanos_i128 = self.units as i128 * NANO_FACTOR as i128 + self.nanos as i128;
362
363 let result_total_nanos = total_nanos_i128
364 .checked_div(rhs as i128)
365 .ok_or(MoneyError::Overflow)?;
366
367 let new_units_i128 = result_total_nanos / NANO_FACTOR as i128;
369 let new_units = new_units_i128
370 .try_into()
371 .map_err(|_| MoneyError::Overflow)?;
372
373 let new_nanos = (result_total_nanos % NANO_FACTOR as i128) as i32;
376
377 Money::new(self.currency_code.clone(), new_units, new_nanos)
378 }
379
380 pub fn try_div_f64(&self, rhs: f64) -> Result<Self, MoneyError> {
384 if rhs == 0.0 {
385 return Err(MoneyError::InvalidAmount);
386 }
387 if !rhs.is_finite() {
388 return Err(MoneyError::NonFiniteResult);
389 }
390
391 let decimal_amount = self.as_imprecise_f64();
392 let result_decimal = decimal_amount / rhs;
393
394 if !result_decimal.is_finite() {
395 return Err(MoneyError::NonFiniteResult);
396 }
397
398 Money::from_imprecise_f64(self.currency_code.clone(), result_decimal)
399 }
400
401 pub fn try_neg(&self) -> Result<Self, MoneyError> {
404 let neg_units = self.units.checked_neg().ok_or(MoneyError::Overflow)?;
405 let neg_nanos = self.nanos.checked_neg().ok_or(MoneyError::Overflow)?;
406
407 Money::new(self.currency_code.clone(), neg_units, neg_nanos)
408 }
409
410 pub fn is_currency(&self, code: &str) -> bool {
413 self.currency_code == code
414 }
415
416 pub fn is_usd(&self) -> bool {
418 self.is_currency("USD")
419 }
420
421 pub fn is_eur(&self) -> bool {
423 self.is_currency("EUR")
424 }
425
426 pub fn is_gbp(&self) -> bool {
428 self.is_currency("GBP")
429 }
430
431 pub fn is_jpy(&self) -> bool {
433 self.is_currency("JPY")
434 }
435
436 pub fn is_cad(&self) -> bool {
438 self.is_currency("CAD")
439 }
440
441 pub fn is_aud(&self) -> bool {
443 self.is_currency("AUD")
444 }
445
446 pub fn is_positive(&self) -> bool {
448 self.units > 0 || (self.units == 0 && self.nanos > 0)
449 }
450
451 pub fn is_negative(&self) -> bool {
453 self.units < 0 || (self.units == 0 && self.nanos < 0)
454 }
455
456 pub fn is_zero(&self) -> bool {
458 self.units == 0 && self.nanos == 0
459 }
460}