Skip to main content

mf2_fn_number/
lib.rs

1//! `mf2-fn-number` — the localized numeric functions of Rust MF2: the
2//! runtime's numeric core
3//! (`mf2_runtime::NumberSpec`, `Number::resolve`) resolves — operand rules,
4//! every digit and rounding option, `signDisplay`, inheritance, selection —
5//! and this crate writes the rounded digits with the catalog's locale data
6//! (its `number.symbols` and `number.patterns` entries):
7//! decimal and group separators, signs, the numbering system's digits,
8//! grouping (`useGrouping` `auto` / `always` / `min2` / `never`, with the
9//! locale's minimum grouping digits), and the percent pattern.
10//!
11//! | Static | Function |
12//! |---|---|
13//! | [`NUMBER`], [`INTEGER`], [`OFFSET`] | `:number`, `:integer`, `:offset`, localized (the core's are neutral) |
14//! | [`PERCENT`] | `:percent` |
15//! | [`CURRENCY`], [`UNIT`] | `:currency`, `:unit` (Draft): a `Measure` with the catalog's `currency.data` / `unit.data` |
16//! | [`NUMBERS`] | unannotated numbers, localized: `Registry::with_numbers(&NUMBERS)` |
17//!
18//! With feature `intl` on `wasm32-unknown-unknown`
19//! ([`mf2_runtime::INTL_NUMBERS`]) the text comes from the host's number
20//! formatter (the browser's `Intl.NumberFormat`) instead.
21//!
22//! Closed world: an application's registry names only the handlers
23//! its corpus uses. Selection, exact-match keys and plural operands are the
24//! core's (neutral digits, the formatted digits' plural category): a
25//! localized handler only changes the text. Client-path code: `no_std`,
26//! `forbid(unsafe_code)`, no `core::fmt`, no panicking operation, no
27//! allocation.
28//!
29//! # The user guide
30//!
31//! The [Rust MF2 book](https://evancarroll.github.io/rust-mf2/) is the user
32//! guide: how the crates fit together, web and native applications, the
33//! command line, and what 2.x promises.
34//! An application reaches this crate through
35//! [`mf2`](https://docs.rs/mf2), as `mf2::fn_number` (feature `fn-number`).
36
37#![warn(missing_docs)]
38// docs.rs (`cargo xtask docs-rs`): each feature-gated item says which features it needs.
39#![cfg_attr(docsrs, feature(doc_cfg))]
40#![no_std]
41#![forbid(unsafe_code)]
42#![deny(
43    clippy::unwrap_used,
44    clippy::expect_used,
45    clippy::indexing_slicing,
46    clippy::panic
47)]
48
49mod intl;
50mod localize;
51mod measure;
52
53pub use measure::{CURRENCY, CurrencyFunction, UNIT, UnitFunction};
54
55use mf2_runtime::{
56    Dir, ErrorSink, FnContext, FormatError, Function, INTL_NUMBERS, Number, NumberOut, NumberSpec,
57    Options, Sink, SubPartSink, Value,
58};
59
60use localize::{Layout, Out};
61
62/// A localized numeric handler: `:number`, `:integer`, `:offset` or
63/// `:percent`.
64#[derive(Clone, Copy)]
65pub struct NumberFunction {
66    spec: NumberSpec,
67    layout: Layout,
68}
69
70impl NumberFunction {
71    const fn new(spec: NumberSpec, layout: Layout) -> Self {
72        NumberFunction { spec, layout }
73    }
74}
75
76/// `:number` (`functions/number.md`), localized.
77pub static NUMBER: NumberFunction = NumberFunction::new(NumberSpec::NUMBER, Layout::Decimal);
78
79/// `:integer` (`functions/number.md`), localized.
80pub static INTEGER: NumberFunction = NumberFunction::new(NumberSpec::INTEGER, Layout::Decimal);
81
82/// `:offset` (`functions/number.md`), localized.
83pub static OFFSET: NumberFunction = NumberFunction::new(NumberSpec::OFFSET, Layout::Decimal);
84
85/// `:percent` (`functions/number.md`): the value × 100 with the locale's
86/// percent pattern; selects on the scaled value (plural).
87pub static PERCENT: NumberFunction = NumberFunction::new(NumberSpec::PERCENT, Layout::Percent);
88
89impl Function for NumberFunction {
90    fn resolve<'a>(
91        &self,
92        cx: &FnContext<'_>,
93        operand: Option<&Value<'a>>,
94        options: &Options<'_, 'a>,
95        errs: &mut dyn ErrorSink,
96    ) -> Option<Value<'a>> {
97        Number::resolve(self.spec, cx, operand, options, errs).map(Value::Number)
98    }
99
100    fn formattable(&self, _cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
101        match value {
102            Value::Number(_) => Ok(()),
103            _ => Err(FormatError::MessageFunctionError),
104        }
105    }
106
107    fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
108        if INTL_NUMBERS {
109            return intl::format(cx, value, self.layout, NumberOut::Text(out));
110        }
111        if let Value::Number(n) = value
112            && let Some(d) = n.digits()
113        {
114            localize::write(
115                cx.catalog(),
116                &d,
117                n.grouping(),
118                self.layout,
119                &mut Out::Text(out),
120            );
121        }
122    }
123
124    fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
125        if INTL_NUMBERS {
126            return intl::format(cx, value, self.layout, NumberOut::Parts(out));
127        }
128        if let Value::Number(n) = value
129            && let Some(d) = n.digits()
130        {
131            localize::write(
132                cx.catalog(),
133                &d,
134                n.grouping(),
135                self.layout,
136                &mut Out::Parts(out),
137            );
138        }
139    }
140
141    fn part_kind(&self) -> &'static str {
142        "number"
143    }
144
145    fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
146        // Digits read left to right in every numbering system CLDR has.
147        Dir::Ltr
148    }
149
150    fn selectable(&self, value: &Value<'_>) -> bool {
151        matches!(value, Value::Number(n) if n.is_selectable())
152    }
153
154    fn matches(
155        &self,
156        cx: &FnContext<'_>,
157        value: &Value<'_>,
158        key: &str,
159        errs: &mut dyn ErrorSink,
160    ) -> bool {
161        match value {
162            Value::Number(n) => n.matches(cx, key, errs),
163            _ => false,
164        }
165    }
166
167    fn better_than(&self, _cx: &FnContext<'_>, _value: &Value<'_>, key1: &str, key2: &str) -> bool {
168        Number::better_than(key1, key2)
169    }
170}
171
172impl core::fmt::Debug for NumberFunction {
173    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174        f.write_str("NumberFunction")
175    }
176}
177
178/// Unannotated numbers — integer, float and decimal arguments — as their
179/// exact value (no rounding) with the locale's symbols, grouping (`auto`)
180/// and digits: the handler of `Registry::with_numbers` (`plans/03-runtime.md`
181/// §2.7). The evaluator checks the value first (a non-finite float is a
182/// Bad Operand, as without this handler).
183#[derive(Clone, Copy, Default, Debug)]
184pub struct Unannotated;
185
186/// The handler for unannotated numbers: `Registry::new(…).with_numbers(&NUMBERS)`.
187pub static NUMBERS: Unannotated = Unannotated;
188
189impl Function for Unannotated {
190    fn resolve<'a>(
191        &self,
192        _cx: &FnContext<'_>,
193        _operand: Option<&Value<'a>>,
194        _options: &Options<'_, 'a>,
195        errs: &mut dyn ErrorSink,
196    ) -> Option<Value<'a>> {
197        // Not a function a message can name: only `with_numbers` uses it.
198        errs.error(FormatError::MessageFunctionError);
199        None
200    }
201
202    fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink) {
203        if INTL_NUMBERS {
204            return intl::format_unannotated(cx, value, NumberOut::Text(out));
205        }
206        if let Some(n) = value.to_number(cx.host()) {
207            localize::write(
208                cx.catalog(),
209                &n.exact_digits(),
210                None,
211                Layout::Decimal,
212                &mut Out::Text(out),
213            );
214        }
215    }
216
217    fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
218        if INTL_NUMBERS {
219            return intl::format_unannotated(cx, value, NumberOut::Parts(out));
220        }
221        if let Some(n) = value.to_number(cx.host()) {
222            localize::write(
223                cx.catalog(),
224                &n.exact_digits(),
225                None,
226                Layout::Decimal,
227                &mut Out::Parts(out),
228            );
229        }
230    }
231
232    fn part_kind(&self) -> &'static str {
233        "number"
234    }
235
236    fn dir(&self, _cx: &FnContext<'_>, _value: &Value<'_>) -> Dir {
237        Dir::Ltr
238    }
239}