1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq)]
8pub enum LocaleId {
9 EnUs,
10 JaJp,
11 DeDe,
12 FrFr,
13 ZhCn,
14}
15
16#[derive(Debug, Clone)]
17pub struct LocaleFormatter {
18 pub locale: LocaleId,
19 pub decimal_sep: char,
20 pub thousands_sep: char,
21 pub date_format: String,
22}
23
24impl LocaleFormatter {
25 pub fn new(locale: LocaleId) -> Self {
26 let (decimal_sep, thousands_sep, date_format) = match &locale {
27 LocaleId::EnUs => ('.', ',', "MM/DD/YYYY".to_string()),
28 LocaleId::JaJp => ('.', ',', "YYYY年MM月DD日".to_string()),
29 LocaleId::DeDe => (',', '.', "DD.MM.YYYY".to_string()),
30 LocaleId::FrFr => (',', ' ', "DD/MM/YYYY".to_string()),
31 LocaleId::ZhCn => ('.', ',', "YYYY-MM-DD".to_string()),
32 };
33 LocaleFormatter {
34 locale,
35 decimal_sep,
36 thousands_sep,
37 date_format,
38 }
39 }
40
41 pub fn format_number(&self, value: f64, decimals: u8) -> String {
42 format_number_locale(value, decimals, self.decimal_sep, self.thousands_sep)
43 }
44
45 pub fn format_date(&self, year: i32, month: u8, day: u8) -> String {
46 format_date_locale(year, month, day, &self.date_format)
47 }
48}
49
50pub fn format_number_locale(
51 value: f64,
52 decimals: u8,
53 decimal_sep: char,
54 thousands_sep: char,
55) -> String {
56 let factor = 10_f64.powi(decimals as i32);
57 let rounded = (value * factor).round() / factor;
58 let int_part = rounded.abs() as u64;
59 let frac_part = ((rounded.abs() - int_part as f64) * factor).round() as u64;
60 let sign = if value < 0.0 { "-" } else { "" };
61 let int_str = format_thousands(int_part, thousands_sep);
62 if decimals == 0 {
63 format!("{}{}", sign, int_str)
64 } else {
65 format!(
66 "{}{}{}{:0>width$}",
67 sign,
68 int_str,
69 decimal_sep,
70 frac_part,
71 width = decimals as usize
72 )
73 }
74}
75
76pub fn format_thousands(mut n: u64, sep: char) -> String {
77 if n == 0 {
78 return "0".to_string();
79 }
80 let mut digits: Vec<char> = Vec::new();
81 let mut count = 0u32;
82 while n > 0 {
83 if count > 0 && count.is_multiple_of(3) {
84 digits.push(sep);
85 }
86 digits.push(char::from_digit((n % 10) as u32, 10).unwrap_or('0'));
87 n /= 10;
88 count += 1;
89 }
90 digits.iter().rev().collect()
91}
92
93pub fn format_date_locale(year: i32, month: u8, day: u8, fmt: &str) -> String {
94 fmt.replace("YYYY", &format!("{:04}", year))
95 .replace("MM", &format!("{:02}", month))
96 .replace("DD", &format!("{:02}", day))
97}
98
99pub fn locale_currency_symbol(locale: &LocaleId) -> &'static str {
100 match locale {
101 LocaleId::EnUs => "$",
102 LocaleId::JaJp => "¥",
103 LocaleId::DeDe | LocaleId::FrFr => "€",
104 LocaleId::ZhCn => "¥",
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn test_format_number_en_us() {
114 let fmt = LocaleFormatter::new(LocaleId::EnUs);
115 let result = fmt.format_number(1234567.89, 2);
116 assert!(result.contains('.') ,);
117 assert!(result.contains(',') ,);
118 }
119
120 #[test]
121 fn test_format_number_de() {
122 let fmt = LocaleFormatter::new(LocaleId::DeDe);
123 let result = fmt.format_number(1234.5, 2);
124 assert!(result.contains(',') ,);
125 }
126
127 #[test]
128 fn test_format_date_ja() {
129 let fmt = LocaleFormatter::new(LocaleId::JaJp);
130 let result = fmt.format_date(2026, 3, 7);
131 assert!(result.contains("2026") ,);
132 assert!(result.contains("03") ,);
133 }
134
135 #[test]
136 fn test_format_date_us() {
137 let fmt = LocaleFormatter::new(LocaleId::EnUs);
138 let result = fmt.format_date(2026, 3, 7);
139 assert_eq!(result, "03/07/2026");
140 }
141
142 #[test]
143 fn test_format_thousands_zero() {
144 assert_eq!(format_thousands(0, ','), "0");
145 }
146
147 #[test]
148 fn test_format_thousands_small() {
149 assert_eq!(format_thousands(999, ','), "999");
150 }
151
152 #[test]
153 fn test_format_thousands_large() {
154 let s = format_thousands(1_000_000, ',');
155 assert_eq!(s, "1,000,000");
156 }
157
158 #[test]
159 fn test_currency_symbol() {
160 assert_eq!(locale_currency_symbol(&LocaleId::EnUs), "$");
161 assert_eq!(locale_currency_symbol(&LocaleId::DeDe), "€");
162 }
163
164 #[test]
165 fn test_negative_number() {
166 let fmt = LocaleFormatter::new(LocaleId::EnUs);
167 let result = fmt.format_number(-42.5, 1);
168 assert!(result.starts_with('-') ,);
169 }
170}