Files
ferro-ta/crates/ferro_ta_core/src/currency.rs
T

174 lines
5.0 KiB
Rust
Raw Normal View History

2026-03-30 12:45:52 +05:30
//! Currency metadata and Indian number formatting.
/// Immutable currency descriptor.
///
/// Carries the currency code, symbol, decimal places, and whether to use
/// Indian lakh/crore grouping (1,23,45,678.00) instead of standard
/// Western grouping (1,234,567.89).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Currency {
/// IETF currency code, e.g. "INR", "USD".
pub code: &'static str,
/// Display symbol, e.g. "₹", "$".
pub symbol: &'static str,
/// Number of decimal places for formatting.
pub decimal_places: u8,
/// Use Indian lakh/crore digit grouping (true only for INR).
pub lakh_grouping: bool,
}
impl Currency {
pub const INR: Currency = Currency {
code: "INR",
symbol: "₹",
decimal_places: 2,
lakh_grouping: true,
};
pub const USD: Currency = Currency {
code: "USD",
symbol: "$",
decimal_places: 2,
lakh_grouping: false,
};
pub const EUR: Currency = Currency {
code: "EUR",
symbol: "€",
decimal_places: 2,
lakh_grouping: false,
};
pub const GBP: Currency = Currency {
code: "GBP",
symbol: "£",
decimal_places: 2,
lakh_grouping: false,
};
pub const JPY: Currency = Currency {
code: "JPY",
symbol: "¥",
decimal_places: 0,
lakh_grouping: false,
};
pub const USDT: Currency = Currency {
code: "USDT",
symbol: "₮",
decimal_places: 2,
lakh_grouping: false,
};
/// Look up a currency by IETF code (case-insensitive).
/// Returns `None` if the code is not recognised.
pub fn from_code(code: &str) -> Option<&'static Currency> {
match code.to_ascii_uppercase().as_str() {
"INR" => Some(&Currency::INR),
"USD" => Some(&Currency::USD),
"EUR" => Some(&Currency::EUR),
"GBP" => Some(&Currency::GBP),
"JPY" => Some(&Currency::JPY),
"USDT" => Some(&Currency::USDT),
_ => None,
}
}
/// Format `amount` according to this currency's style.
///
/// - INR uses Indian lakh/crore grouping: `₹1,23,45,678.00`
/// - Others use standard Western grouping: `$1,234,567.89`
pub fn format(&self, amount: f64) -> String {
let neg = amount < 0.0;
let abs = amount.abs();
let integer_part = abs.floor() as u64;
let frac_part = abs - abs.floor();
let grouped = if self.lakh_grouping {
format_lakh(integer_part)
} else {
format_standard(integer_part)
};
let dp = self.decimal_places as usize;
let decimal_str = if dp > 0 {
let frac = (frac_part * 10f64.powi(dp as i32)).round() as u64;
format!(".{:0>width$}", frac, width = dp)
} else {
String::new()
};
let sign = if neg { "-" } else { "" };
format!("{}{}{}{}", sign, self.symbol, grouped, decimal_str)
}
}
/// Indian lakh/crore grouping: last 3 digits, then groups of 2 from the right.
/// e.g. 12345678 → "1,23,45,678"
fn format_lakh(n: u64) -> String {
let s = n.to_string();
if s.len() <= 3 {
return s;
}
let (rest, last3) = s.split_at(s.len() - 3);
let mut out = String::new();
let chars: Vec<char> = rest.chars().collect();
let first_len = chars.len() % 2;
if first_len > 0 {
out.push_str(&chars[..first_len].iter().collect::<String>());
}
let mut i = first_len;
while i < chars.len() {
if !out.is_empty() {
out.push(',');
}
out.push_str(&chars[i..i + 2].iter().collect::<String>());
i += 2;
}
if !out.is_empty() {
out.push(',');
}
out.push_str(last3);
out
}
/// Standard Western grouping: groups of 3 digits from the right.
/// e.g. 1234567 → "1,234,567"
fn format_standard(n: u64) -> String {
let s = n.to_string();
let mut out = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
out.push(',');
}
out.push(c);
}
out.chars().rev().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inr_format() {
assert_eq!(Currency::INR.format(123456.78), "₹1,23,456.78");
assert_eq!(Currency::INR.format(10000000.0), "₹1,00,00,000.00");
assert_eq!(Currency::INR.format(100.0), "₹100.00");
assert_eq!(Currency::INR.format(-5000.0), "-₹5,000.00");
}
#[test]
fn test_usd_format() {
assert_eq!(Currency::USD.format(1234567.89), "$1,234,567.89");
assert_eq!(Currency::USD.format(0.5), "$0.50");
}
#[test]
fn test_jpy_format() {
assert_eq!(Currency::JPY.format(1000000.0), "¥1,000,000");
}
#[test]
fn test_from_code() {
assert_eq!(Currency::from_code("inr"), Some(&Currency::INR));
assert_eq!(Currency::from_code("USD"), Some(&Currency::USD));
assert_eq!(Currency::from_code("UNKNOWN"), None);
}
}