proto_types/common/
money.rs

1//! Implementations for the google.type.Money message.
2//! DISCLAIMER: all of the methods implemented for Money are just implemented for convenience, and they are provided as is, without warranties of any kind. By using this module, the user is relieving the authors of this library from any responsibility for any damage that may be caused by its usage.
3
4use std::cmp::Ordering;
5
6use thiserror::Error;
7
8use crate::common::Money;
9
10const NANO_FACTOR: i32 = 1_000_000_000;
11
12/// Errors that can occur during the creation, conversion or validation of [`Money`].
13#[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; // Indicate incomparability
54    }
55
56    // To compare accurately, convert the amount to a single i128 value in nanos.
57    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  /// Normalizes the [`Money`] amount and returns a string containing the currency symbol and the monetary amount rounded by the specified decimal places.
66  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      // Handle carry-over from nanos rounding to units
103      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      // Format rounded_nanos to the specified number of decimal places, zero-padded
124      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  /// Normalizes units and nanos. Fails in case of overflow.
135  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  /// Creates a new instance, if the normalization does not return errors like Overflow or Underflow.
145  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  /// Converts the [`Money`] amount into a decimal (f64) representation,
155  /// rounded to the specified number of decimal places.
156  ///
157  /// `decimal_places` determines the precision of the rounding. For example:
158  /// - `0` rounds to the nearest whole unit.
159  /// - `2` rounds to two decimal places (e.g., for cents).
160  ///
161  /// WARNING: The usage of `f64` introduces floating-point precision issues. Do not use it for critical financial calculations.
162  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  /// Converts the `Money` amount into a decimal (f64) representation.
186  ///
187  /// WARNING: The usage of `f64` introduces floating-point precision issues. Do not use it for critical financial calculations.
188  pub fn as_imprecise_f64(&self) -> f64 {
189    self.units as f64 + (self.nanos as f64 / 1_000_000_000.0)
190  }
191
192  /// Creates a new `Money` instance with the given currency code and decimal amount.
193  ///
194  /// This is a convenience constructor that handles splitting a decimal value
195  /// into units and nanos.
196  ///
197  /// WARNING: The usage of `f64` introduces floating-point precision issues. Do not use it for critical financial calculations.
198  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; // Now this cast is safe because we checked the range
212
213    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      // For -0.5, ensure nanos is -500M
220      -nanos
221    } else {
222      nanos
223    };
224
225    Money::new(currency_code, units, final_nanos)
226  }
227
228  /// Attempts to add another [`Money`] amount to this one, returning a new [`Money`] instance.
229  /// Returns an error if currencies mismatch or if addition causes an overflow/underflow.
230  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  /// Attempts to add another [`Money`] amount to this one in place.
251  /// Returns an error if currencies mismatch or if addition causes an overflow/underflow.
252  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  /// Attempts to subtract another [`Money`] amount from this one, returning a new [`Money`] instance.
276  /// Returns an error if currencies mismatch or if subtraction causes an overflow/underflow.
277  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  /// Attempts to subtract another [`Money`] amount from this one in place.
298  /// Returns an error if currencies mismatch or if subtraction causes an overflow/underflow.
299  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  /// Attempts to multiply this [`Money`] amount by an integer scalar, returning a new [`Money`] instance.
323  /// Returns an error if multiplication causes an overflow/underflow.
324  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  /// Attempts to multiply this [`Money`] amount by a float scalar, returning a new [`Money`] instance.
336  /// Returns an error if the result is non-finite or causes an internal conversion error.
337  /// WARNING: The usage of `f64` introduces floating-point precision issues. Do not use it for critical financial calculations.
338  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    // Pass the result to from_decimal_f64, which will normalize and validate.
351    Money::from_imprecise_f64(self.currency_code.clone(), result_decimal)
352  }
353
354  /// Attempts to divide this [`Money`] amount by an integer scalar, returning a new [`Money`] instance.
355  /// Returns an error if the divisor is zero, or if division causes an overflow/underflow.
356  pub fn try_div_i64(&self, rhs: i64) -> Result<Self, MoneyError> {
357    if rhs == 0 {
358      return Err(MoneyError::InvalidAmount); // Division by zero
359    }
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    // Safely convert the `new_units` from i128 to i64
368    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    // This cast is safe because (result % NANO_FACTOR) is always < NANO_FACTOR,
374    // and NANO_FACTOR itself fits in i32.
375    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  /// Attempts to divide this [`Money`] amount by a float scalar, returning a new [`Money`] instance.
381  /// Returns an error if the divisor is zero, non-finite, or if division causes an internal conversion error.
382  /// WARNING: The usage of `f64` introduces floating-point precision issues. Do not use it for critical financial calculations.
383  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  /// Attempts to negate this [`Money`] amount, returning a new [`Money`] instance.
402  /// Returns an error if negation causes an overflow/underflow.
403  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  /// Checks if the money's currency code matches the given `code`.
411  /// The `code` should be a three-letter ISO 4217 currency code (e.g., "USD", "EUR").
412  pub fn is_currency(&self, code: &str) -> bool {
413    self.currency_code == code
414  }
415
416  /// Checks if the money's currency is United States Dollar (USD).
417  pub fn is_usd(&self) -> bool {
418    self.is_currency("USD")
419  }
420
421  /// Checks if the money's currency is Euro (EUR).
422  pub fn is_eur(&self) -> bool {
423    self.is_currency("EUR")
424  }
425
426  /// Checks if the money's currency is British Pound Sterling (GBP).
427  pub fn is_gbp(&self) -> bool {
428    self.is_currency("GBP")
429  }
430
431  /// Checks if the money's currency is Japanese Yen (JPY).
432  pub fn is_jpy(&self) -> bool {
433    self.is_currency("JPY")
434  }
435
436  /// Checks if the money's currency is Canadian Dollar (CAD).
437  pub fn is_cad(&self) -> bool {
438    self.is_currency("CAD")
439  }
440
441  /// Checks if the money's currency is Australian Dollar (AUD).
442  pub fn is_aud(&self) -> bool {
443    self.is_currency("AUD")
444  }
445
446  /// Checks if the money amount is strictly positive (greater than zero).
447  pub fn is_positive(&self) -> bool {
448    self.units > 0 || (self.units == 0 && self.nanos > 0)
449  }
450
451  /// Checks if the money amount is strictly negative (less than zero).
452  pub fn is_negative(&self) -> bool {
453    self.units < 0 || (self.units == 0 && self.nanos < 0)
454  }
455
456  /// Checks if the money amount is exactly zero.
457  pub fn is_zero(&self) -> bool {
458    self.units == 0 && self.nanos == 0
459  }
460}