Skip to main content

mf2_fn_datetime/
neutral.rs

1//! The neutral stub backend: deterministic, locale-independent text in
2//! ISO 8601 pieces, for tests and for a build with no backend feature —
3//! the date counterpart of the core's neutral digits.
4
5use mf2_runtime::{DateFields, FnContext, Sink, TimePrecision, ZoneOption, ZoneStyle};
6
7use crate::plan::{Backend, Plan};
8
9/// The neutral backend. Its text is the same in every locale: the pieces
10/// the options ask for, in the order date, weekday, time, zone, separated
11/// by one space —
12///
13/// | Option | Piece |
14/// |---|---|
15/// | `year-month-day` | `2006-01-02` (a year outside 0–9999: `+010000`, `-000044`) |
16/// | `month-day` | `--01-02` |
17/// | `day-weekday` | `---02` |
18/// | `weekday`, `…-weekday` | `Mon` … `Sun` |
19/// | `hour` / `minute` / `second` | `15` / `15:04` / `15:04:06` |
20/// | `hour12=true` | `03 PM` / `03:04 PM` / `03:04:06 PM` (`12 AM` at midnight) |
21/// | `timeZoneStyle=short` | `Z` or `+05:30` (`+hh:mm:ss` if it has seconds); the zone's name if its offset is not known |
22/// | `timeZoneStyle=long` | `UTC`, `+05:30`, or the zone's name (`Europe/Paris`) |
23///
24/// So `{|2006-01-02T15:04:06| :datetime}` is `2006-01-02 15:04`. The
25/// length (`dateLength`, `length`) and `calendar` do not change it: it is
26/// always the ISO (proleptic Gregorian) calendar.
27#[derive(Clone, Copy, Default, Debug)]
28pub struct Neutral;
29
30const DIGITS: [&str; 10] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
31
32const WEEKDAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
33
34/// Writes `n` with at least `width` digits (`n` < 10⁶, `width` ≤ 6).
35/// Constant divisors only, so no division-by-zero path.
36fn digits(out: &mut dyn Sink, n: u32, width: usize) {
37    const N: usize = 6;
38    let mut buf = [0u8; N];
39    let mut len = 0;
40    let mut rest = n;
41    while len < N {
42        if let Some(b) = buf.get_mut(N - 1 - len) {
43            // `rest % 10` < 10.
44            #[allow(clippy::cast_possible_truncation)]
45            let d = (rest % 10) as u8;
46            *b = d;
47        }
48        rest /= 10;
49        len += 1;
50        if rest == 0 && len >= width {
51            break;
52        }
53    }
54    for &d in buf.get(N - len..).unwrap_or(&[]) {
55        out.push_str(DIGITS.get(usize::from(d)).copied().unwrap_or("0"));
56    }
57}
58
59/// Writes a UTC offset: `±hh:mm`, `:ss` when it has seconds.
60fn offset(out: &mut dyn Sink, seconds: i32) {
61    out.push_str(if seconds < 0 { "-" } else { "+" });
62    let s = seconds.unsigned_abs();
63    digits(out, s / 3600, 2);
64    out.push_str(":");
65    digits(out, s / 60 % 60, 2);
66    if !s.is_multiple_of(60) {
67        out.push_str(":");
68        digits(out, s % 60, 2);
69    }
70}
71
72impl Backend for Neutral {
73    fn format(&self, _cx: &FnContext<'_>, plan: &Plan<'_>, out: &mut dyn Sink) {
74        let o = plan.options;
75        let mut first = true;
76        let mut space = |out: &mut dyn Sink| {
77            if !first {
78                out.push_str(" ");
79            }
80            first = false;
81        };
82        if let Some(style) = o.date {
83            let d = plan.date;
84            let (year, month, weekday) = match style.fields {
85                DateFields::YearMonthDayWeekday => (true, true, true),
86                DateFields::MonthDay => (false, true, false),
87                DateFields::MonthDayWeekday => (false, true, true),
88                DateFields::DayWeekday | DateFields::Weekday => (false, false, true),
89                // `YearMonthDay`, and (not exhaustive) a value this stub
90                // does not know: the whole date.
91                _ => (true, true, false),
92            };
93            if style.fields != DateFields::Weekday {
94                space(out);
95                if year {
96                    let y = d.year();
97                    if (0..=9999).contains(&y) {
98                        digits(out, y.unsigned_abs(), 4);
99                    } else {
100                        out.push_str(if y < 0 { "-" } else { "+" });
101                        digits(out, y.unsigned_abs(), 6);
102                    }
103                } else {
104                    out.push_str("-");
105                }
106                out.push_str("-");
107                if month {
108                    digits(out, u32::from(d.month()), 2);
109                }
110                out.push_str("-");
111                digits(out, u32::from(d.day()), 2);
112            }
113            if weekday {
114                space(out);
115                let w = usize::from(d.weekday().saturating_sub(1));
116                out.push_str(WEEKDAYS.get(w).copied().unwrap_or(""));
117            }
118        }
119        if let Some(precision) = o.time {
120            space(out);
121            let h = plan.time.hour();
122            let twelve = o.hour12 == Some(true);
123            let shown = match (twelve, h % 12) {
124                (true, 0) => 12,
125                (true, h12) => h12,
126                (false, _) => h,
127            };
128            digits(out, u32::from(shown), 2);
129            if precision != TimePrecision::Hour {
130                out.push_str(":");
131                digits(out, u32::from(plan.time.minute()), 2);
132            }
133            if precision == TimePrecision::Second {
134                out.push_str(":");
135                digits(out, u32::from(plan.time.second()), 2);
136            }
137            if twelve {
138                out.push_str(if h < 12 { " AM" } else { " PM" });
139            }
140        }
141        if let Some(style) = o.time_zone_style {
142            space(out);
143            match (style, plan.zone, plan.offset) {
144                (ZoneStyle::Short, _, Some(0)) => out.push_str("Z"),
145                (ZoneStyle::Short, _, Some(s)) | (ZoneStyle::Long, ZoneOption::Offset(s), _) => {
146                    offset(out, s);
147                }
148                (_, ZoneOption::Named(n), _) => out.push_str(n),
149                (ZoneStyle::Short, ZoneOption::Offset(s), None) => offset(out, s),
150                // UTC and `input`, and a kind this stub does not know.
151                _ => out.push_str("UTC"),
152            }
153        }
154    }
155}