1#![warn(missing_docs)]
18#![cfg_attr(docsrs, feature(doc_cfg))]
20#![no_std]
21#![forbid(unsafe_code)]
22
23extern crate alloc;
24
25use alloc::string::String;
26
27use mf2_runtime::Host;
28use unicode_normalization::UnicodeNormalization;
29
30#[derive(Clone, Copy, Default, Debug)]
32pub struct StdHost;
33
34pub static HOST: StdHost = StdHost;
36
37impl Host for StdHost {
38 fn nfc<'a>(&self, s: &'a str, buf: &'a mut String) -> &'a str {
39 buf.clear();
40 buf.extend(s.nfc());
41 buf
42 }
43
44 fn f64_to_text<'b>(&self, x: f64, buf: &'b mut [u8; 32]) -> Option<&'b str> {
45 if !x.is_finite() {
46 return None;
47 }
48 let mut b = ryu::Buffer::new();
49 let text = b.format_finite(x).as_bytes();
50 let out = buf.get_mut(..text.len())?;
51 out.copy_from_slice(text);
52 core::str::from_utf8(out).ok()
53 }
54
55 fn zone_offset(&self, zone: &str, epoch_ms: i64) -> Option<i32> {
62 let t = jiff::Timestamp::from_millisecond(epoch_ms).ok()?;
63 let tz = match jiff::tz::TimeZoneDatabase::bundled().get(zone) {
64 Ok(tz) => tz,
65 Err(_) => jiff::tz::TimeZone::posix(zone).ok()?,
66 };
67 Some(tz.to_offset(t).seconds())
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use alloc::string::String;
74
75 use mf2_runtime::Host;
76
77 use super::HOST;
78
79 #[test]
80 fn nfc() {
81 let mut buf = String::new();
82 assert_eq!(HOST.nfc("\u{1E0A}\u{0323}", &mut buf), "\u{1E0C}\u{0307}");
83 let mut buf = String::new();
84 assert_eq!(HOST.nfc("e\u{301}", &mut buf), "\u{e9}");
85 }
86
87 #[test]
88 fn floats() {
89 let mut buf = [0u8; 32];
90 for (x, want) in [
91 (4.2, "4.2"),
92 (1.0, "1.0"),
93 (-0.5, "-0.5"),
94 (1e21, "1e21"),
95 (1.5e-7, "1.5e-7"),
96 (f64::MAX, "1.7976931348623157e308"),
97 (-f64::MIN_POSITIVE, "-2.2250738585072014e-308"),
98 ] {
99 assert_eq!(HOST.f64_to_text(x, &mut buf), Some(want));
100 }
101 assert_eq!(HOST.f64_to_text(f64::NAN, &mut buf), None);
102 }
103
104 fn at(year: i32, month: u8, day: u8, hour: i64, minute: i64, second: i64) -> i64 {
106 let days =
107 mf2_runtime::Date::new(year, month, day).map_or(0, mf2_runtime::Date::days_since_epoch);
108 ((days * 24 + hour) * 60 + minute) * 60_000 + second * 1000
109 }
110
111 #[test]
112 fn zone_offsets() {
113 const H: i32 = 3600;
114 for (zone, t, want) in [
115 ("Europe/Paris", at(2006, 3, 26, 0, 59, 59), H),
117 ("Europe/Paris", at(2006, 3, 26, 1, 0, 0), 2 * H),
118 ("Europe/Paris", at(2006, 10, 29, 0, 59, 59), 2 * H),
119 ("Europe/Paris", at(2006, 10, 29, 1, 0, 0), H),
120 ("America/New_York", at(2021, 3, 14, 6, 59, 59), -5 * H),
122 ("America/New_York", at(2021, 3, 14, 7, 0, 0), -4 * H),
123 ("America/New_York", at(2021, 11, 7, 5, 59, 59), -4 * H),
124 ("America/New_York", at(2021, 11, 7, 6, 0, 0), -5 * H),
125 ("America/New_York", at(2006, 3, 20, 12, 0, 0), -5 * H),
127 ("America/New_York", at(2006, 4, 2, 7, 0, 0), -4 * H),
128 ("Australia/Sydney", at(2006, 1, 2, 0, 0, 0), 11 * H),
130 ("Australia/Sydney", at(2006, 7, 2, 0, 0, 0), 10 * H),
131 ("Asia/Kolkata", at(2006, 1, 2, 0, 0, 0), 5 * H + 1800),
132 ("Asia/Kathmandu", at(2006, 1, 2, 0, 0, 0), 5 * H + 2700),
133 ("UTC", at(2006, 1, 2, 0, 0, 0), 0),
134 ("Etc/GMT+5", at(2006, 1, 2, 0, 0, 0), -5 * H),
135 ("europe/paris", at(2006, 1, 2, 0, 0, 0), H),
137 ("Europe/Paris", at(1900, 1, 1, 0, 0, 0), 561),
139 ] {
140 assert_eq!(HOST.zone_offset(zone, t), Some(want), "{zone} at {t}");
141 }
142 assert_eq!(HOST.zone_offset("Mars/Olympus_Mons", 0), None);
143 assert_eq!(HOST.zone_offset("", 0), None);
144 assert_eq!(HOST.zone_offset("EST 5", 0), None);
145 assert_eq!(HOST.zone_offset("Europe/Paris", i64::MAX), None);
147 }
148
149 #[test]
153 fn posix_rules() {
154 const H: i32 = 3600;
155 let us = "EST5EDT,M3.2.0,M11.1.0";
156 let eu = "CET-1CEST,M3.5.0,M10.5.0/3";
157 for (zone, t, want) in [
158 (us, at(2026, 3, 8, 6, 59, 59), -5 * H),
160 (us, at(2026, 3, 8, 7, 0, 0), -4 * H),
161 (us, at(2026, 11, 1, 5, 59, 59), -4 * H),
163 (us, at(2026, 11, 1, 6, 0, 0), -5 * H),
164 (us, at(2026, 1, 15, 17, 0, 0), -5 * H),
165 (us, at(2026, 7, 15, 16, 0, 0), -4 * H),
166 (eu, at(2026, 3, 29, 0, 59, 59), H),
168 (eu, at(2026, 3, 29, 1, 0, 0), 2 * H),
169 ("JST-9", at(2026, 7, 15, 0, 0, 0), 9 * H),
171 ] {
172 assert_eq!(HOST.zone_offset(zone, t), Some(want), "{zone} at {t}");
173 }
174 }
175}