Skip to main content

mf2_runtime/
plural.rs

1//! The plural-rule evaluator (decision D3; P0.4): one encoded
2//! `plural.cardinal` / `plural.ordinal` LOCALE entry
3//! (`plans/02-catalog-format.md` §4.1) applied to the UTS #35 operands of a
4//! *formatted* number. No allocation, no panic: malformed data stops the
5//! evaluation with `other`, the spec's catch-all.
6
7/// Operand codes (relation header bits 0–2); `c` and `e` share code 6.
8mod op {
9    pub(super) const N: u8 = 0;
10    pub(super) const I: u8 = 1;
11    pub(super) const V: u8 = 2;
12    pub(super) const W: u8 = 3;
13    pub(super) const F: u8 = 4;
14    pub(super) const T: u8 = 5;
15    pub(super) const E: u8 = 6;
16}
17
18/// Modulus field value: an explicit LEB128 modulus follows.
19const MOD_EXPLICIT: u8 = 7;
20/// Relation header bit 6: `!=`.
21const REL_NEGATED: u8 = 0x40;
22/// Relation header bit 7: last relation of its AND group.
23const REL_LAST: u8 = 0x80;
24/// Item bit 0: last item of the list.
25const ITEM_LAST: u64 = 1;
26/// Item bit 1: a range; `hi − lo` follows.
27const ITEM_RANGE: u64 = 2;
28/// Rule header bits 0–4: the number of OR groups.
29const RULE_GROUPS_MASK: u8 = 0x1f;
30
31/// 10^18: operand values at or above it are kept as `10^18 + (value mod
32/// 10^18)` — exact for every CLDR modulus and literal (§4.1).
33pub(crate) const BIG: u64 = 1_000_000_000_000_000_000;
34
35/// A CLDR plural category.
36#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
37#[repr(u8)]
38pub enum Category {
39    /// `zero`
40    Zero = 0,
41    /// `one`
42    One = 1,
43    /// `two`
44    Two = 2,
45    /// `few`
46    Few = 3,
47    /// `many`
48    Many = 4,
49    /// `other`
50    Other = 5,
51}
52
53impl Category {
54    /// The category of a rule-header code; codes ≥ 5 are `other`.
55    pub const fn from_code(code: u8) -> Category {
56        match code {
57            0 => Category::Zero,
58            1 => Category::One,
59            2 => Category::Two,
60            3 => Category::Few,
61            4 => Category::Many,
62            _ => Category::Other,
63        }
64    }
65
66    /// The keyword a variant key is compared with.
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Category::Zero => "zero",
70            Category::One => "one",
71            Category::Two => "two",
72            Category::Few => "few",
73            Category::Many => "many",
74            Category::Other => "other",
75        }
76    }
77
78    /// The category named `s`.
79    pub fn from_keyword(s: &str) -> Option<Category> {
80        Some(match s {
81            "zero" => Category::Zero,
82            "one" => Category::One,
83            "two" => Category::Two,
84            "few" => Category::Few,
85            "many" => Category::Many,
86            "other" => Category::Other,
87            _ => return None,
88        })
89    }
90}
91
92/// The UTS #35 operands of a non-negative decimal (Part 3 §5.1.1): the
93/// formatter's side of the contract of `plans/02-catalog-format.md` §4.1.
94/// `n` is integral iff `t == 0`, and then equals `i`.
95#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
96pub struct Operands {
97    /// Integer digits.
98    pub i: u64,
99    /// Visible fraction digits, with trailing zeros.
100    pub f: u64,
101    /// Visible fraction digits, without trailing zeros.
102    pub t: u64,
103    /// The number of visible fraction digits, with trailing zeros.
104    pub v: u32,
105    /// The number of visible fraction digits, without trailing zeros.
106    pub w: u32,
107    /// The compact decimal exponent (`c`, `e`); 0 from MF2's `:number`.
108    pub e: u32,
109}
110
111/// Accumulates decimal digits, exact below [`BIG`], else keeping the value
112/// modulo 10^18 plus a sticky flag.
113#[derive(Clone, Copy, Default)]
114pub(crate) struct Acc {
115    low: u64,
116    big: bool,
117}
118
119impl Acc {
120    pub(crate) fn push(&mut self, digit: u8) {
121        // low < 10^18, so low × 10 + 9 < 2^64.
122        let x = self
123            .low
124            .wrapping_mul(10)
125            .wrapping_add(u64::from(digit.min(9)));
126        if x >= BIG {
127            self.big = true;
128            self.low = x % BIG;
129        } else {
130            self.low = x;
131        }
132    }
133
134    pub(crate) fn value(self) -> u64 {
135        if self.big {
136            self.low.wrapping_add(BIG)
137        } else {
138            self.low
139        }
140    }
141}
142
143impl Operands {
144    /// The operands of a CLDR sample or an ASCII decimal:
145    /// `-? digit+ ('.' digit+)? ([ce] digit+)?`. The exponent moves the
146    /// decimal point right and keeps visible trailing zeros (`1.20050c3` is
147    /// `1200.50`, `e = 3`). `None` if `s` does not match.
148    pub fn parse(s: &str) -> Option<Operands> {
149        let b = s.as_bytes();
150        let b = match b.split_first() {
151            Some((b'-', rest)) => rest,
152            _ => b,
153        };
154        let (mantissa, exponent) = split_at(b, |c| c == b'c' || c == b'e');
155        let e = match exponent {
156            None => 0u32,
157            Some(x) if all_digits(x) => x.iter().try_fold(0u32, |acc, &c| {
158                acc.checked_mul(10)?
159                    .checked_add(u32::from(c.wrapping_sub(b'0')))
160            })?,
161            Some(_) => return None,
162        };
163        let (int, frac) = split_at(mantissa, |c| c == b'.');
164        let frac = match frac {
165            None => &[][..],
166            Some(x) if all_digits(x) => x,
167            Some(_) => return None,
168        };
169        if !all_digits(int) {
170            return None;
171        }
172        let point = int.len().saturating_add(usize::try_from(e).ok()?);
173        let mut ops = OperandsBuilder::default();
174        for (k, &c) in int.iter().chain(frac.iter()).enumerate() {
175            ops.digit(c.wrapping_sub(b'0'), k < point);
176        }
177        // An exponent past the digits pads the integer with zeros; after 18
178        // zero pushes `low` is 0 and more change nothing, so 19 is exact.
179        let total = int.len().saturating_add(frac.len());
180        for _ in 0..point.saturating_sub(total).min(19) {
181            ops.digit(0, true);
182        }
183        let mut o = ops.finish();
184        o.e = e;
185        Some(o)
186    }
187}
188
189/// Builds operands from digits, most significant first.
190#[derive(Default)]
191pub(crate) struct OperandsBuilder {
192    i: Acc,
193    f: Acc,
194    t: Acc,
195    v: u32,
196    w: u32,
197    zeros: u32,
198}
199
200impl OperandsBuilder {
201    /// One digit, of the integer part or of the fraction.
202    pub(crate) fn digit(&mut self, d: u8, integer: bool) {
203        if integer {
204            self.i.push(d);
205            return;
206        }
207        self.f.push(d);
208        self.v = self.v.saturating_add(1);
209        if d == 0 {
210            self.zeros = self.zeros.saturating_add(1);
211        } else {
212            for _ in 0..self.zeros.min(19) {
213                self.t.push(0);
214            }
215            self.t.push(d);
216            self.zeros = 0;
217            self.w = self.v;
218        }
219    }
220
221    pub(crate) fn finish(self) -> Operands {
222        Operands {
223            i: self.i.value(),
224            f: self.f.value(),
225            t: self.t.value(),
226            v: self.v,
227            w: self.w,
228            e: 0,
229        }
230    }
231}
232
233fn all_digits(s: &[u8]) -> bool {
234    !s.is_empty() && s.iter().all(u8::is_ascii_digit)
235}
236
237fn split_at(s: &[u8], pred: impl Fn(u8) -> bool) -> (&[u8], Option<&[u8]>) {
238    match s.iter().position(|&c| pred(c)) {
239        Some(p) => match (s.get(..p), s.get(p.wrapping_add(1)..)) {
240            (Some(a), Some(b)) => (a, Some(b)),
241            _ => (s, None),
242        },
243        None => (s, None),
244    }
245}
246
247/// The plural category of `o` under the encoded rules `entry`. An empty
248/// entry (every number is `other`) is valid; malformed data gives `other`.
249pub fn select(entry: &[u8], o: &Operands) -> Category {
250    eval(entry, o).unwrap_or(Category::Other)
251}
252
253struct Reader<'a>(&'a [u8]);
254
255impl Reader<'_> {
256    fn byte(&mut self) -> Option<u8> {
257        let (&b, rest) = self.0.split_first()?;
258        self.0 = rest;
259        Some(b)
260    }
261
262    /// Unsigned LEB128, at most 10 bytes.
263    fn varint(&mut self) -> Option<u64> {
264        let mut value = 0u64;
265        let mut shift = 0u32;
266        loop {
267            let b = self.byte()?;
268            value |= u64::from(b & 0x7f).checked_shl(shift)?;
269            if b & 0x80 == 0 {
270                return Some(value);
271            }
272            shift = shift.checked_add(7)?;
273        }
274    }
275}
276
277/// `(value, is_integer)` of an operand. Only `n` can be non-integral.
278fn operand(o: &Operands, code: u8) -> Option<(u64, bool)> {
279    Some(match code {
280        op::N => (o.i, o.t == 0),
281        op::I => (o.i, true),
282        op::V => (u64::from(o.v), true),
283        op::W => (u64::from(o.w), true),
284        op::F => (o.f, true),
285        op::T => (o.t, true),
286        op::E => (u64::from(o.e), true),
287        _ => return None,
288    })
289}
290
291// `n i v w f t e` are UTS #35's operand names.
292#[allow(clippy::many_single_char_names)]
293fn eval(entry: &[u8], o: &Operands) -> Option<Category> {
294    let mut r = Reader(entry);
295    while let Some(header) = r.byte() {
296        let mut rule = false;
297        for _ in 0..(header & RULE_GROUPS_MASK) {
298            let mut group = true;
299            loop {
300                let rel = r.byte()?;
301                let (mut x, integral) = operand(o, rel & 7)?;
302                let modulus = match (rel >> 3) & 7 {
303                    0 => None,
304                    MOD_EXPLICIT => Some(r.varint()?),
305                    k => {
306                        // 1 ≤ k ≤ 6: 10^k cannot wrap (and `checked_pow` would
307                        // pull in a 128-bit multiply on wasm32).
308                        let mut m = 1u64;
309                        for _ in 0..k {
310                            m = m.wrapping_mul(10);
311                        }
312                        Some(m)
313                    }
314                };
315                if let Some(m) = modulus {
316                    x = x.checked_rem(m)?;
317                }
318                let mut hit = false;
319                loop {
320                    let item = r.varint()?;
321                    let lo = item >> 2;
322                    let hi = if item & ITEM_RANGE != 0 {
323                        lo.checked_add(r.varint()?)?
324                    } else {
325                        lo
326                    };
327                    hit |= integral && lo <= x && x <= hi;
328                    if item & ITEM_LAST != 0 {
329                        break;
330                    }
331                }
332                group &= hit != (rel & REL_NEGATED != 0);
333                if rel & REL_LAST != 0 {
334                    break;
335                }
336            }
337            rule |= group;
338        }
339        if rule {
340            return Some(Category::from_code(header >> 5));
341        }
342    }
343    Some(Category::Other)
344}
345
346#[cfg(test)]
347#[allow(clippy::many_single_char_names, clippy::unnecessary_wraps)]
348mod tests {
349    use super::{BIG, Category, Operands, select};
350
351    fn ops(s: &str) -> Operands {
352        Operands::parse(s).unwrap_or_default()
353    }
354
355    fn o(i: u64, v: u32, w: u32, f: u64, t: u64, e: u32) -> Option<Operands> {
356        Some(Operands { i, f, t, v, w, e })
357    }
358
359    /// en cardinal `one: i = 1 and v = 0`.
360    const EN: &[u8] = &[0x21, 0x01, 0x05, 0x82, 0x01];
361
362    #[test]
363    fn en_cardinal() {
364        assert_eq!(select(EN, &ops("1")), Category::One);
365        assert_eq!(select(EN, &ops("1.0")), Category::Other);
366        assert_eq!(select(EN, &ops("2")), Category::Other);
367        assert_eq!(select(&[], &ops("1")), Category::Other);
368    }
369
370    #[test]
371    fn malformed_is_other() {
372        for len in 0..EN.len() {
373            let _ = select(EN.get(..len).unwrap_or(&[]), &ops("1"));
374        }
375        assert_eq!(select(&[0x21, 0x07, 0x05], &ops("1")), Category::Other);
376        assert_eq!(
377            select(&[0x21, 0x39, 0x00, 0x05], &ops("1")),
378            Category::Other
379        );
380        assert_eq!(
381            select(&[0x21, 0x01, 0xff, 0xff], &ops("1")),
382            Category::Other
383        );
384        assert_eq!(select(&[0xffu8; 64], &ops("1")), Category::Other);
385    }
386
387    /// UTS #35 Part 3 §5.1.1's operand table.
388    #[test]
389    fn uts35_table() {
390        assert_eq!(Operands::parse("1"), o(1, 0, 0, 0, 0, 0));
391        assert_eq!(Operands::parse("1.0"), o(1, 1, 0, 0, 0, 0));
392        assert_eq!(Operands::parse("1.00"), o(1, 2, 0, 0, 0, 0));
393        assert_eq!(Operands::parse("1.3"), o(1, 1, 1, 3, 3, 0));
394        assert_eq!(Operands::parse("1.30"), o(1, 2, 1, 30, 3, 0));
395        assert_eq!(Operands::parse("1.03"), o(1, 2, 2, 3, 3, 0));
396        assert_eq!(Operands::parse("1.230"), o(1, 3, 2, 230, 23, 0));
397        assert_eq!(Operands::parse("1200000"), o(1_200_000, 0, 0, 0, 0, 0));
398        assert_eq!(Operands::parse("1.2c6"), o(1_200_000, 0, 0, 0, 0, 6));
399        assert_eq!(Operands::parse("123c5"), o(12_300_000, 0, 0, 0, 0, 5));
400        assert_eq!(Operands::parse("1.20050c3"), o(1200, 2, 1, 50, 5, 3));
401        assert_eq!(Operands::parse("1.1e6"), o(1_100_000, 0, 0, 0, 0, 6));
402        assert_eq!(Operands::parse("-1.5"), o(1, 1, 1, 5, 5, 0));
403        assert_eq!(Operands::parse("0.00"), o(0, 2, 0, 0, 0, 0));
404        for bad in ["", "-", ".", "1.", ".5", "1c", "1x", "1..2", "c3"] {
405            assert_eq!(Operands::parse(bad), None, "{bad}");
406        }
407        let x = ops("12345678901234567890123");
408        assert_eq!(x.i, BIG + 678_901_234_567_890_123);
409    }
410}