Skip to main content

mf2_runtime/
number.rs

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