mf2_fn_datetime/plan.rs
1//! The seam between the date semantics and a backend
2//! (`plans/03-runtime.md` §5.2: "a backend only turns an instant plus a
3//! style into text"): a resolved value becomes a [`Plan`] — the wall time to
4//! show, the zone it is shown in, the options — and a [`Backend`] writes it.
5
6use mf2_runtime::{
7 Date, DateTime, DateTimeOptions, DateTimeRequest, Dir, FnContext, FormatError, Sink,
8 SubPartSink, Time, ZoneOption,
9};
10
11use crate::zone::wall_ms;
12
13/// What a backend formats: backend-independent, everything MF2 decided.
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
15#[non_exhaustive]
16pub struct Plan<'p> {
17 /// The date to show: the wall date in [`Plan::zone`].
18 pub date: Date,
19 /// The time to show: the wall time in [`Plan::zone`], to the millisecond.
20 pub time: Time,
21 /// The zone's UTC offset at that instant, in seconds east; `None`: not
22 /// known — a floating value placed in a named zone the host has no data
23 /// for.
24 pub offset: Option<i32>,
25 /// The zone the value is shown in: [`ZoneOption::Utc`], an offset or a
26 /// named zone, never [`ZoneOption::Input`]. What `timeZoneStyle`
27 /// names.
28 pub zone: ZoneOption<'p>,
29 /// The resolved options: `date` (fields and length) for `:datetime` and
30 /// `:date`, `time` (precision) for `:datetime` and `:time`,
31 /// `time_zone_style`, `hour12`, `calendar`. Their `time_zone` is the
32 /// *option* (the inheritance record): a backend shows [`Plan::zone`].
33 pub options: &'p DateTimeOptions<'p>,
34}
35
36impl<'p> Plan<'p> {
37 /// The plan of `value`, which a date/time function resolved in the
38 /// context `cx` (so it is in the zone its `options.time_zone` names,
39 /// the context's when `None`).
40 pub fn new(cx: &FnContext<'p>, value: &'p DateTime<'p>) -> Plan<'p> {
41 let zone = match (value.zone, value.options.time_zone) {
42 (Some(z), _) => ZoneOption::Named(z),
43 (None, Some(z @ (ZoneOption::Utc | ZoneOption::Offset(_) | ZoneOption::Named(_)))) => z,
44 // `timeZone=input`, none, or a kind this version does not know.
45 (None, _) => cx.time_zone().as_option(),
46 };
47 Plan {
48 date: value.date,
49 time: value.time,
50 offset: value.offset,
51 zone,
52 options: &value.options,
53 }
54 }
55
56 /// The wall time, in milliseconds since the epoch, read as UTC.
57 pub fn wall_ms(&self) -> i64 {
58 wall_ms(&DateTime::floating(self.date, self.time))
59 }
60
61 /// The instant, in milliseconds since the epoch; `None` when the offset
62 /// is not known.
63 pub fn epoch_ms(&self) -> Option<i64> {
64 self.offset.map(|o| self.wall_ms() - i64::from(o) * 1000)
65 }
66
67 /// The request for a host's date formatter (`Host::format_date_time`,
68 /// `datetime-intl`): the instant in [`Plan::zone`]; without a known
69 /// offset, the wall time read as UTC, shown in UTC (the same wall
70 /// time, but the zone is lost).
71 pub fn request(&self) -> DateTimeRequest<'p> {
72 match self.epoch_ms() {
73 Some(epoch_ms) => DateTimeRequest::new(epoch_ms, self.zone, self.options),
74 None => DateTimeRequest::new(self.wall_ms(), ZoneOption::Utc, self.options),
75 }
76 }
77}
78
79/// A date/time backend: turns a [`Plan`] into text. The semantics —
80/// operands, options, errors, zones — are done; a backend only formats.
81/// Implemented by [`crate::Neutral`] (the stub), and by the `datetime-icu`
82/// and `datetime-intl` backends behind features (`plans/11` A6).
83///
84/// Client-path code: no `core::fmt`, no panics, no allocation.
85pub trait Backend: Sync {
86 /// Whether `plan` can be formatted. `Err(e)`: the placeholder becomes
87 /// a fallback value and `e` is reported (e.g. *Unsupported
88 /// Operation* for a calendar the backend's data lacks). Called before
89 /// `dir` and `format`.
90 fn supports(&self, cx: &FnContext<'_>, plan: &Plan<'_>) -> Result<(), FormatError> {
91 let _ = (cx, plan);
92 Ok(())
93 }
94
95 /// Writes `plan` for `cx.locale()`.
96 fn format(&self, cx: &FnContext<'_>, plan: &Plan<'_>, out: &mut dyn Sink);
97
98 /// Writes `plan`'s sub-parts (none by default).
99 fn format_parts(&self, cx: &FnContext<'_>, plan: &Plan<'_>, out: &mut dyn SubPartSink) {
100 let _ = (cx, plan, out);
101 }
102
103 /// The direction of the formatted text (the Default Bidi Strategy);
104 /// `Ltr` by default.
105 fn dir(&self, cx: &FnContext<'_>, plan: &Plan<'_>) -> Dir {
106 let _ = (cx, plan);
107 Dir::Ltr
108 }
109}