Skip to main content

mf2_runtime/number/
options.rs

1//! The numeric options (`number.md`) as resolved values, and ECMA-402's
2//! `SetNumberFormatDigitOptions` (MF2 took its option names and meanings from
3//! `Intl.NumberFormat`): where ECMA-402 throws, this reports *Bad Option* and
4//! ignores the offending option. Ported from P0.5.
5
6use super::decimal::{Increment, RoundingMode};
7use crate::error::FormatError;
8use crate::sink::ErrorSink;
9use crate::value::Value;
10
11/// `select`.
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
13pub(crate) enum Select {
14    Plural,
15    Ordinal,
16    Exact,
17}
18
19/// `signDisplay`.
20#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
21#[non_exhaustive]
22pub enum SignDisplay {
23    /// `auto` (the default): a minus sign for negative numbers, `-0` included.
24    Auto,
25    /// `always`.
26    Always,
27    /// `exceptZero`: a sign for nonzero numbers only.
28    ExceptZero,
29    /// `negative`: a minus sign for negative numbers, not `-0`.
30    Negative,
31    /// `never`.
32    Never,
33}
34
35/// `useGrouping`. Core output never groups; `mf2-fn-number` honours it.
36#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
37#[non_exhaustive]
38pub enum Grouping {
39    /// `auto` (the default): the locale's grouping, with its minimum
40    /// grouping digits.
41    Auto,
42    /// `always`: grouping even below the locale's minimum grouping digits.
43    Always,
44    /// `never`.
45    Never,
46    /// `min2`: grouping only with at least two digits in the highest group.
47    Min2,
48}
49
50/// `roundingPriority`.
51#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
52#[non_exhaustive]
53pub enum RoundingPriority {
54    /// `auto` (the default): significant digits when set, else fraction digits.
55    Auto,
56    /// `morePrecision`: of the two roundings, the one keeping more digits.
57    MorePrecision,
58    /// `lessPrecision`: the one keeping fewer.
59    LessPrecision,
60}
61
62/// The resolved options of a numeric value; `None` = not set. They travel
63/// with the value into a function that takes it as its operand.
64#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
65pub(crate) struct NumOpts {
66    pub(crate) select: Option<Select>,
67    pub(crate) sign_display: Option<SignDisplay>,
68    pub(crate) use_grouping: Option<Grouping>,
69    pub(crate) min_int: Option<u8>,
70    pub(crate) min_frac: Option<u8>,
71    pub(crate) max_frac: Option<u8>,
72    pub(crate) min_sig: Option<u8>,
73    pub(crate) max_sig: Option<u8>,
74    pub(crate) strip_if_integer: Option<bool>,
75    pub(crate) rounding_priority: Option<RoundingPriority>,
76    /// 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500 or 5000.
77    pub(crate) rounding_increment: Option<u16>,
78    pub(crate) rounding_mode: Option<RoundingMode>,
79}
80
81/// Default fraction digits (`:number` 0–3, `:integer` 0–0; P4: `:percent`,
82/// `:currency`).
83#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
84pub(crate) struct FracDefaults {
85    pub(crate) min: u8,
86    pub(crate) max: u8,
87}
88
89/// Option kinds.
90#[derive(Clone, Copy, PartialEq, Eq)]
91pub(crate) enum Kind {
92    Select,
93    Sign,
94    Grouping,
95    MinInt,
96    MinFrac,
97    MaxFrac,
98    MinSig,
99    MaxSig,
100    TrailingZero,
101    Priority,
102    Increment,
103    Mode,
104}
105
106/// Which functions an option applies to (bits).
107pub(crate) const NUM: u8 = 1;
108pub(crate) const INT: u8 = 2;
109pub(crate) const PCT: u8 = 4;
110pub(crate) const CUR: u8 = 8;
111pub(crate) const UNIT: u8 = 16;
112
113/// `(name, functions, kind)`; `:offset` has only `add`/`subtract`.
114pub(crate) const OPTIONS: [(&str, u8, Kind); 12] = [
115    ("select", NUM | INT, Kind::Select),
116    ("signDisplay", NUM | INT | PCT | UNIT, Kind::Sign),
117    ("useGrouping", NUM | INT | PCT | CUR | UNIT, Kind::Grouping),
118    ("minimumIntegerDigits", NUM | INT | CUR | UNIT, Kind::MinInt),
119    ("minimumFractionDigits", NUM | PCT | UNIT, Kind::MinFrac),
120    ("maximumFractionDigits", NUM | PCT | UNIT, Kind::MaxFrac),
121    (
122        "minimumSignificantDigits",
123        NUM | PCT | CUR | UNIT,
124        Kind::MinSig,
125    ),
126    (
127        "maximumSignificantDigits",
128        NUM | INT | PCT | CUR | UNIT,
129        Kind::MaxSig,
130    ),
131    ("trailingZeroDisplay", NUM | PCT | CUR, Kind::TrailingZero),
132    ("roundingPriority", NUM | PCT | CUR | UNIT, Kind::Priority),
133    ("roundingIncrement", NUM | CUR | UNIT, Kind::Increment),
134    ("roundingMode", NUM | PCT | CUR | UNIT, Kind::Mode),
135];
136
137const SELECTS: [(&str, Select); 3] = [
138    ("plural", Select::Plural),
139    ("ordinal", Select::Ordinal),
140    ("exact", Select::Exact),
141];
142const SIGNS: [(&str, SignDisplay); 5] = [
143    ("auto", SignDisplay::Auto),
144    ("always", SignDisplay::Always),
145    ("exceptZero", SignDisplay::ExceptZero),
146    ("negative", SignDisplay::Negative),
147    ("never", SignDisplay::Never),
148];
149const GROUPINGS: [(&str, Grouping); 4] = [
150    ("auto", Grouping::Auto),
151    ("always", Grouping::Always),
152    ("never", Grouping::Never),
153    ("min2", Grouping::Min2),
154];
155const TRAILING: [(&str, bool); 2] = [("auto", false), ("stripIfInteger", true)];
156const PRIORITIES: [(&str, RoundingPriority); 3] = [
157    ("auto", RoundingPriority::Auto),
158    ("morePrecision", RoundingPriority::MorePrecision),
159    ("lessPrecision", RoundingPriority::LessPrecision),
160];
161const MODES: [(&str, RoundingMode); 9] = [
162    ("ceil", RoundingMode::Ceil),
163    ("floor", RoundingMode::Floor),
164    ("expand", RoundingMode::Expand),
165    ("trunc", RoundingMode::Trunc),
166    ("halfCeil", RoundingMode::HalfCeil),
167    ("halfFloor", RoundingMode::HalfFloor),
168    ("halfExpand", RoundingMode::HalfExpand),
169    ("halfTrunc", RoundingMode::HalfTrunc),
170    ("halfEven", RoundingMode::HalfEven),
171];
172const INCREMENTS: [u16; 15] = [
173    1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000,
174];
175
176fn find<T: Copy>(table: &[(&str, T)], v: &str) -> Option<T> {
177    table.iter().find(|(k, _)| *k == v).map(|&(_, t)| t)
178}
179
180/// The select mode named by a literal.
181pub(crate) fn select_named(v: &str) -> Option<Select> {
182    find(&SELECTS, v)
183}
184
185/// A *digit size option* (`number.md`): `"0" / ("1"-"9") [DIGIT]` as text,
186/// or an integer value 0..=99.
187pub(crate) fn digit_size(v: &Value<'_>) -> Option<u8> {
188    match v {
189        Value::Int(n) => u8::try_from(*n).ok().filter(|n| *n <= 99),
190        Value::Number(n) => n
191            .value
192            .to_i64()
193            .and_then(|n| u8::try_from(n).ok())
194            .filter(|n| *n <= 99),
195        Value::Float(x) => {
196            if !(0.0..=99.0).contains(x) {
197                return None;
198            }
199            // In range, so the cast is exact for an integral `x`, and the
200            // comparison is exact on purpose: only an integral value is a
201            // digit size.
202            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
203            let n = *x as u8;
204            #[allow(clippy::float_cmp)]
205            (f64::from(n) == *x).then_some(n)
206        }
207        _ => match *v.as_str()?.as_bytes() {
208            [d @ b'0'..=b'9'] => Some(d - b'0'),
209            [a @ b'1'..=b'9', b @ b'0'..=b'9'] => Some((a - b'0') * 10 + (b - b'0')),
210            _ => None,
211        },
212    }
213}
214
215fn increment_of(v: &Value<'_>) -> Option<u16> {
216    let n = match v {
217        Value::Int(n) => u16::try_from(*n).ok()?,
218        Value::Number(n) => u16::try_from(n.value.to_i64()?).ok()?,
219        _ => {
220            let b = v.as_str()?.as_bytes();
221            if b.is_empty() || b.len() > 4 || b.first() == Some(&b'0') {
222                return None;
223            }
224            b.iter().try_fold(0u16, |acc, &d| {
225                d.is_ascii_digit().then(|| acc * 10 + u16::from(d - b'0'))
226            })?
227        }
228    };
229    INCREMENTS.contains(&n).then_some(n)
230}
231
232/// Applies one option of kind `kind` (other than `select`); `false` for an
233/// invalid value (→ *Bad Option*, the option is ignored).
234pub(crate) fn apply(o: &mut NumOpts, kind: Kind, v: &Value<'_>) -> bool {
235    let t = v.as_str();
236    let ok = match kind {
237        Kind::Sign => t
238            .and_then(|t| find(&SIGNS, t))
239            .map(|x| o.sign_display = Some(x)),
240        Kind::Grouping => t
241            .and_then(|t| find(&GROUPINGS, t))
242            .map(|x| o.use_grouping = Some(x)),
243        Kind::MinInt => digit_size(v).map(|x| o.min_int = Some(x)),
244        Kind::MinFrac => digit_size(v).map(|x| o.min_frac = Some(x)),
245        Kind::MaxFrac => digit_size(v).map(|x| o.max_frac = Some(x)),
246        // An implementation limit: significant digits ≥ 1.
247        Kind::MinSig => digit_size(v)
248            .filter(|x| *x >= 1)
249            .map(|x| o.min_sig = Some(x)),
250        Kind::MaxSig => digit_size(v)
251            .filter(|x| *x >= 1)
252            .map(|x| o.max_sig = Some(x)),
253        Kind::TrailingZero => t
254            .and_then(|t| find(&TRAILING, t))
255            .map(|x| o.strip_if_integer = Some(x)),
256        Kind::Priority => t
257            .and_then(|t| find(&PRIORITIES, t))
258            .map(|x| o.rounding_priority = Some(x)),
259        Kind::Increment => increment_of(v).map(|x| o.rounding_increment = Some(x)),
260        Kind::Mode => t
261            .and_then(|t| find(&MODES, t))
262            .map(|x| o.rounding_mode = Some(x)),
263        Kind::Select => None,
264    };
265    ok.is_some()
266}
267
268/// How the digits are rounded (ECMA-402 `[[RoundingType]]`).
269#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
270pub(crate) enum RoundingType {
271    Fraction,
272    Significant,
273    More,
274    Less,
275}
276
277/// ECMA-402 `SetNumberFormatDigitOptions`, resolved once per value.
278#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
279pub(crate) struct DigitPlan {
280    pub(crate) min_int: u8,
281    pub(crate) min_frac: u8,
282    pub(crate) max_frac: u8,
283    pub(crate) min_sig: u8,
284    pub(crate) max_sig: u8,
285    pub(crate) ty: RoundingType,
286    /// The increment's mantissa and its power of ten.
287    pub(crate) increment: (Increment, i16),
288    pub(crate) mode: RoundingMode,
289    pub(crate) strip_if_integer: bool,
290}
291
292/// `m × 10^k` for a `roundingIncrement` value.
293fn split_increment(n: u16) -> (Increment, i16) {
294    let mut n = n;
295    let mut k = 0;
296    while n >= 10 && n.is_multiple_of(10) && n != 25 {
297        n /= 10;
298        k += 1;
299    }
300    let inc = match n {
301        2 => Increment::Two,
302        5 => Increment::Five,
303        25 => Increment::TwentyFive,
304        _ => Increment::One,
305    };
306    (inc, k)
307}
308
309/// The digit plan of options `o` with fraction defaults `frac`; conflicting
310/// options report *Bad Option* (once, at resolution).
311// Inlined into `resolve`, its one caller on a client's path: the Rust
312// backend's `plan` (reached only through `Number::format_by_host`, which a
313// client without `intl` never links) is a second caller that otherwise keeps
314// it out of line, +54 B raw in `b12-runtime` (bench/b12/check.sh).
315#[allow(clippy::inline_always)]
316#[inline(always)]
317pub(crate) fn digit_plan(o: &NumOpts, frac: FracDefaults, errs: &mut dyn ErrorSink) -> DigitPlan {
318    let increment = o.rounding_increment.unwrap_or(1);
319    let mnfd_default = frac.min;
320    // A rounding increment makes the maximum default to the minimum.
321    let mxfd_default = if increment == 1 { frac.max } else { frac.min };
322    let priority = o.rounding_priority.unwrap_or(RoundingPriority::Auto);
323    let has_sd = o.min_sig.is_some() || o.max_sig.is_some();
324    let has_fd = o.min_frac.is_some() || o.max_frac.is_some();
325    let need_sd = priority != RoundingPriority::Auto || has_sd;
326    let need_fd = priority != RoundingPriority::Auto || !has_sd;
327    let (mut min_sig, mut max_sig) = (1, 21);
328    if need_sd && has_sd {
329        min_sig = o.min_sig.unwrap_or(1);
330        max_sig = o.max_sig.unwrap_or(21.max(min_sig));
331        if min_sig > max_sig {
332            errs.error(FormatError::BadOption);
333            min_sig = max_sig;
334        }
335    }
336    let (mut min_frac, mut max_frac) = (mnfd_default, mxfd_default);
337    if need_fd && has_fd {
338        match (o.min_frac, o.max_frac) {
339            (None, Some(mx)) => {
340                max_frac = mx;
341                min_frac = mnfd_default.min(mx);
342            }
343            (Some(mn), None) => {
344                min_frac = mn;
345                max_frac = mxfd_default.max(mn);
346            }
347            (Some(mn), Some(mx)) => {
348                if mn > mx {
349                    errs.error(FormatError::BadOption);
350                    min_frac = mx;
351                } else {
352                    min_frac = mn;
353                }
354                max_frac = mx;
355            }
356            (None, None) => {}
357        }
358    }
359    let ty = match priority {
360        RoundingPriority::Auto if need_sd => RoundingType::Significant,
361        RoundingPriority::Auto => RoundingType::Fraction,
362        RoundingPriority::MorePrecision => RoundingType::More,
363        RoundingPriority::LessPrecision => RoundingType::Less,
364    };
365    let increment = if increment != 1 && (ty != RoundingType::Fraction || min_frac != max_frac) {
366        errs.error(FormatError::BadOption);
367        1
368    } else {
369        increment
370    };
371    DigitPlan {
372        min_int: o.min_int.unwrap_or(1),
373        min_frac,
374        max_frac,
375        min_sig,
376        max_sig,
377        ty,
378        increment: split_increment(increment),
379        mode: o.rounding_mode.unwrap_or(RoundingMode::HalfExpand),
380        strip_if_integer: o.strip_if_integer.unwrap_or(false),
381    }
382}