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