robinpath_modules/modules/
money_mod.rs1use 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(¤cy);
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 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 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}