Skip to main content

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}