Skip to main content

teaql_tool_std/
money.rs

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}