1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// Locale contract for the engine.
///
/// Milestone 0 intentionally uses an invariant locale:
///
/// - Numeric parsing is ASCII/invariant only (`.` decimal separator; no thousands separators),
/// with support for trailing percent suffix (`"90%" -> 0.9`).
/// - Strings are case-folded with ASCII-only rules (`to_ascii_lowercase`).
///
/// This means locale-dependent inputs like `"1.234,56"` are *not* interpreted as numbers.
/// Callers should surface `#VALUE!` for locale-dependent numeric coercions (e.g. `VALUE()`)
/// rather than silently producing a wrong number.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Locale;
impl Locale {
pub const fn invariant() -> Self {
Locale
}
/// Parse a number using invariant rules (ASCII, dot decimal separator).
///
/// Also supports percent-suffixed numeric text (e.g. "90%" -> 0.9),
/// matching spreadsheet numeric-coercion behavior in numeric contexts.
pub fn parse_number_invariant(&self, s: &str) -> Option<f64> {
let trimmed = s.trim();
if let Some(without_pct) = trimmed.strip_suffix('%') {
let n = without_pct.trim().parse::<f64>().ok()?;
Some(n / 100.0)
} else {
trimmed.parse::<f64>().ok()
}
}
/// Case folding for comparisons; invariant = ASCII lower.
pub fn fold_case_invariant(&self, s: &str) -> String {
s.to_ascii_lowercase()
}
}
#[cfg(test)]
mod tests {
use super::Locale;
#[test]
fn parse_number_invariant_supports_percent_suffix() {
let loc = Locale::invariant();
assert_eq!(loc.parse_number_invariant("90%"), Some(0.9));
assert_eq!(loc.parse_number_invariant(" 90.5% "), Some(0.905));
assert_eq!(loc.parse_number_invariant("90 %"), Some(0.9));
}
#[test]
fn parse_number_invariant_rejects_invalid_percent_text() {
let loc = Locale::invariant();
assert_eq!(loc.parse_number_invariant("abc%"), None);
assert_eq!(loc.parse_number_invariant("%"), None);
assert_eq!(loc.parse_number_invariant("90% trailing"), None);
}
}