1mod 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
18const MOD_EXPLICIT: u8 = 7;
20const REL_NEGATED: u8 = 0x40;
22const REL_LAST: u8 = 0x80;
24const ITEM_LAST: u64 = 1;
26const ITEM_RANGE: u64 = 2;
28const RULE_GROUPS_MASK: u8 = 0x1f;
30
31pub(crate) const BIG: u64 = 1_000_000_000_000_000_000;
34
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
37#[repr(u8)]
38pub enum Category {
39 Zero = 0,
41 One = 1,
43 Two = 2,
45 Few = 3,
47 Many = 4,
49 Other = 5,
51}
52
53impl Category {
54 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 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 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#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
96pub struct Operands {
97 pub i: u64,
99 pub f: u64,
101 pub t: u64,
103 pub v: u32,
105 pub w: u32,
107 pub e: u32,
109}
110
111#[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 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 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 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#[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 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
247pub 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 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
277fn 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#[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 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 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 #[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}