1use iso_currency::Currency;
2use rust_decimal::Decimal;
3use rust_decimal::RoundingStrategy;
4use std::str::FromStr;
5use teaql_tool_core::{Result, TeaQLToolError};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Money {
9 pub amount: Decimal,
10 pub currency: Currency,
11}
12
13pub struct MoneyTool;
14
15impl MoneyTool {
16 pub fn new() -> Self {
17 Self
18 }
19
20 pub fn of(&self, amount_str: &str, currency_code: &str) -> Result<Money> {
21 let amount =
22 Decimal::from_str(amount_str).map_err(|e| TeaQLToolError::ParseError(e.to_string()))?;
23 let currency = Currency::from_code(currency_code).ok_or_else(|| {
24 TeaQLToolError::InvalidArgument(format!("Invalid currency code: {}", currency_code))
25 })?;
26 Ok(Money { amount, currency })
27 }
28
29 pub fn zero(&self, currency_code: &str) -> Result<Money> {
30 self.of("0", currency_code)
31 }
32
33 pub fn same_currency(&self, a: &Money, b: &Money) -> bool {
34 a.currency == b.currency
35 }
36
37 fn check_currency(&self, a: &Money, b: &Money) -> Result<()> {
38 if !self.same_currency(a, b) {
39 Err(TeaQLToolError::InvalidArgument(
40 "Currency mismatch".to_string(),
41 ))
42 } else {
43 Ok(())
44 }
45 }
46
47 pub fn add(&self, a: &Money, b: &Money) -> Result<Money> {
48 self.check_currency(a, b)?;
49 Ok(Money {
50 amount: a.amount + b.amount,
51 currency: a.currency.clone(),
52 })
53 }
54
55 pub fn sub(&self, a: &Money, b: &Money) -> Result<Money> {
56 self.check_currency(a, b)?;
57 Ok(Money {
58 amount: a.amount - b.amount,
59 currency: a.currency.clone(),
60 })
61 }
62
63 pub fn mul(&self, a: &Money, multiplier: Decimal) -> Result<Money> {
64 Ok(Money {
65 amount: a.amount * multiplier,
66 currency: a.currency.clone(),
67 })
68 }
69
70 pub fn div(&self, a: &Money, divisor: Decimal) -> Result<Money> {
71 if divisor.is_zero() {
72 Err(TeaQLToolError::InvalidArgument(
73 "Division by zero".to_string(),
74 ))
75 } else {
76 Ok(Money {
77 amount: a.amount / divisor,
78 currency: a.currency.clone(),
79 })
80 }
81 }
82
83 pub fn round(&self, a: &Money) -> Result<Money> {
84 let exp = a.currency.exponent().unwrap_or(2) as u32;
85 Ok(Money {
86 amount: a
87 .amount
88 .round_dp_with_strategy(exp, RoundingStrategy::MidpointNearestEven),
89 currency: a.currency.clone(),
90 })
91 }
92
93 pub fn allocate(&self, a: &Money, ratios: Vec<u32>) -> Result<Vec<Money>> {
94 if ratios.is_empty() {
95 return Err(TeaQLToolError::InvalidArgument(
96 "Ratios cannot be empty".to_string(),
97 ));
98 }
99 let total_ratio: u32 = ratios.iter().sum();
100 if total_ratio == 0 {
101 return Err(TeaQLToolError::InvalidArgument(
102 "Total ratio cannot be zero".to_string(),
103 ));
104 }
105
106 let mut remainder = a.amount;
107 let mut results = Vec::with_capacity(ratios.len());
108
109 let exp = a.currency.exponent().unwrap_or(2) as u32;
110 let total_decimal = Decimal::from(total_ratio);
111
112 for (i, &ratio) in ratios.iter().enumerate() {
113 if i == ratios.len() - 1 {
114 results.push(Money {
115 amount: remainder,
116 currency: a.currency.clone(),
117 });
118 } else {
119 let share = (a.amount * Decimal::from(ratio)) / total_decimal;
120 let rounded =
121 share.round_dp_with_strategy(exp, RoundingStrategy::MidpointNearestEven);
122 results.push(Money {
123 amount: rounded,
124 currency: a.currency.clone(),
125 });
126 remainder -= rounded;
127 }
128 }
129 Ok(results)
130 }
131
132 pub fn format(&self, a: &Money) -> String {
133 format!("{} {}", a.amount, a.currency.code())
134 }
135}
136
137impl Default for MoneyTool {
138 fn default() -> Self {
139 Self::new()
140 }
141}