Skip to main content

mf2_runtime/
number.rs

1//! The core numeric semantics (`number.md`; `plans/03-runtime.md` §5.1):
2//! `:number`, `:integer` and `:offset` — operand rules, every digit and
3//! rounding option, `signDisplay`, option inheritance, `select` = `exact` /
4//! `plural` / `ordinal`, the exact-match serialization and the plural
5//! operands of the *formatted* number — with neutral output (ASCII digits,
6//! `.`, `-`/`+`, no grouping). Ported from P0.5 over the digit backend of
7//! [`decimal`].
8//!
9//! The display, `:integer`'s rounding and the plural category come from a
10//! backend with one interface: the Rust one (`display.rs`), or — feature
11//! `intl` on `wasm32-unknown-unknown`, [`crate::INTL_NUMBERS`] — the host's
12//! number formatter (`intl.rs`; `plans/03-runtime.md` §2.7, §5.3), and
13//! then the Rust rounding, digit output and plural evaluator are not linked.
14
15mod decimal;
16#[cfg(not(all(feature = "intl", target_arch = "wasm32", target_os = "unknown")))]
17mod display;
18#[cfg(all(feature = "intl", target_arch = "wasm32", target_os = "unknown"))]
19mod intl;
20mod measure;
21mod options;
22mod request;
23
24#[cfg(not(all(feature = "intl", target_arch = "wasm32", target_os = "unknown")))]
25use display as backend;
26#[cfg(all(feature = "intl", target_arch = "wasm32", target_os = "unknown"))]
27use intl as backend;
28
29use crate::error::FormatError;
30use crate::function::{FnContext, Options};
31use crate::host::Host;
32use crate::plural::{self, Category, OperandsBuilder};
33use crate::sink::{ErrorSink, Sink, SubPartSink};
34use crate::value::Value;
35
36use decimal::{Decimal, ParseError, split_literal};
37use options::{
38    CUR, FracDefaults, INT, Kind, NUM, NumOpts, OPTIONS, PCT, Select, UNIT, apply, digit_plan,
39    select_named,
40};
41
42pub(crate) use options::digit_size;
43
44pub use decimal::RoundingMode;
45pub use measure::{Measure, MeasureUnit};
46pub use options::{Grouping, RoundingPriority, SignDisplay};
47pub use request::{
48    CurrencyDisplay, DigitOptions, NumberOut, NumberRequest, NumberStyle, UnitDisplay,
49};
50
51/// An exact decimal and, once a numeric handler resolved it, its resolved
52/// options and its display form. Opaque: the digit backend is internal
53/// (owner decision 1).
54#[derive(Clone)]
55pub struct Number {
56    /// The numeric value (after `:integer`'s rounding and `:offset`'s
57    /// adjustment), unscaled.
58    pub(crate) value: Decimal,
59    /// Set by a numeric handler: the resolved state; `None` for a bare number.
60    resolved: Option<Resolved>,
61}
62
63/// What a numeric handler resolved.
64#[derive(Clone)]
65struct Resolved {
66    opts: NumOpts,
67    frac: FracDefaults,
68    /// A power of ten applied when formatting and selecting (P4 `:percent`).
69    scale: i16,
70    /// `false` when `select` came from a variable or from the operand.
71    selectable: bool,
72    /// What the backend keeps: the rounded display (Rust), or the digit
73    /// plan and the plural category once asked (`intl`).
74    shown: backend::Shown,
75}
76
77/// The sign a formatted number shows, after `signDisplay`.
78#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
79pub enum Sign {
80    /// No sign.
81    None,
82    /// A minus sign.
83    Minus,
84    /// A plus sign.
85    Plus,
86}
87
88/// Digits to show (`plans/03-runtime.md` §2.7): a resolved number's
89/// rounded display digits ([`Number::digits`]), or a number's exact value
90/// ([`Number::exact_digits`]) — what `mf2-fn-number` localizes.
91#[derive(Clone, Copy)]
92pub struct Digits<'n> {
93    dec: &'n Decimal,
94    lo: i16,
95    hi: i16,
96    sign: Sign,
97}
98
99impl Digits<'_> {
100    /// The sign (for display digits, after `signDisplay`).
101    pub fn sign(&self) -> Sign {
102        self.sign
103    }
104
105    /// The number of integer digits shown (≥ 1; for display digits, after
106    /// `minimumIntegerDigits`).
107    pub fn integer_count(&self) -> u16 {
108        u16::try_from(i32::from(self.hi) + 1).unwrap_or(1)
109    }
110
111    /// The number of fraction digits shown.
112    pub fn fraction_count(&self) -> u16 {
113        u16::try_from(-i32::from(self.lo)).unwrap_or(0)
114    }
115
116    /// The digit (0–9) worth 10^`magnitude`: the integer digits are at
117    /// magnitudes `0 .. integer_count`, the fraction digits at `-1 ..=
118    /// -fraction_count`; 0 anywhere else.
119    pub fn digit(&self, magnitude: i16) -> u8 {
120        if magnitude < self.lo || magnitude > self.hi {
121            return 0;
122        }
123        self.dec.digit_at(magnitude)
124    }
125
126    /// Whether every digit is 0.
127    pub fn is_zero(&self) -> bool {
128        self.dec.is_zero()
129    }
130
131    /// Writes the core's neutral output: ASCII digits, `.`, `-`/`+`, no
132    /// grouping.
133    pub fn write_neutral(&self, out: &mut dyn Sink) {
134        match self.sign {
135            Sign::Minus => out.push_str("-"),
136            Sign::Plus => out.push_str("+"),
137            Sign::None => {}
138        }
139        self.dec.write_digits(self.hi, self.lo, out);
140    }
141
142    /// The neutral output as sub-parts (`minusSign`, `plusSign`, `integer`,
143    /// `decimal`, `fraction`).
144    pub fn neutral_parts(&self, out: &mut dyn SubPartSink) {
145        write_parts(self.dec, self.sign, self.hi, self.lo, out);
146    }
147
148    /// The CLDR plural operands of these digits as shown (`1.0` has `v = 1`):
149    /// what a unit or currency name's plural form is chosen by
150    /// ([`crate::plural_category`]).
151    pub fn operands(&self) -> plural::Operands {
152        let mut b = OperandsBuilder::default();
153        let mut m = self.hi;
154        while m >= self.lo {
155            b.digit(self.dec.digit_at(m), m >= 0);
156            m -= 1;
157        }
158        b.finish()
159    }
160}
161
162impl Number {
163    /// A `number-literal`; `None` if `s` is not one (or is past the
164    /// implementation limits: 40 significant digits, exponent ±9999).
165    pub fn parse(number_literal: &str) -> Option<Number> {
166        Decimal::parse(number_literal.as_bytes())
167            .ok()
168            .map(Number::bare_of)
169    }
170
171    /// `n`.
172    pub fn from_i64(n: i64) -> Number {
173        let mut d = Decimal::from_u64(n.unsigned_abs());
174        d.set_negative(n < 0);
175        Number::bare_of(d)
176    }
177
178    /// The finite `x` (the shortest decimal that round-trips it, through
179    /// the host); `None` for NaN and infinities.
180    pub fn from_f64(x: f64, host: &dyn Host) -> Option<Number> {
181        if !x.is_finite() {
182            return None;
183        }
184        if x == 0.0 {
185            let mut d = Decimal::from_u64(0);
186            d.set_negative(x.is_sign_negative());
187            return Some(Number::bare_of(d));
188        }
189        let mut buf = [0u8; 32];
190        let text = host.f64_to_text(x, &mut buf)?;
191        Number::parse(text)
192    }
193
194    /// Whether the value is negative (`-0` included).
195    pub fn is_negative(&self) -> bool {
196        self.value.negative()
197    }
198
199    /// Whether the value is an integer.
200    pub fn is_integer(&self) -> bool {
201        self.value.is_integer()
202    }
203
204    /// The value as an `i64`, when it is an integer below 10^18 in magnitude.
205    pub fn to_i64(&self) -> Option<i64> {
206        self.value.to_i64()
207    }
208
209    /// Writes the exact value in plain neutral digits: `-1234.5`, `0.001`,
210    /// `-0` — no exponent, no rounding.
211    pub fn write_plain(&self, out: &mut dyn Sink) {
212        self.value.write_plain(out);
213    }
214
215    /// Resolves an expression under `spec`, as `:number` does
216    /// (`number.md`): the operand rules, the options of `spec`'s function
217    /// (any other option is ignored), inheritance from a number or measure
218    /// operand, the digit plan and rounding. `None`: a fallback value, the
219    /// reason reported through `errs`.
220    pub fn resolve(
221        spec: NumberSpec,
222        cx: &FnContext<'_>,
223        operand: Option<&Value<'_>>,
224        options: &Options<'_, '_>,
225        errs: &mut dyn ErrorSink,
226    ) -> Option<Number> {
227        resolve(spec, cx, operand, *options, errs)
228    }
229
230    /// The rounded digits to display; `None` for a number no handler
231    /// resolved — and, with feature `intl` on `wasm32-unknown-unknown`
232    /// ([`crate::INTL_NUMBERS`]), always: the host rounds and formats
233    /// ([`Number::format_by_host`]).
234    pub fn digits(&self) -> Option<Digits<'_>> {
235        self.resolved.as_ref().and_then(backend::digits)
236    }
237
238    /// The exact value's digits, unrounded — how an unannotated number
239    /// formats (`plans/03-runtime.md` §2.6): `-1234.5`, `0.001`, `-0`.
240    pub fn exact_digits(&self) -> Digits<'_> {
241        let d = &self.value;
242        Digits {
243            dec: d,
244            lo: d.low().min(0),
245            hi: d.high().max(0),
246            sign: if d.negative() {
247                Sign::Minus
248            } else {
249                Sign::None
250            },
251        }
252    }
253
254    /// The resolved `useGrouping`; `None` when it is not set (or the number
255    /// is bare).
256    pub fn grouping(&self) -> Option<Grouping> {
257        self.resolved.as_ref().and_then(|r| r.opts.use_grouping)
258    }
259
260    /// Whether a numeric handler resolved this number and it may select
261    /// (formatting.md, "Resolve Selectors").
262    pub fn is_selectable(&self) -> bool {
263        self.selectable()
264    }
265
266    /// Match(`self`, `key`) for numeric selectors (`number.md`, "Number
267    /// Selection"): exact numeric keys, then the plural or ordinal category
268    /// of the formatted digits; *Bad Variant Key* for any other key.
269    pub fn matches(&self, cx: &FnContext<'_>, key: &str, errs: &mut dyn ErrorSink) -> bool {
270        matches(self, cx, key, errs)
271    }
272
273    /// `BetterThan(key1, key2)` for two matching keys: an exact (numeric)
274    /// key beats a keyword.
275    pub fn better_than(key1: &str, key2: &str) -> bool {
276        better_than(key1, key2)
277    }
278
279    fn bare_of(value: Decimal) -> Number {
280        Number {
281            value,
282            resolved: None,
283        }
284    }
285
286    /// The value alone, without resolved options.
287    pub(crate) fn bare(&self) -> Number {
288        Number::bare_of(self.value.clone())
289    }
290
291    /// The plain value as sub-parts (`minusSign`, `integer`, `decimal`,
292    /// `fraction`).
293    pub(crate) fn plain_parts(&self, out: &mut dyn SubPartSink) {
294        self.exact_digits().neutral_parts(out);
295    }
296
297    /// Whether a numeric handler resolved this number and it may select.
298    pub(crate) fn selectable(&self) -> bool {
299        self.resolved.as_ref().is_some_and(|r| r.selectable)
300    }
301
302    /// Writes the display form in neutral symbols (a bare number: its
303    /// plain value).
304    pub(crate) fn write_display(&self, cx: &FnContext<'_>, out: &mut dyn Sink) {
305        backend::write_neutral(self, cx, out);
306    }
307
308    /// The display form as sub-parts.
309    pub(crate) fn display_parts(&self, cx: &FnContext<'_>, out: &mut dyn SubPartSink) {
310        backend::neutral_parts(self, cx, out);
311    }
312}
313
314/// Writes digits `hi..=lo` of `d` as Intl-style sub-parts, in chunks of at
315/// most 64 digits (a longer run arrives as several parts of one kind).
316fn write_parts(d: &Decimal, sign: Sign, hi: i16, lo: i16, out: &mut dyn SubPartSink) {
317    match sign {
318        Sign::Minus => out.sub_part("minusSign", "-"),
319        Sign::Plus => out.sub_part("plusSign", "+"),
320        Sign::None => {}
321    }
322    digits_part(d, hi, 0, "integer", out);
323    if lo < 0 {
324        out.sub_part("decimal", ".");
325        digits_part(d, -1, lo, "fraction", out);
326    }
327}
328
329fn digits_part(d: &Decimal, hi: i16, lo: i16, kind: &str, out: &mut dyn SubPartSink) {
330    let mut buf = [0u8; 64];
331    let mut n = 0;
332    let mut m = hi;
333    while m >= lo {
334        if let Some(b) = buf.get_mut(n) {
335            *b = b'0' + d.digit_at(m);
336            n += 1;
337        }
338        if n == buf.len() {
339            out.sub_part(kind, core::str::from_utf8(&buf).unwrap_or(""));
340            n = 0;
341        }
342        m -= 1;
343    }
344    if n > 0 {
345        out.sub_part(
346            kind,
347            core::str::from_utf8(buf.get(..n).unwrap_or(&[])).unwrap_or(""),
348        );
349    }
350}
351
352/// The error a `number-literal` operand gives, if any: *Bad Operand* for
353/// one that does not match the production, *Unsupported Operation* past the
354/// implementation limits.
355pub(crate) fn literal_error(s: &str) -> Option<FormatError> {
356    match Decimal::parse(s.as_bytes()) {
357        Ok(_) => None,
358        Err(ParseError::Syntax) => Some(FormatError::BadOperand),
359        Err(ParseError::Limit) => Some(FormatError::UnsupportedOperation),
360    }
361}
362
363/// Whether `key` matches the production `number-literal` (past our limits
364/// included: such a key is a number, it just never equals a value).
365fn is_number_literal(key: &str) -> bool {
366    !matches!(split_literal(key.as_bytes()), Err(ParseError::Syntax))
367}
368
369// ─────────────────────────────────────────────────────────── resolution ──
370
371/// How one numeric function resolves (`plans/03-runtime.md` §2.7): which
372/// options it reads, its fraction-digit defaults, whether it rounds to an
373/// integer, whether it selects, and the power of ten it applies. Closed
374/// world: a handler is a spec.
375#[derive(Clone, Copy, PartialEq, Eq, Debug)]
376pub struct NumberSpec {
377    /// Option-set bit (`NUM`, `INT`, `PCT`, `CUR`, `UNIT`); 0 = `:offset`.
378    bit: u8,
379    /// Fraction-digit defaults; `None` keeps the operand's (`:offset`).
380    frac: Option<FracDefaults>,
381    /// Round the value to an integer (`:integer`).
382    integer: bool,
383    /// Supports selection (`:currency` and `:unit` do not).
384    selectable: bool,
385    /// Power of ten applied when formatting and selecting (`:percent` = 2).
386    scale: i16,
387}
388
389impl NumberSpec {
390    /// `:number`: 0–3 fraction digits, selects.
391    pub const NUMBER: NumberSpec = NumberSpec {
392        bit: NUM,
393        frac: Some(FracDefaults { min: 0, max: 3 }),
394        integer: false,
395        selectable: true,
396        scale: 0,
397    };
398
399    /// `:integer`: the value rounded to an integer, selects.
400    pub const INTEGER: NumberSpec = NumberSpec {
401        bit: INT,
402        frac: Some(FracDefaults { min: 0, max: 0 }),
403        integer: true,
404        selectable: true,
405        scale: 0,
406    };
407
408    /// `:offset`: `add` / `subtract`, the operand's other options kept.
409    pub const OFFSET: NumberSpec = NumberSpec {
410        bit: 0,
411        frac: None,
412        integer: false,
413        selectable: true,
414        scale: 0,
415    };
416
417    /// `:percent`: the value × 100 when formatting and selecting, 0–0
418    /// fraction digits, plural selection only.
419    pub const PERCENT: NumberSpec = NumberSpec {
420        bit: PCT,
421        frac: Some(FracDefaults { min: 0, max: 0 }),
422        integer: false,
423        selectable: true,
424        scale: 2,
425    };
426
427    /// `:unit`: 0–3 fraction digits, not selectable.
428    pub const UNIT: NumberSpec = NumberSpec {
429        bit: UNIT,
430        frac: Some(FracDefaults { min: 0, max: 3 }),
431        integer: false,
432        selectable: false,
433        scale: 0,
434    };
435
436    /// `:currency` whose currency shows `fraction_digits` fraction digits
437    /// (`fractionDigits`, or the currency's own for `auto`); not
438    /// selectable.
439    pub const fn currency(fraction_digits: u8) -> NumberSpec {
440        NumberSpec {
441            bit: CUR,
442            frac: Some(FracDefaults {
443                min: fraction_digits,
444                max: fraction_digits,
445            }),
446            integer: false,
447            selectable: false,
448            scale: 0,
449        }
450    }
451}
452
453/// Resolves a numeric expression; `None` = a fallback value (reported).
454pub(crate) fn resolve(
455    spec: NumberSpec,
456    cx: &FnContext<'_>,
457    operand: Option<&Value<'_>>,
458    options: Options<'_, '_>,
459    errs: &mut dyn ErrorSink,
460) -> Option<Number> {
461    let is_offset = spec.bit == 0;
462    let Some(operand) = operand else {
463        errs.error(FormatError::BadOperand);
464        return None;
465    };
466    let resolved_operand = match operand {
467        Value::Number(n) | Value::Measure(Measure { number: n, .. }) => {
468            n.resolved.as_ref().map(|r| (&n.value, r))
469        }
470        _ => None,
471    };
472    let (mut value, mut o, inherited, mut selectable) = match resolved_operand {
473        Some((value, r)) => {
474            let mut o = r.opts;
475            let mut selectable = r.selectable;
476            if o.select.is_some() {
477                // `select` set by an operand: Bad Option and no selection for
478                // the selecting functions (number.md, "Number Selection");
479                // `:offset` passes it on.
480                if spec.bit & (NUM | INT) != 0 {
481                    errs.error(FormatError::BadOption);
482                    selectable = false;
483                }
484                if !is_offset {
485                    o.select = None;
486                }
487            }
488            if spec.bit == INT {
489                o.min_frac = None;
490                o.max_frac = None;
491                o.min_sig = None;
492            }
493            if spec.bit == PCT {
494                o.min_int = None;
495                o.rounding_increment = None;
496            }
497            if spec.bit == CUR {
498                // `fractionDigits` decides (NumberSpec::currency).
499                o.min_frac = None;
500                o.max_frac = None;
501            }
502            (value.clone(), o, Some((r.frac, r.scale)), selectable)
503        }
504        None => match numeric_operand(operand, cx.host()) {
505            Ok(d) => (d, NumOpts::default(), None, true),
506            Err(e) => {
507                errs.error(e);
508                return None;
509            }
510        },
511    };
512    let mut offset: Option<(bool, u8)> = None;
513    let mut offset_bad = false;
514    for (name, v) in options.iter() {
515        if is_offset {
516            let sub = match name {
517                "add" => false,
518                "subtract" => true,
519                _ => continue, // other options are ignored
520            };
521            match (digit_size(v.value), offset) {
522                (Some(d), None) => offset = Some((sub, d)),
523                _ => offset_bad = true,
524            }
525            continue;
526        }
527        let Some(kind) = OPTIONS
528            .iter()
529            .find(|(n, mask, _)| *n == name && mask & spec.bit != 0)
530            .map(|&(_, _, k)| k)
531        else {
532            continue; // not an option of this function: ignored
533        };
534        if kind == Kind::Select {
535            // MUST be a literal; otherwise Bad Option and no selection.
536            if v.literal {
537                match v.value.as_str().and_then(select_named) {
538                    Some(x) => o.select = Some(x),
539                    None => errs.error(FormatError::BadOption),
540                }
541            } else {
542                errs.error(FormatError::BadOption);
543                selectable = false;
544            }
545            continue;
546        }
547        if !apply(&mut o, kind, v.value) {
548            errs.error(FormatError::BadOption);
549        }
550    }
551    if spec.integer {
552        // The resolved value of `:integer` is the integer value.
553        let mode = o.rounding_mode.unwrap_or(RoundingMode::HalfExpand);
554        backend::round_to_integer(&mut value, mode, cx);
555    }
556    let (frac, scale) = match (spec.frac, inherited) {
557        (Some(f), _) => (f, spec.scale),
558        (None, Some(inherited)) => inherited,
559        (None, None) => (FracDefaults { min: 0, max: 3 }, 0),
560    };
561    if is_offset {
562        let Some((sub, d)) = offset.filter(|_| !offset_bad) else {
563            errs.error(FormatError::BadOption);
564            return None;
565        };
566        let mut delta = Decimal::from_u64(u64::from(d));
567        delta.set_negative(sub);
568        let Some(sum) = value.add(&delta) else {
569            // An implementation limit (number.md allows Unsupported Operation).
570            errs.error(FormatError::UnsupportedOperation);
571            return None;
572        };
573        value = sum;
574    }
575    backend::limit_digit_sizes(&mut o, errs);
576    let plan = digit_plan(&o, frac, errs);
577    let shown = backend::shown(
578        &value,
579        scale,
580        &plan,
581        o.sign_display.unwrap_or(SignDisplay::Auto),
582    );
583    backend::check_host(cx, errs);
584    Some(Number {
585        value,
586        resolved: Some(Resolved {
587            opts: o,
588            frac,
589            scale,
590            selectable: selectable && spec.selectable,
591            shown,
592        }),
593    })
594}
595
596/// The numeric value of an operand that is not a resolved number.
597fn numeric_operand(v: &Value<'_>, host: &dyn Host) -> Result<Decimal, FormatError> {
598    match v {
599        Value::Str(s) | Value::Decimal(s) => Decimal::parse(s.as_bytes()).map_err(|e| match e {
600            ParseError::Syntax => FormatError::BadOperand,
601            ParseError::Limit => FormatError::UnsupportedOperation,
602        }),
603        Value::Int(n) => {
604            let mut d = Decimal::from_u64(n.unsigned_abs());
605            d.set_negative(*n < 0);
606            Ok(d)
607        }
608        _ => v
609            .to_number(host)
610            .map(|n| n.value)
611            .ok_or(FormatError::BadOperand),
612    }
613}
614
615// ────────────────────────────────────────────────────────── selection ──
616
617/// Compares written bytes with a key.
618struct Cmp<'k> {
619    key: &'k [u8],
620    pos: usize,
621    ok: bool,
622}
623
624impl Sink for Cmp<'_> {
625    fn push_str(&mut self, s: &str) {
626        let end = self.pos + s.len();
627        self.ok &= self.key.get(self.pos..end) == Some(s.as_bytes());
628        self.pos = end;
629    }
630}
631
632impl Cmp<'_> {
633    fn done(&self) -> bool {
634        self.ok && self.pos == self.key.len()
635    }
636}
637
638/// Whether `key` equals the text `write` produces.
639pub(crate) fn equals(key: &str, write: impl FnOnce(&mut dyn Sink)) -> bool {
640    let mut c = Cmp {
641        key: key.as_bytes(),
642        pos: 0,
643        ok: true,
644    };
645    write(&mut c);
646    c.done()
647}
648
649/// The exact-match serialization (number.md): the integer form when the
650/// value is an integer and none of the min-fraction, min-integer, min/max-
651/// significant options is set; otherwise (implementation-defined) the
652/// display form, in neutral symbols.
653fn exact_matches(n: &Number, r: &Resolved, cx: &FnContext<'_>, key: &str) -> bool {
654    let o = &r.opts;
655    let plain =
656        o.min_frac.is_none() && o.min_int.is_none() && o.min_sig.is_none() && o.max_sig.is_none();
657    let mut value = n.value.clone();
658    if r.scale != 0 {
659        value.shift(r.scale);
660    }
661    if plain && value.is_integer() {
662        if value.is_zero() {
663            value.set_negative(false);
664        }
665        equals(key, |s| value.write_plain(s))
666    } else {
667        equals(key, |s| backend::write_selected(n, r, cx, s))
668    }
669}
670
671/// Match(`n`, `key`) for numeric selectors (number.md, "Number Selection").
672pub(crate) fn matches(n: &Number, cx: &FnContext<'_>, key: &str, errs: &mut dyn ErrorSink) -> bool {
673    let Some(r) = &n.resolved else {
674        return false;
675    };
676    if is_number_literal(key) {
677        return exact_matches(n, r, cx, key);
678    }
679    let Some(keyword) = Category::from_keyword(key) else {
680        errs.error(FormatError::BadVariantKey);
681        return false;
682    };
683    if r.opts.select == Some(Select::Exact) {
684        return false;
685    }
686    backend::category(n, r, cx) == keyword
687}
688
689/// `BetterThan(n, key1, key2)` for two matching keys: an exact
690/// (numeric) key beats a keyword.
691pub(crate) fn better_than(key1: &str, key2: &str) -> bool {
692    is_number_literal(key1) && !is_number_literal(key2)
693}
694
695#[cfg(test)]
696mod tests;