/// Number, date, and currency formatting per locale. /// /// Formatting is locale-sensitive: number grouping, decimal separators, /// currency symbol placement, and date ordering all vary by locale. /// Use these formatters rather than hardcoding formatting logic. use crate::locale::Locale; /// Format a number with locale-appropriate grouping and decimals. /// /// Examples: /// - en-US: 1,234,567.89 /// - de-DE: 1.234.567,89 /// - fr-FR: 1 234 567,89 pub fn format_number(value: f64, locale: &Locale, decimal_places: usize) -> String { let (group_sep, decimal_sep) = separators_for_locale(locale); let rounded = round_to(value, decimal_places); let is_negative = rounded < 0.0; let abs_value = rounded.abs(); let int_part = abs_value.trunc() as u64; let frac_part = ((abs_value.fract() * 10f64.powi(decimal_places as i32)).round()) as u64; let int_str = format_integer_with_grouping(int_part, group_sep); let result = if decimal_places > 0 { format!( "{}{}{}", int_str, decimal_sep, format!("{:0>width$}", frac_part, width = decimal_places) ) } else { int_str }; if is_negative { format!("-{}", result) } else { result } } /// Format a currency value with locale-appropriate symbol and placement. /// /// Examples: /// - en-US / USD: $1,234.56 /// - de-DE / EUR: 1.234,56 € /// - ja / JPY: ¥1,235 pub fn format_currency(value: f64, locale: &Locale, currency_code: &str) -> String { let (symbol, prefix, decimals) = currency_info(currency_code); let formatted = format_number(value, locale, decimals); if prefix { format!("{}{}", symbol, formatted) } else { format!("{} {}", formatted, symbol) } } /// Format an integer with locale-appropriate grouping separators. pub fn format_integer(value: i64, locale: &Locale) -> String { let (group_sep, _) = separators_for_locale(locale); let is_negative = value < 0; let abs_val = value.unsigned_abs(); let grouped = format_integer_with_grouping(abs_val, group_sep); if is_negative { format!("-{}", grouped) } else { grouped } } /// Format a percentage (0.85 → "85%", locale-aware). pub fn format_percent(value: f64, locale: &Locale, decimal_places: usize) -> String { let pct = value * 100.0; let (_, decimal_sep) = separators_for_locale(locale); let int_part = pct.trunc() as u64; let frac = ((pct.fract() * 10f64.powi(decimal_places as i32)).round()) as u64; if decimal_places > 0 { format!( "{}{}{}%", int_part, decimal_sep, format!("{:0>width$}", frac, width = decimal_places) ) } else { format!("{}%", int_part) } } // --- Internal helpers --- fn separators_for_locale(locale: &Locale) -> (char, char) { match locale.language.as_str() { // Comma grouping, period decimal (en-US style) "en" | "ja" | "ko" | "zh" | "th" => (',', '.'), // Period grouping, comma decimal (European style) "de" | "nl" | "it" | "pt" | "es" | "tr" | "pl" | "ru" | "uk" | "el" => ('.', ','), // Thin space grouping, comma decimal (French style) "fr" | "sv" | "no" | "nb" | "da" | "fi" => ('\u{202F}', ','), // Default: comma grouping, period decimal _ => (',', '.'), } } fn format_integer_with_grouping(value: u64, sep: char) -> String { let s = value.to_string(); if s.len() <= 3 { return s; } let mut result = String::new(); let chars: Vec = s.chars().collect(); let len = chars.len(); for (i, &ch) in chars.iter().enumerate() { if i > 0 && (len - i) % 3 == 0 { result.push(sep); } result.push(ch); } result } fn round_to(value: f64, places: usize) -> f64 { let factor = 10f64.powi(places as i32); (value * factor).round() / factor } fn currency_info(code: &str) -> (&'static str, bool, usize) { // (symbol, prefix, decimal_places) match code.to_uppercase().as_str() { "USD" => ("$", true, 2), "EUR" => ("€", false, 2), "GBP" => ("£", true, 2), "JPY" => ("¥", true, 0), "CNY" => ("¥", true, 2), "KRW" => ("₩", true, 0), "INR" => ("₹", true, 2), "CHF" => ("CHF", true, 2), "CAD" => ("CA$", true, 2), "AUD" => ("A$", true, 2), "BRL" => ("R$", true, 2), "MXN" => ("MX$", true, 2), "RUB" => ("₽", false, 2), "SEK" => ("kr", false, 2), "NOK" => ("kr", false, 2), "DKK" => ("kr", false, 2), "PLN" => ("zł", false, 2), "TRY" => ("₺", true, 2), "SAR" => ("﷼", false, 2), "AED" => ("د.إ", false, 2), _ => ("¤", true, 2), // generic currency sign for unknown codes } } #[cfg(test)] mod tests { use super::*; #[test] fn format_number_en_us() { let locale = Locale::en_us(); assert_eq!(format_number(1234567.89, &locale, 2), "1,234,567.89"); } #[test] fn format_number_de() { let locale = Locale::new("de-DE"); assert_eq!(format_number(1234.56, &locale, 2), "1.234,56"); } #[test] fn format_number_no_decimals() { let locale = Locale::en_us(); assert_eq!(format_number(42.0, &locale, 0), "42"); } #[test] fn format_number_negative() { let locale = Locale::en_us(); assert_eq!(format_number(-1000.0, &locale, 2), "-1,000.00"); } #[test] fn format_currency_usd() { let locale = Locale::en_us(); assert_eq!(format_currency(1234.56, &locale, "USD"), "$1,234.56"); } #[test] fn format_currency_jpy_no_decimals() { let locale = Locale::ja(); assert_eq!(format_currency(1234.0, &locale, "JPY"), "¥1,234"); } #[test] fn format_integer_groups() { let locale = Locale::en_us(); assert_eq!(format_integer(1000000, &locale), "1,000,000"); } #[test] fn format_integer_small() { let locale = Locale::en_us(); assert_eq!(format_integer(42, &locale), "42"); } #[test] fn format_percent_whole() { let locale = Locale::en_us(); assert_eq!(format_percent(0.85, &locale, 0), "85%"); } #[test] fn format_percent_with_decimal() { let locale = Locale::en_us(); assert_eq!(format_percent(0.856, &locale, 1), "85.6%"); } }