Skip to main content

formualizer_eval/
locale.rs

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