Skip to main content

flat/
parse.rs

1/// Parse a human-friendly number with decimal (SI) suffixes.
2///
3/// - `k` / `K` = ×1,000
4/// - `M` = ×1,000,000
5/// - `G` = ×1,000,000,000
6///
7/// Used for token counts and other abstract quantities.
8pub fn parse_decimal_number(input: &str) -> Result<usize, String> {
9    let input = input.trim();
10    if input.is_empty() {
11        return Err("empty input".to_string());
12    }
13
14    let (digits, multiplier) = match input.as_bytes().last() {
15        Some(b'k' | b'K') => (&input[..input.len() - 1], 1_000usize),
16        Some(b'M') => (&input[..input.len() - 1], 1_000_000),
17        Some(b'G') => (&input[..input.len() - 1], 1_000_000_000),
18        _ => (input, 1),
19    };
20
21    let base: usize = digits
22        .parse()
23        .map_err(|_| format!("invalid number: '{input}'"))?;
24
25    base.checked_mul(multiplier)
26        .ok_or_else(|| format!("number too large: '{input}'"))
27}
28
29/// Parse a human-friendly number with binary (IEC) suffixes.
30///
31/// - `k` / `K` = ×1,024
32/// - `M` = ×1,048,576
33/// - `G` = ×1,073,741,824
34///
35/// Used for byte sizes where binary multipliers are conventional.
36pub fn parse_binary_number(input: &str) -> Result<u64, String> {
37    let input = input.trim();
38    if input.is_empty() {
39        return Err("empty input".to_string());
40    }
41
42    let (digits, multiplier) = match input.as_bytes().last() {
43        Some(b'k' | b'K') => (&input[..input.len() - 1], 1_024u64),
44        Some(b'M') => (&input[..input.len() - 1], 1_048_576),
45        Some(b'G') => (&input[..input.len() - 1], 1_073_741_824),
46        _ => (input, 1),
47    };
48
49    let base: u64 = digits
50        .parse()
51        .map_err(|_| format!("invalid number: '{input}'"))?;
52
53    base.checked_mul(multiplier)
54        .ok_or_else(|| format!("number too large: '{input}'"))
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    // ── Decimal parsing ──────────────────────────────────────────────
62
63    #[test]
64    fn decimal_plain_number() {
65        assert_eq!(parse_decimal_number("10000").unwrap(), 10_000);
66    }
67
68    #[test]
69    fn decimal_zero() {
70        assert_eq!(parse_decimal_number("0").unwrap(), 0);
71    }
72
73    #[test]
74    fn decimal_suffix_k_lower() {
75        assert_eq!(parse_decimal_number("10k").unwrap(), 10_000);
76    }
77
78    #[test]
79    fn decimal_suffix_k_upper() {
80        assert_eq!(parse_decimal_number("10K").unwrap(), 10_000);
81    }
82
83    #[test]
84    fn decimal_suffix_m() {
85        assert_eq!(parse_decimal_number("5M").unwrap(), 5_000_000);
86    }
87
88    #[test]
89    fn decimal_suffix_g() {
90        assert_eq!(parse_decimal_number("2G").unwrap(), 2_000_000_000);
91    }
92
93    #[test]
94    fn decimal_one_k() {
95        assert_eq!(parse_decimal_number("1k").unwrap(), 1_000);
96    }
97
98    #[test]
99    fn decimal_100k() {
100        assert_eq!(parse_decimal_number("100K").unwrap(), 100_000);
101    }
102
103    #[test]
104    fn decimal_whitespace_trimmed() {
105        assert_eq!(parse_decimal_number("  8k  ").unwrap(), 8_000);
106    }
107
108    #[test]
109    fn decimal_invalid_letters() {
110        assert!(parse_decimal_number("abc").is_err());
111    }
112
113    #[test]
114    fn decimal_invalid_decimal_point() {
115        assert!(parse_decimal_number("1.5k").is_err());
116    }
117
118    #[test]
119    fn decimal_empty_input() {
120        assert!(parse_decimal_number("").is_err());
121    }
122
123    #[test]
124    fn decimal_suffix_only() {
125        assert!(parse_decimal_number("k").is_err());
126    }
127
128    #[test]
129    fn decimal_negative() {
130        assert!(parse_decimal_number("-1k").is_err());
131    }
132
133    #[test]
134    fn decimal_overflow() {
135        // usize::MAX / 1000 + 1 with k suffix should overflow
136        let huge = format!("{}k", usize::MAX);
137        assert!(parse_decimal_number(&huge).is_err());
138    }
139
140    // ── Binary parsing ───────────────────────────────────────────────
141
142    #[test]
143    fn binary_plain_number() {
144        assert_eq!(parse_binary_number("1048576").unwrap(), 1_048_576);
145    }
146
147    #[test]
148    fn binary_zero() {
149        assert_eq!(parse_binary_number("0").unwrap(), 0);
150    }
151
152    #[test]
153    fn binary_suffix_k_lower() {
154        assert_eq!(parse_binary_number("1k").unwrap(), 1_024);
155    }
156
157    #[test]
158    fn binary_suffix_k_upper() {
159        assert_eq!(parse_binary_number("10K").unwrap(), 10_240);
160    }
161
162    #[test]
163    fn binary_suffix_m() {
164        assert_eq!(parse_binary_number("1M").unwrap(), 1_048_576);
165    }
166
167    #[test]
168    fn binary_suffix_m_10() {
169        assert_eq!(parse_binary_number("10M").unwrap(), 10_485_760);
170    }
171
172    #[test]
173    fn binary_suffix_g() {
174        assert_eq!(parse_binary_number("1G").unwrap(), 1_073_741_824);
175    }
176
177    #[test]
178    fn binary_whitespace_trimmed() {
179        assert_eq!(parse_binary_number("  5M  ").unwrap(), 5_242_880);
180    }
181
182    #[test]
183    fn binary_invalid_letters() {
184        assert!(parse_binary_number("xyz").is_err());
185    }
186
187    #[test]
188    fn binary_invalid_decimal_point() {
189        assert!(parse_binary_number("1.5M").is_err());
190    }
191
192    #[test]
193    fn binary_empty_input() {
194        assert!(parse_binary_number("").is_err());
195    }
196
197    #[test]
198    fn binary_suffix_only() {
199        assert!(parse_binary_number("M").is_err());
200    }
201
202    #[test]
203    fn binary_overflow() {
204        let huge = format!("{}G", u64::MAX);
205        assert!(parse_binary_number(&huge).is_err());
206    }
207}