Skip to main content

karbon_framework/util/
number.rs

1/// Number formatting and utility helpers
2pub struct NumberHelper;
3
4impl NumberHelper {
5    /// Format a file size in human-readable format (French units)
6    pub fn filesize_format(bytes: u64) -> String {
7        const UNITS: &[&str] = &["o", "Ko", "Mo", "Go", "To"];
8        let mut size = bytes as f64;
9        let mut unit_idx = 0;
10
11        while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
12            size /= 1024.0;
13            unit_idx += 1;
14        }
15
16        if unit_idx == 0 {
17            format!("{} {}", bytes, UNITS[0])
18        } else {
19            format!("{:.1} {}", size, UNITS[unit_idx])
20        }
21    }
22
23    /// Format a number with thousands separator
24    /// e.g., 1234567 => "1 234 567" (French style with spaces)
25    pub fn format_number(value: i64, separator: Option<&str>) -> String {
26        let separator = separator.unwrap_or(" ");
27        let negative = value < 0;
28        let s = value.unsigned_abs().to_string();
29        let chars: Vec<char> = s.chars().collect();
30        let mut result = String::new();
31
32        for (i, c) in chars.iter().enumerate() {
33            if i > 0 && (chars.len() - i) % 3 == 0 {
34                result.push_str(separator);
35            }
36            result.push(*c);
37        }
38
39        if negative {
40            format!("-{}", result)
41        } else {
42            result
43        }
44    }
45
46    /// Format a float with specified decimal places and thousands separator
47    pub fn format_decimal(value: f64, decimals: usize, separator: Option<&str>) -> String {
48        let integer_part = value.trunc() as i64;
49        let formatted_int = Self::format_number(integer_part, separator);
50
51        if decimals == 0 {
52            return formatted_int;
53        }
54
55        let decimal_part = (value.fract().abs() * 10_f64.powi(decimals as i32)).round() as u64;
56        let decimal_str = format!("{:0>width$}", decimal_part, width = decimals);
57
58        format!("{},{}", formatted_int, decimal_str)
59    }
60
61    /// Format as ordinal (French): 1 => "1er", 2 => "2e", etc.
62    pub fn ordinal(value: u64) -> String {
63        if value == 1 {
64            "1er".to_string()
65        } else {
66            format!("{}e", value)
67        }
68    }
69
70    /// Format as percentage
71    /// e.g., percentage(0.756, 1) => "75.6%"
72    pub fn percentage(value: f64, decimals: usize) -> String {
73        format!("{:.prec$}%", value * 100.0, prec = decimals)
74    }
75
76    /// Clamp a value between min and max
77    pub fn clamp<T: PartialOrd>(value: T, min: T, max: T) -> T {
78        if value < min {
79            min
80        } else if value > max {
81            max
82        } else {
83            value
84        }
85    }
86
87    /// Convert bytes to a human-readable duration string (French)
88    /// e.g., 3661 => "1h 1min 1s"
89    pub fn duration_format(total_seconds: u64) -> String {
90        let hours = total_seconds / 3600;
91        let minutes = (total_seconds % 3600) / 60;
92        let seconds = total_seconds % 60;
93
94        let mut parts = Vec::new();
95
96        if hours > 0 {
97            parts.push(format!("{}h", hours));
98        }
99        if minutes > 0 {
100            parts.push(format!("{}min", minutes));
101        }
102        if seconds > 0 || parts.is_empty() {
103            parts.push(format!("{}s", seconds));
104        }
105
106        parts.join(" ")
107    }
108
109    /// Round a float to n decimal places
110    pub fn round(value: f64, decimals: u32) -> f64 {
111        let factor = 10_f64.powi(decimals as i32);
112        (value * factor).round() / factor
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_filesize_format() {
122        assert_eq!(NumberHelper::filesize_format(0), "0 o");
123        assert_eq!(NumberHelper::filesize_format(512), "512 o");
124        assert_eq!(NumberHelper::filesize_format(1024), "1.0 Ko");
125        assert_eq!(NumberHelper::filesize_format(1536), "1.5 Ko");
126        assert_eq!(NumberHelper::filesize_format(1048576), "1.0 Mo");
127        assert_eq!(NumberHelper::filesize_format(1073741824), "1.0 Go");
128    }
129
130    #[test]
131    fn test_format_number() {
132        assert_eq!(NumberHelper::format_number(1234567, None), "1 234 567");
133        assert_eq!(NumberHelper::format_number(42, None), "42");
134        assert_eq!(NumberHelper::format_number(1000, Some(",")), "1,000");
135        assert_eq!(NumberHelper::format_number(-1234, None), "-1 234");
136        assert_eq!(NumberHelper::format_number(0, None), "0");
137    }
138
139    #[test]
140    fn test_format_decimal() {
141        assert_eq!(NumberHelper::format_decimal(1234.56, 2, None), "1 234,56");
142        assert_eq!(NumberHelper::format_decimal(0.5, 2, None), "0,50");
143        assert_eq!(NumberHelper::format_decimal(1000.0, 0, None), "1 000");
144    }
145
146    #[test]
147    fn test_ordinal() {
148        assert_eq!(NumberHelper::ordinal(1), "1er");
149        assert_eq!(NumberHelper::ordinal(2), "2e");
150        assert_eq!(NumberHelper::ordinal(100), "100e");
151    }
152
153    #[test]
154    fn test_percentage() {
155        assert_eq!(NumberHelper::percentage(0.756, 1), "75.6%");
156        assert_eq!(NumberHelper::percentage(1.0, 0), "100%");
157        assert_eq!(NumberHelper::percentage(0.5, 0), "50%");
158    }
159
160    #[test]
161    fn test_clamp() {
162        assert_eq!(NumberHelper::clamp(5, 0, 10), 5);
163        assert_eq!(NumberHelper::clamp(-5, 0, 10), 0);
164        assert_eq!(NumberHelper::clamp(15, 0, 10), 10);
165        assert_eq!(NumberHelper::clamp(0.5, 0.0, 1.0), 0.5);
166    }
167
168    #[test]
169    fn test_duration_format() {
170        assert_eq!(NumberHelper::duration_format(0), "0s");
171        assert_eq!(NumberHelper::duration_format(45), "45s");
172        assert_eq!(NumberHelper::duration_format(90), "1min 30s");
173        assert_eq!(NumberHelper::duration_format(3600), "1h");
174        assert_eq!(NumberHelper::duration_format(3661), "1h 1min 1s");
175        assert_eq!(NumberHelper::duration_format(7200), "2h");
176    }
177
178    #[test]
179    fn test_round() {
180        assert_eq!(NumberHelper::round(3.14159, 2), 3.14);
181        assert_eq!(NumberHelper::round(3.145, 2), 3.15);
182        assert_eq!(NumberHelper::round(3.0, 2), 3.0);
183    }
184}