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> {
60 let tz = jiff::tz::TimeZoneDatabase::bundled().get(zone).ok()?;
61 let t = jiff::Timestamp::from_millisecond(epoch_ms).ok()?;
62 Some(tz.to_offset(t).seconds())
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use alloc::string::String;
69
70 use mf2_runtime::Host;
71
72 use super::HOST;
73
74 #[test]
75 fn nfc() {
76 let mut buf = String::new();
77 assert_eq!(HOST.nfc("\u{1E0A}\u{0323}", &mut buf), "\u{1E0C}\u{0307}");
78 let mut buf = String::new();
79 assert_eq!(HOST.nfc("e\u{301}", &mut buf), "\u{e9}");
80 }
81
82 #[test]
83 fn floats() {
84 let mut buf = [0u8; 32];
85 for (x, want) in [
86 (4.2, "4.2"),
87 (1.0, "1.0"),
88 (-0.5, "-0.5"),
89 (1e21, "1e21"),
90 (1.5e-7, "1.5e-7"),
91 (f64::MAX, "1.7976931348623157e308"),
92 (-f64::MIN_POSITIVE, "-2.2250738585072014e-308"),
93 ] {
94 assert_eq!(HOST.f64_to_text(x, &mut buf), Some(want));
95 }
96 assert_eq!(HOST.f64_to_text(f64::NAN, &mut buf), None);
97 }
98
99 fn at(year: i32, month: u8, day: u8, hour: i64, minute: i64, second: i64) -> i64 {
101 let days =
102 mf2_runtime::Date::new(year, month, day).map_or(0, mf2_runtime::Date::days_since_epoch);
103 ((days * 24 + hour) * 60 + minute) * 60_000 + second * 1000
104 }
105
106 #[test]
107 fn zone_offsets() {
108 const H: i32 = 3600;
109 for (zone, t, want) in [
110 ("Europe/Paris", at(2006, 3, 26, 0, 59, 59), H),
112 ("Europe/Paris", at(2006, 3, 26, 1, 0, 0), 2 * H),
113 ("Europe/Paris", at(2006, 10, 29, 0, 59, 59), 2 * H),
114 ("Europe/Paris", at(2006, 10, 29, 1, 0, 0), H),
115 ("America/New_York", at(2021, 3, 14, 6, 59, 59), -5 * H),
117 ("America/New_York", at(2021, 3, 14, 7, 0, 0), -4 * H),
118 ("America/New_York", at(2021, 11, 7, 5, 59, 59), -4 * H),
119 ("America/New_York", at(2021, 11, 7, 6, 0, 0), -5 * H),
120 ("America/New_York", at(2006, 3, 20, 12, 0, 0), -5 * H),
122 ("America/New_York", at(2006, 4, 2, 7, 0, 0), -4 * H),
123 ("Australia/Sydney", at(2006, 1, 2, 0, 0, 0), 11 * H),
125 ("Australia/Sydney", at(2006, 7, 2, 0, 0, 0), 10 * H),
126 ("Asia/Kolkata", at(2006, 1, 2, 0, 0, 0), 5 * H + 1800),
127 ("Asia/Kathmandu", at(2006, 1, 2, 0, 0, 0), 5 * H + 2700),
128 ("UTC", at(2006, 1, 2, 0, 0, 0), 0),
129 ("Etc/GMT+5", at(2006, 1, 2, 0, 0, 0), -5 * H),
130 ("europe/paris", at(2006, 1, 2, 0, 0, 0), H),
132 ("Europe/Paris", at(1900, 1, 1, 0, 0, 0), 561),
134 ] {
135 assert_eq!(HOST.zone_offset(zone, t), Some(want), "{zone} at {t}");
136 }
137 assert_eq!(HOST.zone_offset("Mars/Olympus_Mons", 0), None);
138 assert_eq!(HOST.zone_offset("", 0), None);
139 assert_eq!(HOST.zone_offset("Europe/Paris", i64::MAX), None);
141 }
142}