Skip to main content

robinpath_modules/modules/
money_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    rp.register_builtin("money.format", |args, _| {
5        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
6        let currency = args.get(1).map(|v| v.to_display_string()).unwrap_or_else(|| "USD".to_string());
7        let info = get_currency_info(&currency);
8        let formatted = format_number(amount, info.decimals);
9        Ok(Value::String(format!("{}{}", info.symbol, formatted)))
10    });
11
12    rp.register_builtin("money.parse", |args, _| {
13        let s = args.first().map(|v| v.to_display_string()).unwrap_or_default();
14        let cleaned: String = s.chars().filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-').collect();
15        Ok(Value::Number(cleaned.parse::<f64>().unwrap_or(0.0)))
16    });
17
18    rp.register_builtin("money.add", |args, _| {
19        let a = args.first().map(|v| v.to_number()).unwrap_or(0.0);
20        let b = args.get(1).map(|v| v.to_number()).unwrap_or(0.0);
21        Ok(Value::Number(round_cents(a + b)))
22    });
23
24    rp.register_builtin("money.subtract", |args, _| {
25        let a = args.first().map(|v| v.to_number()).unwrap_or(0.0);
26        let b = args.get(1).map(|v| v.to_number()).unwrap_or(0.0);
27        Ok(Value::Number(round_cents(a - b)))
28    });
29
30    rp.register_builtin("money.multiply", |args, _| {
31        let a = args.first().map(|v| v.to_number()).unwrap_or(0.0);
32        let b = args.get(1).map(|v| v.to_number()).unwrap_or(1.0);
33        Ok(Value::Number(round_cents(a * b)))
34    });
35
36    rp.register_builtin("money.divide", |args, _| {
37        let a = args.first().map(|v| v.to_number()).unwrap_or(0.0);
38        let b = args.get(1).map(|v| v.to_number()).unwrap_or(1.0);
39        if b == 0.0 {
40            Err("money.divide: division by zero".to_string())
41        } else {
42            Ok(Value::Number(round_cents(a / b)))
43        }
44    });
45
46    rp.register_builtin("money.round", |args, _| {
47        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
48        let decimals = args.get(1).map(|v| v.to_number() as i32).unwrap_or(2);
49        let factor = 10f64.powi(decimals);
50        Ok(Value::Number((amount * factor).round() / factor))
51    });
52
53    rp.register_builtin("money.convert", |args, _| {
54        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
55        let rate = args.get(1).map(|v| v.to_number()).unwrap_or(1.0);
56        Ok(Value::Number(round_cents(amount * rate)))
57    });
58
59    rp.register_builtin("money.split", |args, _| {
60        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
61        let ways = args.get(1).map(|v| v.to_number() as usize).unwrap_or(2).max(1);
62        let each = round_cents(amount / ways as f64);
63        let remainder = round_cents(amount - each * ways as f64);
64        let mut parts: Vec<Value> = (0..ways).map(|_| Value::Number(each)).collect();
65        // Distribute remainder to first part
66        if remainder != 0.0 {
67            if let Some(Value::Number(first)) = parts.first_mut() {
68                *first = round_cents(*first + remainder);
69            }
70        }
71        Ok(Value::Array(parts))
72    });
73
74    rp.register_builtin("money.percentage", |args, _| {
75        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
76        let percent = args.get(1).map(|v| v.to_number()).unwrap_or(0.0);
77        Ok(Value::Number(round_cents(amount * percent / 100.0)))
78    });
79
80    rp.register_builtin("money.discount", |args, _| {
81        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
82        let percent = args.get(1).map(|v| v.to_number()).unwrap_or(0.0);
83        Ok(Value::Number(round_cents(amount * (1.0 - percent / 100.0))))
84    });
85
86    rp.register_builtin("money.tax", |args, _| {
87        let amount = args.first().map(|v| v.to_number()).unwrap_or(0.0);
88        let rate = args.get(1).map(|v| v.to_number()).unwrap_or(0.0);
89        Ok(Value::Number(round_cents(amount * (1.0 + rate / 100.0))))
90    });
91
92    rp.register_builtin("money.currencyInfo", |args, _| {
93        let code = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "USD".to_string());
94        let info = get_currency_info(&code);
95        let mut obj = indexmap::IndexMap::new();
96        obj.insert("code".to_string(), Value::String(info.code.to_string()));
97        obj.insert("symbol".to_string(), Value::String(info.symbol.to_string()));
98        obj.insert("name".to_string(), Value::String(info.name.to_string()));
99        obj.insert("decimals".to_string(), Value::Number(info.decimals as f64));
100        Ok(Value::Object(obj))
101    });
102
103    rp.register_builtin("money.listCurrencies", |_args, _| {
104        let codes: Vec<Value> = CURRENCIES.iter().map(|c| Value::String(c.code.to_string())).collect();
105        Ok(Value::Array(codes))
106    });
107
108    rp.register_builtin("money.isValidCode", |args, _| {
109        let code = args.first().map(|v| v.to_display_string()).unwrap_or_default();
110        Ok(Value::Bool(CURRENCIES.iter().any(|c| c.code == code.to_uppercase())))
111    });
112}
113
114fn round_cents(val: f64) -> f64 {
115    (val * 100.0).round() / 100.0
116}
117
118fn format_number(val: f64, decimals: usize) -> String {
119    let formatted = format!("{:.prec$}", val.abs(), prec = decimals);
120    let parts: Vec<&str> = formatted.split('.').collect();
121    let int_part = parts[0];
122
123    // Add thousand separators
124    let mut with_commas = String::new();
125    for (i, ch) in int_part.chars().rev().enumerate() {
126        if i > 0 && i % 3 == 0 {
127            with_commas.push(',');
128        }
129        with_commas.push(ch);
130    }
131    let int_with_commas: String = with_commas.chars().rev().collect();
132
133    if val < 0.0 {
134        if decimals > 0 {
135            format!("-{}.{}", int_with_commas, parts.get(1).unwrap_or(&""))
136        } else {
137            format!("-{}", int_with_commas)
138        }
139    } else if decimals > 0 {
140        format!("{}.{}", int_with_commas, parts.get(1).unwrap_or(&""))
141    } else {
142        int_with_commas
143    }
144}
145
146struct CurrencyInfo {
147    code: &'static str,
148    symbol: &'static str,
149    name: &'static str,
150    decimals: usize,
151}
152
153const CURRENCIES: &[CurrencyInfo] = &[
154    CurrencyInfo { code: "USD", symbol: "$", name: "US Dollar", decimals: 2 },
155    CurrencyInfo { code: "EUR", symbol: "€", name: "Euro", decimals: 2 },
156    CurrencyInfo { code: "GBP", symbol: "£", name: "British Pound", decimals: 2 },
157    CurrencyInfo { code: "JPY", symbol: "¥", name: "Japanese Yen", decimals: 0 },
158    CurrencyInfo { code: "CNY", symbol: "¥", name: "Chinese Yuan", decimals: 2 },
159    CurrencyInfo { code: "KRW", symbol: "₩", name: "South Korean Won", decimals: 0 },
160    CurrencyInfo { code: "INR", symbol: "₹", name: "Indian Rupee", decimals: 2 },
161    CurrencyInfo { code: "CAD", symbol: "C$", name: "Canadian Dollar", decimals: 2 },
162    CurrencyInfo { code: "AUD", symbol: "A$", name: "Australian Dollar", decimals: 2 },
163    CurrencyInfo { code: "CHF", symbol: "CHF", name: "Swiss Franc", decimals: 2 },
164    CurrencyInfo { code: "BRL", symbol: "R$", name: "Brazilian Real", decimals: 2 },
165    CurrencyInfo { code: "MXN", symbol: "$", name: "Mexican Peso", decimals: 2 },
166    CurrencyInfo { code: "RUB", symbol: "₽", name: "Russian Ruble", decimals: 2 },
167    CurrencyInfo { code: "TRY", symbol: "₺", name: "Turkish Lira", decimals: 2 },
168    CurrencyInfo { code: "SEK", symbol: "kr", name: "Swedish Krona", decimals: 2 },
169    CurrencyInfo { code: "NOK", symbol: "kr", name: "Norwegian Krone", decimals: 2 },
170    CurrencyInfo { code: "DKK", symbol: "kr", name: "Danish Krone", decimals: 2 },
171    CurrencyInfo { code: "PLN", symbol: "zł", name: "Polish Zloty", decimals: 2 },
172    CurrencyInfo { code: "THB", symbol: "฿", name: "Thai Baht", decimals: 2 },
173    CurrencyInfo { code: "SGD", symbol: "S$", name: "Singapore Dollar", decimals: 2 },
174    CurrencyInfo { code: "HKD", symbol: "HK$", name: "Hong Kong Dollar", decimals: 2 },
175    CurrencyInfo { code: "NZD", symbol: "NZ$", name: "New Zealand Dollar", decimals: 2 },
176    CurrencyInfo { code: "ZAR", symbol: "R", name: "South African Rand", decimals: 2 },
177    CurrencyInfo { code: "BTC", symbol: "₿", name: "Bitcoin", decimals: 8 },
178    CurrencyInfo { code: "ETH", symbol: "Ξ", name: "Ethereum", decimals: 8 },
179];
180
181fn get_currency_info(code: &str) -> &'static CurrencyInfo {
182    let upper = code.to_uppercase();
183    CURRENCIES
184        .iter()
185        .find(|c| c.code == upper)
186        .unwrap_or(&CURRENCIES[0])
187}