Skip to main content

mf2_fn_datetime/
lib.rs

1//! `mf2-fn-datetime` — the MessageFormat 2 date/time functions of Rust MF2:
2//! `:datetime`, `:date`, `:time`, and the handler that formats unannotated
3//! date/time values. The semantics — operands, options, errors, time zones
4//! — are here, once; a [`Backend`] only turns the result, a [`Plan`], into
5//! text:
6//!
7//! | Feature | [`DefaultBackend`] (the statics') |
8//! |---|---|
9//! | none | [`Neutral`], a deterministic locale-independent stub (ISO 8601 pieces) |
10//! | `datetime-icu` | `icu::Icu`: ICU4X over the catalog's `icu.blob` LOCALE entry, on client and server alike (narrower variants: `Icu<GregorianOnly, NoZones>` …) |
11//! | `datetime-intl` | `Intl` on `wasm32-unknown-unknown`: `Host::format_date_time` (the browser's `Intl.DateTimeFormat` through `mf2-host-web`'s `INTL_HOST`); elsewhere `Icu` over ICU4X's compiled data |
12//!
13//! ```
14//! use mf2_fn_datetime::{DateTimeFunction, Neutral};
15//! use mf2_runtime::{Function, Registry};
16//!
17//! // `DATETIME`, `DATE`, `TIME`, `DATES` are these over the default backend.
18//! static DATETIME: DateTimeFunction<Neutral> = DateTimeFunction::datetime(Neutral);
19//! static DATES: DateTimeFunction<Neutral> = DateTimeFunction::unannotated(Neutral);
20//! static FUNCTIONS: [(&str, &dyn Function); 1] = [("datetime", &DATETIME)];
21//! static REGISTRY: Registry = Registry::new(&FUNCTIONS).with_dates(&DATES);
22//! ```
23//!
24//! A message formatted with that registry: the `mf2` facade's front page,
25//! which reaches this crate as `mf2::fn_datetime`.
26//!
27//! # The handlers
28//!
29//! | Static | Registry name | What |
30//! |---|---|---|
31//! | [`DATETIME`] | `datetime` | date and time; `dateFields` (`year-month-day`), `dateLength` (`medium`), `timePrecision` (`minute`), `timeZoneStyle` |
32//! | [`DATE`] | `date` | the date; `fields` (`year-month-day`), `length` (`medium`) |
33//! | [`TIME`] | `time` | the time; `precision` (`minute`), `timeZoneStyle` |
34//! | [`DATES`] | — (`Registry::with_dates`) | an unannotated date/time, as `:datetime` with its defaults |
35//!
36//! All three functions take the override options `timeZone` and `calendar`;
37//! `:datetime` and `:time` also `hour12`. Closed world: a registry
38//! names only the handlers its corpus uses, and an unused one is never
39//! linked. A handler over another backend: [`DateTimeFunction::datetime`]
40//! and its siblings.
41//!
42//! # Semantics, and the choices the spec leaves open
43//!
44//! * **Operands.** A date/time value — an [`Arg::DateTime`], or what a
45//!   date/time function resolved, with its options — or an application
46//!   value's `CustomValue::as_date_time`, or a string that is a *date/time
47//!   literal value*: a literal, a string argument or an application value's
48//!   `as_str` ([`parse_literal`]). A literal must match the spec's regular
49//!   expression as a whole and name a day that exists; the spec's MAY —
50//!   other ISO 8601 forms — is not taken, so what parses is exactly the
51//!   regex (`-00:00` is offset 0). Without a time it is 00:00:00, without
52//!   an offset floating. Anything else (numbers, booleans, a fallback
53//!   value, no operand) is *Bad Operand* and a fallback value.
54//! * **A `:date` value as a `:time` operand** (and the reverse, which the
55//!   spec says MAY be a *Bad Operand*): accepted. A resolved value keeps the
56//!   whole date/time of its operand, so `{$d :time}` over `$d = {|…T15:04:06|
57//!   :date}` shows 15:04; a date literal's time is 00:00.
58//! * **Options.** Each function takes exactly the options datetime.md lists
59//!   for it; any other option is ignored (so `hour12` and `timeZoneStyle`
60//!   on `:date`). The non-override options must be literals: a variable is
61//!   *Bad Option* and the option is ignored. A value that is not one the
62//!   option takes is *Bad Option*, ignored. Values compare exactly
63//!   (case-sensitive). An override option may come from a variable whose
64//!   value is a string (or an application value with `as_str`); any other
65//!   value is *Bad Option*.
66//! * **Override options.** `timeZone`: `input`, `UTC`, an RFC 3339
67//!   `time-numoffset` (`±hh:mm`, hour 00–23), or an RFC 9557
68//!   `time-zone-name` ([`mf2_runtime::is_zone_name`]; any well-formed name —
69//!   whether a zone of that name exists is the host's knowledge). `hour12`:
70//!   `true` / `false`. `calendar`: a well-formed `uvalue`, `3*8alphanum
71//!   *("-" 3*8alphanum)` in its BCP 47 spelling (UTS 35 also admits `_`,
72//!   which `Intl.DateTimeFormat` rejects); whether the calendar is known is
73//!   the backend's to report. They are inherited from a date/time operand
74//!   (an argument may carry them too) and the expression's own take
75//!   priority; the operand's other options are not inherited: over `.local
76//!   $d = {|2006-01-02| :date length=long}`, `{$d}` formats `$d` as it was
77//!   resolved (long), `{$d :date}` is a new `:date` (medium). A `:date`
78//!   value keeps an `hour12` it inherited, for a later `:time`.
79//! * **Resolved value.** A [`Value::DateTime`] whose `options` hold what
80//!   resolved: `date` for `:datetime` and `:date`, `time` for `:datetime`
81//!   and `:time`, `time_zone_style`, `hour12`, `calendar`, and `time_zone`
82//!   — the zone the value is now in (`None`: the formatting context's). Not
83//!   selectable: a selector on it is *Bad Selector* (the runtime's). Part
84//!   kind `datetime`, direction `Ltr` for the neutral backend.
85//! * **Time zones** (`zone`'s `place`). The default of `timeZone` is the
86//!   formatting context's zone ([`FnContext::time_zone`]). `input` is the
87//!   operand's own zone; on a floating operand it is *Bad Operand* and the
88//!   context's zone is used (and recorded, so a later expression inheriting
89//!   it does not report it again). A floating value takes the target zone
90//!   without conversion; a value with an offset or a zone is converted to
91//!   a different target: to UTC or an offset by arithmetic, to a named zone
92//!   through [`Host::zone_offset`]. A floating wall time placed in a named
93//!   zone gets its instant from a bracketing search over `zone_offset`
94//!   (java.time's / Temporal's `compatible` choice: in an overlap the
95//!   earlier instant, in a gap the wall time moves forward by the gap).
96//! * **No zone data** (`Host::zone_offset` answers `None`, the default).
97//!   Converting a value with an offset to a named zone, or a value in a
98//!   named zone whose offset is unknown to any other zone, cannot be done:
99//!   *Bad Option* and a fallback value, the alternative the specification
100//!   allows —
101//!   not a wall time shown in the wrong zone, and not *Unsupported
102//!   Operation*, since the conversion itself is what the spec asks for. This
103//!   includes the formatting context's default zone, which is the
104//!   `timeZone` option's resolved value when the expression sets none: a
105//!   context in a named zone needs a host with zone data for instants.
106//!   Placing a *floating* value in a named
107//!   zone needs no conversion, so it is no error: the wall time shows, and
108//!   the zone's offset stays unknown (the neutral backend then names the
109//!   zone for `timeZoneStyle`). A converted value past `Date`'s year limit
110//!   is *Bad Operand*.
111//! * **Unannotated** date/time values ([`DATES`], `Registry::with_dates`):
112//!   formatted as `:datetime` with no options (its own override options
113//!   apply); if that resolution reports an error, the placeholder is a
114//!   fallback value with that error.
115//!
116//! Client-path code: `no_std`, `forbid(unsafe_code)`, no `core::fmt` use,
117//! no panicking operation, no allocation —
118//! the semantics, the neutral and the `Intl` backends. The ICU4X backend
119//! allocates (the blob's provider) and links ICU4X's own `core::fmt` and
120//! panic paths: that is `datetime-icu`'s cost in client size.
121//!
122//!
123//! # The user guide
124//!
125//! The [Rust MF2 book](https://evancarroll.github.io/rust-mf2/) is the user
126//! guide: how the crates fit together, web and native applications, the
127//! command line, and what 2.x promises.
128//! An application reaches this crate through
129//! [`mf2`](https://docs.rs/mf2), as `mf2::fn_datetime` (feature `fn-datetime`).
130//!
131//! [`Arg::DateTime`]: mf2_runtime::Arg::DateTime
132//! [`Value::DateTime`]: mf2_runtime::Value::DateTime
133//! [`FnContext::time_zone`]: mf2_runtime::FnContext::time_zone
134//! [`Host::zone_offset`]: mf2_runtime::Host::zone_offset
135
136#![warn(missing_docs)]
137// docs.rs (`cargo xtask docs-rs`): each feature-gated item says which features it needs.
138#![cfg_attr(docsrs, feature(doc_cfg))]
139#![no_std]
140#![forbid(unsafe_code)]
141#![deny(
142    clippy::unwrap_used,
143    clippy::expect_used,
144    clippy::indexing_slicing,
145    clippy::panic
146)]
147
148#[cfg(any(feature = "datetime-icu", feature = "datetime-intl"))]
149extern crate alloc;
150
151mod function;
152#[cfg(any(
153    feature = "datetime-icu",
154    all(
155        feature = "datetime-intl",
156        not(all(target_arch = "wasm32", target_os = "unknown"))
157    )
158))]
159pub mod icu;
160#[cfg(feature = "datetime-intl")]
161mod intl;
162mod literal;
163mod neutral;
164mod options;
165mod plan;
166mod zone;
167
168pub use function::{DateTimeFunction, operand};
169// The build's reading of a literal's options (`mf2-locale-data`'s `icu.blob`).
170#[doc(hidden)]
171pub use function::literal_options;
172#[cfg(feature = "datetime-intl")]
173pub use intl::Intl;
174pub use literal::parse_literal;
175pub use neutral::Neutral;
176pub use plan::{Backend, Plan};
177
178/// The backend the statics format with: [`icu::Icu`] with `datetime-icu`
179/// (every calendar, zone styles, the catalog's `icu.blob`); with
180/// `datetime-intl` alone, [`Intl`] in the browser (`wasm32-unknown-unknown`)
181/// and `Icu` over compiled data elsewhere; [`Neutral`] with neither.
182#[cfg(feature = "datetime-icu")]
183pub type DefaultBackend = icu::Icu;
184
185/// The backend the statics format with (see the `datetime-icu` build).
186#[cfg(all(
187    not(feature = "datetime-icu"),
188    feature = "datetime-intl",
189    all(target_arch = "wasm32", target_os = "unknown")
190))]
191pub type DefaultBackend = Intl;
192
193/// The backend the statics format with (see the `datetime-icu` build).
194#[cfg(all(
195    not(feature = "datetime-icu"),
196    feature = "datetime-intl",
197    not(all(target_arch = "wasm32", target_os = "unknown"))
198))]
199pub type DefaultBackend = icu::Icu<icu::AnyCalendar, icu::WithZones, icu::Compiled>;
200
201/// The backend the statics format with (see the `datetime-icu` build).
202#[cfg(not(any(feature = "datetime-icu", feature = "datetime-intl")))]
203pub type DefaultBackend = Neutral;
204
205#[cfg(any(
206    feature = "datetime-icu",
207    all(
208        feature = "datetime-intl",
209        not(all(target_arch = "wasm32", target_os = "unknown"))
210    )
211))]
212const DEFAULT_BACKEND: DefaultBackend = icu::Icu::NEW;
213
214#[cfg(all(
215    not(feature = "datetime-icu"),
216    feature = "datetime-intl",
217    all(target_arch = "wasm32", target_os = "unknown")
218))]
219const DEFAULT_BACKEND: DefaultBackend = Intl;
220
221#[cfg(not(any(feature = "datetime-icu", feature = "datetime-intl")))]
222const DEFAULT_BACKEND: DefaultBackend = Neutral;
223
224/// `:datetime`.
225pub static DATETIME: DateTimeFunction = DateTimeFunction::datetime(DEFAULT_BACKEND);
226
227/// `:date`.
228pub static DATE: DateTimeFunction = DateTimeFunction::date(DEFAULT_BACKEND);
229
230/// `:time`.
231pub static TIME: DateTimeFunction = DateTimeFunction::time(DEFAULT_BACKEND);
232
233/// Unannotated date/time values, for `Registry::with_dates`: as
234/// `:datetime` with its defaults.
235pub static DATES: DateTimeFunction = DateTimeFunction::unannotated(DEFAULT_BACKEND);