Skip to main content

mf2_fn_number/
measure.rs

1//! `:currency` and `:unit` (`functions/number.md`; `plans/03-runtime.md`
2//! §2.7, §5): the runtime's numeric core resolves the number
3//! (`NumberSpec::currency`, `NumberSpec::UNIT`), this module the currency or
4//! unit and the options only these functions have, into a `Measure`; the
5//! catalog's `currency.data` / `unit.data` entries (02 §4.6–§4.7) and the
6//! number patterns write it.
7//!
8//! Decisions where the spec leaves a choice (tested in
9//! `tests/measure.rs` and `conformance/extra/functions/unit.json`):
10//!
11//! * a `currency` / `unit` value that is not well-formed (`3ALPHA`; a
12//!   lower-case `[a-z0-9]` identifier with `-` between parts) is *Bad
13//!   Option* and ignored — then, with no currency or unit, *Bad Operand*;
14//! * `currency` on a value that has one is *Bad Option* (number.md: MUST
15//!   NOT override); `unit` on a value with another unit likewise (number.md:
16//!   MUST NOT substitute a unit without converting); the same unit again is
17//!   no error;
18//! * `usage` is *Unsupported Operation*: no conversion, the unit stays;
19//! * a currency the catalog has no data for formats with its code as the
20//!   symbol and name and CLDR's default fraction digits (UTS #35's
21//!   fallback); a unit it has no data for (and cannot compose as
22//!   `X-per-Y` from two it has) is *Unsupported Operation* when formatted;
23//! * neither selects (*Bad Selector*); the part kind is `number`.
24
25use mf2_catalog::Catalog;
26use mf2_catalog::currency::{Currencies, Currency};
27use mf2_catalog::format::locale_key;
28use mf2_catalog::number::{Patterns, Style, Symbols, Template, TemplatePart};
29use mf2_catalog::unit::{Unit, Units, Width};
30use mf2_runtime::{
31    Category, Digits, Dir, ErrorSink, FnContext, FormatError, Function, INTL_NUMBERS, Measure,
32    MeasureUnit, Number, NumberOut, NumberSpec, Options, Sink, SubPartSink, Value, plural_category,
33};
34
35use crate::intl;
36use crate::localize::{self, Out, Seps, Symbol, ends_with_currency, starts_with_currency};
37
38// ─────────────────────────────────────────────────────── the flags ──
39//
40// `Measure::flags`, this crate's encoding of the options the runtime does
41// not know (inherited by a later `:currency` / `:unit` over the value):
42// bits 0–2 `currencyDisplay`, 3–4 `currencySign`, 5–11 `fractionDigits`
43// (0 unset, 1 `auto`, 2 + n), 12–13 `unitDisplay`. 0 = unset everywhere.
44
45const DISPLAY_SHIFT: u32 = 0;
46const SIGN_SHIFT: u32 = 3;
47const DIGITS_SHIFT: u32 = 5;
48const WIDTH_SHIFT: u32 = 12;
49
50/// `currencyDisplay`.
51#[derive(Clone, Copy, PartialEq, Eq, Debug)]
52pub(crate) enum Display {
53    NarrowSymbol = 1,
54    Symbol = 2,
55    Name = 3,
56    Code = 4,
57    Never = 5,
58}
59
60/// The keyword options' values and their codes in the flags.
61const DISPLAYS: [(&str, u32); 5] = [
62    ("narrowSymbol", Display::NarrowSymbol as u32),
63    ("symbol", Display::Symbol as u32),
64    ("name", Display::Name as u32),
65    ("code", Display::Code as u32),
66    ("never", Display::Never as u32),
67];
68const SIGNS: [(&str, u32); 2] = [("standard", 1), ("accounting", 2)];
69const WIDTHS: [(&str, u32); 3] = [("short", 1), ("narrow", 2), ("long", 3)];
70
71fn field(flags: u32, shift: u32, bits: u32) -> u32 {
72    (flags >> shift) & ((1 << bits) - 1)
73}
74
75fn set(flags: &mut u32, shift: u32, bits: u32, v: u32) {
76    let mask = ((1 << bits) - 1) << shift;
77    *flags = (*flags & !mask) | ((v << shift) & mask);
78}
79
80pub(crate) fn display_of(flags: u32) -> Display {
81    match field(flags, DISPLAY_SHIFT, 3) {
82        1 => Display::NarrowSymbol,
83        3 => Display::Name,
84        4 => Display::Code,
85        5 => Display::Never,
86        _ => Display::Symbol,
87    }
88}
89
90pub(crate) fn accounting(flags: u32) -> bool {
91    field(flags, SIGN_SHIFT, 2) == 2
92}
93
94/// `fractionDigits` unset or `auto`: the currency's own digits.
95pub(crate) fn own_digits(flags: u32) -> bool {
96    field(flags, DIGITS_SHIFT, 7) < 2
97}
98
99pub(crate) fn width_of(flags: u32) -> Width {
100    match field(flags, WIDTH_SHIFT, 2) {
101        2 => Width::Narrow,
102        3 => Width::Long,
103        _ => Width::Short,
104    }
105}
106
107// ─────────────────────────────────────────────────────── helpers ──
108
109/// A string value's text with the value's lifetime (literals, string
110/// arguments, application values' `as_str`).
111fn str_of<'a>(v: &Value<'a>) -> Option<&'a str> {
112    match v {
113        Value::Str(s) | Value::Decimal(s) => Some(s),
114        Value::Custom(c) => c.as_str(),
115        _ => None,
116    }
117}
118
119/// A well-formed currency code (`currency_code = 3ALPHA`, case-insensitive),
120/// upper-cased.
121fn currency_code(s: &str) -> Option<[u8; 3]> {
122    match *s.as_bytes() {
123        [a, b, c]
124            if a.is_ascii_alphabetic() && b.is_ascii_alphabetic() && c.is_ascii_alphabetic() =>
125        {
126            Some([
127                a.to_ascii_uppercase(),
128                b.to_ascii_uppercase(),
129                c.to_ascii_uppercase(),
130            ])
131        }
132        _ => None,
133    }
134}
135
136/// A well-formed unit identifier, as CLDR writes them: parts of lower-case
137/// ASCII letters and digits joined by `-` (`kilometer-per-hour`,
138/// `liter-per-100-kilometer`), at most 64 bytes.
139fn is_unit_id(s: &str) -> bool {
140    let b = s.as_bytes();
141    if b.is_empty() || b.len() > 64 || b.first() == Some(&b'-') || b.last() == Some(&b'-') {
142        return false;
143    }
144    let mut prev_dash = false;
145    for &c in b {
146        let dash = c == b'-';
147        if !(dash || c.is_ascii_lowercase() || c.is_ascii_digit()) || (dash && prev_dash) {
148            return false;
149        }
150        prev_dash = dash;
151    }
152    true
153}
154
155/// The plural category of `d` as shown, by the catalog's cardinal rules.
156fn category(catalog: &Catalog, d: &Digits<'_>) -> Category {
157    let rules = catalog
158        .locale_entry(locale_key::PLURAL_CARDINAL)
159        .unwrap_or(&[]);
160    plural_category(rules, &d.operands())
161}
162
163/// The operand's currency or unit and flags, when it is a measure (a
164/// previous `:currency` / `:unit`, or an application value); its number is
165/// `Number::resolve`'s.
166fn operand_measure<'a>(operand: Option<&Value<'a>>) -> Option<(MeasureUnit<'a>, u32)> {
167    match operand? {
168        Value::Measure(m) => Some((m.unit, m.flags)),
169        Value::Custom(c) => c.as_measure().map(|m| (m.unit, m.flags)),
170        _ => None,
171    }
172}
173
174/// Sets the `bits`-wide field at `shift` to the code of the keyword `text`
175/// in `table`; *Bad Option* when it is not one.
176fn keyword(
177    text: Option<&str>,
178    table: &[(&str, u32)],
179    shift: u32,
180    bits: u32,
181    flags: &mut u32,
182    errs: &mut dyn ErrorSink,
183) {
184    match text.and_then(|t| table.iter().find(|(k, _)| *k == t)) {
185        Some(&(_, code)) => set(flags, shift, bits, code),
186        None => errs.error(FormatError::BadOption),
187    }
188}
189
190/// `:currency` (`unit` false) and `:unit` (`unit` true): the currency or
191/// unit — the operand's, or the option's — and the options only these
192/// functions have, then the number (`Number::resolve`).
193fn resolve_measure<'a>(
194    unit: bool,
195    cx: &FnContext<'_>,
196    operand: Option<&Value<'a>>,
197    options: Options<'_, 'a>,
198    errs: &mut dyn ErrorSink,
199) -> Option<Value<'a>> {
200    let inherited =
201        operand_measure(operand).filter(|(u, _)| matches!(u, MeasureUnit::Unit(_)) == unit);
202    let mut what = inherited.map(|(u, _)| u);
203    let mut flags = inherited.map_or(0, |(_, f)| f);
204    for (name, v) in options.iter() {
205        let text = str_of(v.value);
206        match (unit, name) {
207            (false, "currency") => match text.and_then(currency_code) {
208                // MUST NOT override the currency of a value that has one.
209                Some(_) if inherited.is_some() => errs.error(FormatError::BadOption),
210                Some(c) => what = Some(MeasureUnit::Currency(c)),
211                None => errs.error(FormatError::BadOption),
212            },
213            (true, "unit") => match text.filter(|u| is_unit_id(u)).map(MeasureUnit::Unit) {
214                // No other unit without converting (number.md).
215                Some(u) if inherited.is_some_and(|(i, _)| i != u) => {
216                    errs.error(FormatError::BadOption);
217                }
218                Some(u) => what = Some(u),
219                None => errs.error(FormatError::BadOption),
220            },
221            (false, "currencyDisplay") => {
222                keyword(text, &DISPLAYS, DISPLAY_SHIFT, 3, &mut flags, errs);
223            }
224            (false, "currencySign") => keyword(text, &SIGNS, SIGN_SHIFT, 2, &mut flags, errs),
225            (true, "unitDisplay") => keyword(text, &WIDTHS, WIDTH_SHIFT, 2, &mut flags, errs),
226            (false, "fractionDigits") => {
227                if text == Some("auto") {
228                    set(&mut flags, DIGITS_SHIFT, 7, 1);
229                } else if let Some(n) = v.value.digit_size() {
230                    set(&mut flags, DIGITS_SHIFT, 7, 2 + u32::from(n));
231                } else {
232                    errs.error(FormatError::BadOption);
233                }
234            }
235            // Conversion is optional and not implemented (number.md,
236            // "Unit Conversion"): the value keeps its unit.
237            (true, "usage") => errs.error(FormatError::UnsupportedOperation),
238            _ => {}
239        }
240    }
241    let Some(what) = what else {
242        // A numeric operand without a currency or unit (number.md).
243        errs.error(FormatError::BadOperand);
244        return None;
245    };
246    let spec = match what {
247        MeasureUnit::Currency(code) => NumberSpec::currency(match field(flags, DIGITS_SHIFT, 7) {
248            n if n >= 2 => u8::try_from(n - 2).unwrap_or(2),
249            // `intl`: the currency's own digits are the formatter's
250            // (`NumberStyle::Currency::own_digits`); the catalog's currency
251            // data is not read.
252            _ if INTL_NUMBERS => 2,
253            _ => auto_digits(cx.catalog(), code),
254        }),
255        MeasureUnit::Unit(_) => NumberSpec::UNIT,
256    };
257    let number = Number::resolve(spec, cx, operand, &options, errs)?;
258    Some(Value::Measure(Measure::new(number, what, flags)))
259}
260
261// ────────────────────────────────────────────────────── :currency ──
262
263/// `:currency` (`functions/number.md`).
264#[derive(Clone, Copy, Default, Debug)]
265pub struct CurrencyFunction;
266
267/// `:currency`.
268pub static CURRENCY: CurrencyFunction = CurrencyFunction;
269
270impl Function for CurrencyFunction {
271    fn resolve<'a>(
272        &self,
273        cx: &FnContext<'_>,
274        operand: Option<&Value<'a>>,
275        options: &Options<'_, 'a>,
276        errs: &mut dyn ErrorSink,
277    ) -> Option<Value<'a>> {
278        resolve_measure(false, cx, operand, *options, errs)
279    }
280
281    fn formattable(&self, _cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
282        match value {
283            Value::Measure(m) if matches!(m.unit, MeasureUnit::Currency(_)) => Ok(()),
284            _ => Err(FormatError::MessageFunctionError),
285        }
286    }
287
288    fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
289        if let Value::Measure(m) = value {
290            if INTL_NUMBERS {
291                return intl::measure(cx, m, NumberOut::Text(out));
292            }
293            write_currency(cx.catalog(), m, &mut Out::Text(out));
294        }
295    }
296
297    fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
298        if let Value::Measure(m) = value {
299            if INTL_NUMBERS {
300                return intl::measure(cx, m, NumberOut::Parts(out));
301            }
302            write_currency(cx.catalog(), m, &mut Out::Parts(out));
303        }
304    }
305
306    fn part_kind(&self) -> &'static str {
307        "number"
308    }
309
310    fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
311        Dir::Ltr
312    }
313}
314
315/// The currency's own fraction digits (`fractionDigits=auto`): from the
316/// catalog's data, else CLDR's default, else 2.
317fn auto_digits(catalog: &Catalog, code: [u8; 3]) -> u8 {
318    Currencies::of(catalog).map_or(2, |c| {
319        c.get(code)
320            .map_or(c.default_digits(), |x| x.fraction_digits())
321    })
322}
323
324fn write_currency(catalog: &Catalog, m: &Measure<'_>, out: &mut Out<'_>) {
325    let Some(d) = m.number.digits() else {
326        return;
327    };
328    let code = m.unit.as_str();
329    let Some(sym) = Symbols::of(catalog) else {
330        // No number data: the core's digits and the code.
331        localize::neutral(&d, out);
332        out.put("literal", "\u{a0}");
333        out.put("currency", code);
334        return;
335    };
336    let bytes = match m.unit {
337        MeasureUnit::Currency(c) => c,
338        MeasureUnit::Unit(_) => return,
339    };
340    let data = Currencies::of(catalog);
341    let cur: Option<Currency<'_>> = data.and_then(|c| c.get(bytes));
342    let display = display_of(m.flags);
343    let mut seps = Seps::of(&sym);
344    if let Some(c) = cur {
345        if let Some(dec) = c.decimal() {
346            seps.decimal = dec;
347        }
348        if let Some(g) = c.group() {
349            seps.group = g;
350        }
351    }
352    if display == Display::Name {
353        return write_currency_name(catalog, &sym, seps, cur, code, &d, m, out);
354    }
355    // The symbol text and whether its ends are letter-like.
356    let (text, first, last) = match (display, cur) {
357        (Display::Never, _) => ("", false, false),
358        (Display::Code, _) | (_, None) => (code, true, true),
359        (Display::NarrowSymbol, Some(c)) => match c.narrow_symbol() {
360            Some(n) => (n, c.narrow_edges().first, c.narrow_edges().last),
361            None => (c.symbol(), c.symbol_edges().first, c.symbol_edges().last),
362        },
363        (_, Some(c)) => (c.symbol(), c.symbol_edges().first, c.symbol_edges().last),
364    };
365    let accounting = accounting(m.flags);
366    let (base, alpha) = match (accounting, display == Display::Never) {
367        (false, false) => (Style::Currency, Some(Style::CurrencyAlpha)),
368        (true, false) => (Style::Accounting, Some(Style::AccountingAlpha)),
369        (false, true) => (Style::CurrencyNoSymbol, None),
370        (true, true) => (Style::AccountingNoSymbol, None),
371    };
372    let patterns = Patterns::of(catalog);
373    // The currency's own standard pattern replaces the locale's.
374    let own = cur
375        .and_then(|c| c.pattern())
376        .filter(|_| base == Style::Currency);
377    let mut pattern = own.or_else(|| patterns.and_then(|p| p.resolve(base)));
378    // Where a letter-like end of the symbol touches the digits, CLDR's
379    // currency spacing puts a U+00A0 between them — or, when the locale has
380    // one, the `…alphaNextToNumber` pattern places the space itself. No
381    // currency pattern in the catalog: the symbol goes before the number.
382    let (mut before, mut after) = pattern.map_or((last, false), |p| {
383        let s = p.signed(localize::shown(&d));
384        (
385            last && ends_with_currency(s.prefix),
386            first && starts_with_currency(s.suffix),
387        )
388    });
389    if own.is_none()
390        && (before || after)
391        && let Some(a) = alpha.and_then(|a| patterns.and_then(|p| p.get(a)))
392    {
393        pattern = Some(a);
394        (before, after) = (false, false);
395    }
396    let sizes = pattern.map_or(sym.grouping(), |p| p.grouping());
397    let symbol = Symbol {
398        text,
399        before,
400        after,
401    };
402    if pattern.is_none() {
403        out.put("currency", text);
404        if before {
405            out.put("literal", "\u{a0}");
406        }
407    }
408    localize::write_number(
409        &sym,
410        pattern,
411        Some(symbol),
412        seps,
413        sizes,
414        &d,
415        m.number.grouping(),
416        out,
417    );
418}
419
420/// `currencyDisplay=name`: the locale's name pattern (`{0} {1}`) with the
421/// number in the decimal pattern and the display name for its plural
422/// category.
423#[allow(clippy::too_many_arguments)]
424fn write_currency_name(
425    catalog: &Catalog,
426    sym: &Symbols<'_>,
427    seps: Seps<'_>,
428    cur: Option<Currency<'_>>,
429    code: &str,
430    d: &Digits<'_>,
431    m: &Measure<'_>,
432    out: &mut Out<'_>,
433) {
434    let cat = category(catalog, d) as u8;
435    let name = cur.and_then(|c| c.name_for(cat)).unwrap_or(code);
436    let template = Currencies::of(catalog).and_then(|c| c.name_pattern(cat));
437    let mut number = |out: &mut Out<'_>| {
438        localize::write_number(
439            sym,
440            None,
441            None,
442            seps,
443            sym.grouping(),
444            d,
445            m.number.grouping(),
446            out,
447        );
448    };
449    if let Some(t) = template {
450        fill(t, out, &mut number, &mut |out| out.put("currency", name));
451    } else {
452        number(out);
453        out.put("literal", " ");
454        out.put("currency", name);
455    }
456}
457
458/// Writes `t` with `arg0` for `{0}` and `arg1` for `{1}`; a text's blank
459/// ends are `literal` parts, the rest `unit` (Intl's parts for unit and
460/// currency-name patterns).
461fn fill(
462    t: Template<'_>,
463    out: &mut Out<'_>,
464    arg0: &mut dyn FnMut(&mut Out<'_>),
465    arg1: &mut dyn FnMut(&mut Out<'_>),
466) {
467    for p in t.parts() {
468        match p {
469            TemplatePart::Arg0 => arg0(out),
470            TemplatePart::Arg1 => arg1(out),
471            TemplatePart::Text(s) => {
472                let (lead, text, trail) = split_blank(s);
473                out.put("literal", lead);
474                out.put("unit", text);
475                out.put("literal", trail);
476            }
477        }
478    }
479}
480
481/// The spaces of CLDR's unit and name patterns: U+0020, U+00A0, U+2009,
482/// U+202F.
483const BLANKS: [&str; 4] = [" ", "\u{a0}", "\u{2009}", "\u{202f}"];
484
485/// `text` as its leading blanks, the text between, and its trailing blanks.
486fn split_blank(text: &str) -> (&str, &str, &str) {
487    let mut rest = text;
488    while let Some(r) = BLANKS.iter().find_map(|b| rest.strip_prefix(b)) {
489        rest = r;
490    }
491    let (head, mut mid) = text
492        .split_at_checked(text.len() - rest.len())
493        .unwrap_or(("", text));
494    while let Some(r) = BLANKS.iter().find_map(|b| mid.strip_suffix(b)) {
495        mid = r;
496    }
497    let tail = rest.get(mid.len()..).unwrap_or("");
498    (head, mid, tail)
499}
500
501// ────────────────────────────────────────────────────────── :unit ──
502
503/// `:unit` (`functions/number.md`, Draft).
504#[derive(Clone, Copy, Default, Debug)]
505pub struct UnitFunction;
506
507/// `:unit`.
508pub static UNIT: UnitFunction = UnitFunction;
509
510impl Function for UnitFunction {
511    fn resolve<'a>(
512        &self,
513        cx: &FnContext<'_>,
514        operand: Option<&Value<'a>>,
515        options: &Options<'_, 'a>,
516        errs: &mut dyn ErrorSink,
517    ) -> Option<Value<'a>> {
518        resolve_measure(true, cx, operand, *options, errs)
519    }
520
521    fn formattable(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
522        match value {
523            Value::Measure(m) => match m.unit {
524                MeasureUnit::Unit(_) if INTL_NUMBERS => intl::unit_formattable(cx, m),
525                MeasureUnit::Unit(id) => {
526                    let units = Units::of(cx.catalog());
527                    if Symbols::of(cx.catalog()).is_none()
528                        || units.is_some_and(|u| resolve_unit(&u, id).is_some())
529                    {
530                        Ok(())
531                    } else {
532                        Err(FormatError::UnsupportedOperation)
533                    }
534                }
535                MeasureUnit::Currency(_) => Err(FormatError::MessageFunctionError),
536            },
537            _ => Err(FormatError::MessageFunctionError),
538        }
539    }
540
541    fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
542        if let Value::Measure(m) = value {
543            if INTL_NUMBERS {
544                return intl::measure(cx, m, NumberOut::Text(out));
545            }
546            write_unit(cx.catalog(), m, &mut Out::Text(out));
547        }
548    }
549
550    fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
551        if let Value::Measure(m) = value {
552            if INTL_NUMBERS {
553                return intl::measure(cx, m, NumberOut::Parts(out));
554            }
555            write_unit(cx.catalog(), m, &mut Out::Parts(out));
556        }
557    }
558
559    fn part_kind(&self) -> &'static str {
560        "number"
561    }
562
563    fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
564        Dir::Ltr
565    }
566}
567
568/// How a unit is written: one CLDR unit, or `X-per-Y` composed from two.
569enum Resolved<'u> {
570    One(Unit<'u>),
571    Per(Unit<'u>, Unit<'u>),
572}
573
574/// The unit `id` in `units`: itself, or the first split `X-per-Y` whose
575/// parts both are there (as `mf2_locale_data::unit::composition` slices it).
576fn resolve_unit<'u>(units: &Units<'u>, id: &str) -> Option<Resolved<'u>> {
577    if let Some(u) = units.get(id) {
578        return Some(Resolved::One(u));
579    }
580    let b = id.as_bytes();
581    let mut at = 0;
582    while at + 5 <= b.len() {
583        if b.get(at..at + 5) == Some(b"-per-") {
584            let (x, y) = (id.get(..at)?, id.get(at + 5..)?);
585            if let (Some(x), Some(y)) = (units.get(x), units.get(y)) {
586                return Some(Resolved::Per(x, y));
587            }
588        }
589        at += 1;
590    }
591    None
592}
593
594/// The width to write: the one asked for if the catalog carries it, else
595/// the first it has patterns in.
596fn width_for(u: &Unit<'_>, want: Width) -> Width {
597    if u.has_patterns(want) {
598        return want;
599    }
600    [Width::Short, Width::Long, Width::Narrow]
601        .into_iter()
602        .find(|w| u.has_patterns(*w))
603        .unwrap_or(want)
604}
605
606fn write_unit(catalog: &Catalog, measure: &Measure<'_>, out: &mut Out<'_>) {
607    let Some(digits) = measure.number.digits() else {
608        return;
609    };
610    let MeasureUnit::Unit(id) = measure.unit else {
611        return;
612    };
613    let (Some(sym), Some(units)) = (Symbols::of(catalog), Units::of(catalog)) else {
614        // No data: the core's digits and the identifier.
615        localize::neutral(&digits, out);
616        out.put("literal", " ");
617        out.put("unit", id);
618        return;
619    };
620    // Not formattable (`formattable` said so): nothing.
621    let (unit, per) = match resolve_unit(&units, id) {
622        Some(Resolved::One(u)) => (u, None),
623        Some(Resolved::Per(x, y)) => (x, Some(y)),
624        None => return,
625    };
626    let seps = Seps::of(&sym);
627    let grouping = measure.number.grouping();
628    let cat = category(catalog, &digits) as u8;
629    let mut number = |out: &mut Out<'_>| {
630        localize::write_number(
631            &sym,
632            None,
633            None,
634            seps,
635            sym.grouping(),
636            &digits,
637            grouping,
638            out,
639        );
640    };
641    let w = width_for(&unit, width_of(measure.flags));
642    let mut numerator = |out: &mut Out<'_>| match unit.pattern(w, cat) {
643        Some(t) => fill(t, out, &mut number, &mut |_| {}),
644        None => number(out),
645    };
646    let Some(den) = per else {
647        return numerator(out);
648    };
649    if let Some(p) = den.per_unit_pattern(w) {
650        return fill(p, out, &mut numerator, &mut |_| {});
651    }
652    // Else the locale's `per` pattern (`{0}/{1}`) with the denominator's
653    // singular form without its number.
654    let Some(p) = units.per_pattern(w) else {
655        return numerator(out);
656    };
657    let denominator = den.pattern(w, Category::One as u8);
658    fill(p, out, &mut numerator, &mut |out| {
659        for part in denominator.iter().flat_map(Template::parts) {
660            if let TemplatePart::Text(s) = part {
661                out.put("unit", split_blank(s).1);
662            }
663        }
664    });
665}