Skip to main content

oxihuman_core/
locale_formatter.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Locale-aware number and date formatter stub.
6
7#[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('.') /* decimal separator present */,);
117        assert!(result.contains(',') /* thousands separator present */,);
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(',') /* German decimal sep */,);
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") /* year present */,);
132        assert!(result.contains("03") /* month present */,);
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('-') /* negative sign present */,);
169    }
170}