1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct DateTime {
13 pub year: i32,
14 pub month: u32,
16 pub day: u32,
18 pub hour: u32,
20 pub minute: u32,
21 pub second: u32,
22}
23
24impl DateTime {
25 pub fn new(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> Self {
26 DateTime {
27 year,
28 month,
29 day,
30 hour,
31 minute,
32 second,
33 }
34 }
35
36 pub fn now_local() -> Option<DateTime> {
38 #[cfg(not(target_arch = "wasm32"))]
39 {
40 use chrono::{Datelike, Timelike};
41 let n = chrono::Local::now();
42 Some(DateTime::new(
43 n.year(),
44 n.month(),
45 n.day(),
46 n.hour(),
47 n.minute(),
48 n.second(),
49 ))
50 }
51 #[cfg(target_arch = "wasm32")]
52 {
53 None
54 }
55 }
56
57 fn is_valid(&self) -> bool {
59 let leap = self.year % 4 == 0 && (self.year % 100 != 0 || self.year % 400 == 0);
60 let days = match self.month {
61 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
62 4 | 6 | 9 | 11 => 30,
63 2 if leap => 29,
64 2 => 28,
65 _ => return false,
66 };
67 (1..=9999).contains(&self.year)
68 && (1..=days).contains(&self.day)
69 && self.hour < 24
70 && self.minute < 60
71 && self.second < 61
72 }
73
74 fn weekday(&self) -> usize {
76 let (y, m, d) = (
77 i64::from(self.year),
78 i64::from(self.month),
79 i64::from(self.day),
80 );
81 let y = if m <= 2 { y - 1 } else { y };
82 let era = y.div_euclid(400);
83 let yoe = y - era * 400;
84 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1;
85 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
86 let days = era * 146_097 + doe - 719_468;
87 (days + 4).rem_euclid(7) as usize
88 }
89}
90
91const MONTHS: [&str; 12] = [
92 "January",
93 "February",
94 "March",
95 "April",
96 "May",
97 "June",
98 "July",
99 "August",
100 "September",
101 "October",
102 "November",
103 "December",
104];
105const DAYS: [&str; 7] = [
106 "Sunday",
107 "Monday",
108 "Tuesday",
109 "Wednesday",
110 "Thursday",
111 "Friday",
112 "Saturday",
113];
114
115pub fn is_date_field(kind: &str) -> bool {
117 kind.starts_with("datetime")
118}
119
120pub fn field_text(kind: &str, lang: Option<&str>, now: DateTime) -> Option<String> {
124 if !now.is_valid() {
125 return None;
126 }
127 let lang = lang
128 .map(str::trim)
129 .filter(|l| !l.is_empty())
130 .unwrap_or("en-US")
131 .to_ascii_lowercase();
132 let english = lang == "en" || lang.starts_with("en-");
133 let month = MONTHS.get(now.month.checked_sub(1)? as usize)?;
134 let mon = month.get(..3)?;
135 let day = DAYS.get(now.weekday())?;
136 let (d, y) = (now.day, now.year);
137 let yy = y.rem_euclid(100);
138 let h12 = match now.hour % 12 {
139 0 => 12,
140 h => h,
141 };
142 let ampm = if now.hour < 12 { "AM" } else { "PM" };
143 let (mi, s) = (now.minute, now.second);
144 let short = short_date(&lang, now);
145 let named = |text: String| english.then_some(text);
146 match kind {
147 "datetime" | "datetimeFigureOut" | "datetime1" => Some(short),
148 "datetime2" => named(format!("{day}, {month} {d}, {y}")),
149 "datetime3" => named(format!("{d} {month} {y}")),
150 "datetime4" => named(format!("{month} {d}, {y}")),
151 "datetime5" => named(format!("{d}-{mon}-{yy:02}")),
152 "datetime6" => named(format!("{month} {yy:02}")),
153 "datetime7" => named(format!("{mon}-{yy:02}")),
154 "datetime8" => named(format!("{short} {h12}:{mi:02} {ampm}")),
155 "datetime9" => named(format!("{short} {h12}:{mi:02}:{s:02} {ampm}")),
156 "datetime10" => Some(format!("{}:{mi:02}", now.hour)),
157 "datetime11" => Some(format!("{}:{mi:02}:{s:02}", now.hour)),
158 "datetime12" => named(format!("{h12}:{mi:02} {ampm}")),
159 "datetime13" => named(format!("{h12}:{mi:02}:{s:02} {ampm}")),
160 _ => None,
161 }
162}
163
164fn short_date(lang: &str, t: DateTime) -> String {
167 let (y, m, d) = (t.year, t.month, t.day);
168 let primary = lang.split(['-', '_']).next().unwrap_or("");
169 match lang {
170 "en-gb" | "en-au" | "en-nz" | "en-ie" | "en-in" => return format!("{d:02}/{m:02}/{y}"),
171 "en-ca" | "fr-ca" | "en-za" => return format!("{y}-{m:02}-{d:02}"),
172 _ => {}
173 }
174 match primary {
175 "fr" | "es" | "it" | "pt" | "el" | "ca" | "vi" => format!("{d:02}/{m:02}/{y}"),
176 "de" | "ru" | "pl" | "cs" | "sk" | "tr" | "fi" | "nb" | "nn" | "no" | "da" | "uk"
177 | "ro" => format!("{d:02}.{m:02}.{y}"),
178 "nl" => format!("{d}-{m}-{y}"),
179 "ja" | "zh" => format!("{y}/{m}/{d}"),
180 "ko" | "sv" | "lt" => format!("{y}-{m:02}-{d:02}"),
181 _ => format!("{m}/{d}/{y}"),
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 const FRI: DateTime = DateTime {
190 year: 2026,
191 month: 10,
192 day: 9,
193 hour: 20,
194 minute: 5,
195 second: 7,
196 };
197
198 #[test]
199 fn english_formats_of_every_type() {
200 let f = |k: &str| field_text(k, Some("en-US"), FRI);
201 assert_eq!(f("datetime1").as_deref(), Some("10/9/2026"));
202 assert_eq!(f("datetime2").as_deref(), Some("Friday, October 9, 2026"));
203 assert_eq!(f("datetime3").as_deref(), Some("9 October 2026"));
204 assert_eq!(f("datetime4").as_deref(), Some("October 9, 2026"));
205 assert_eq!(f("datetime5").as_deref(), Some("9-Oct-26"));
206 assert_eq!(f("datetime6").as_deref(), Some("October 26"));
207 assert_eq!(f("datetime7").as_deref(), Some("Oct-26"));
208 assert_eq!(f("datetime8").as_deref(), Some("10/9/2026 8:05 PM"));
209 assert_eq!(f("datetime9").as_deref(), Some("10/9/2026 8:05:07 PM"));
210 assert_eq!(f("datetime10").as_deref(), Some("20:05"));
211 assert_eq!(f("datetime11").as_deref(), Some("20:05:07"));
212 assert_eq!(f("datetime12").as_deref(), Some("8:05 PM"));
213 assert_eq!(f("datetime13").as_deref(), Some("8:05:07 PM"));
214 assert_eq!(f("datetimeFigureOut").as_deref(), Some("10/9/2026"));
215 assert_eq!(f("datetime").as_deref(), Some("10/9/2026"));
216 assert_eq!(
218 field_text("datetime12", None, DateTime::new(2024, 2, 29, 0, 0, 0)).as_deref(),
219 Some("12:00 AM")
220 );
221 assert_eq!(
222 field_text("datetime12", None, DateTime::new(2024, 2, 29, 12, 30, 0)).as_deref(),
223 Some("12:30 PM")
224 );
225 assert_eq!(
226 field_text("datetime2", None, DateTime::new(2000, 1, 1, 0, 0, 0)).as_deref(),
227 Some("Saturday, January 1, 2000")
228 );
229 }
230
231 #[test]
232 fn language_orders_numeric_dates_and_keeps_names_english_only() {
233 assert_eq!(
234 field_text("datetime1", Some("en-GB"), FRI).as_deref(),
235 Some("09/10/2026")
236 );
237 assert_eq!(
238 field_text("datetimeFigureOut", Some("de-DE"), FRI).as_deref(),
239 Some("09.10.2026")
240 );
241 assert_eq!(
242 field_text("datetime1", Some("ja-JP"), FRI).as_deref(),
243 Some("2026/10/9")
244 );
245 assert_eq!(
246 field_text("datetime1", Some("sv-SE"), FRI).as_deref(),
247 Some("2026-10-09")
248 );
249 assert_eq!(
250 field_text("datetime10", Some("fr-FR"), FRI).as_deref(),
251 Some("20:05")
252 );
253 assert_eq!(field_text("datetime2", Some("de-DE"), FRI), None);
255 assert_eq!(field_text("datetime12", Some("fr-FR"), FRI), None);
256 assert_eq!(
257 field_text("datetime4", Some("en-GB"), FRI).as_deref(),
258 Some("October 9, 2026")
259 );
260 }
261
262 #[test]
263 fn unknown_types_and_impossible_dates_give_nothing() {
264 assert_eq!(field_text("slidenum", None, FRI), None);
265 assert_eq!(field_text("datetime14", None, FRI), None);
266 for bad in [
267 DateTime::new(2026, 0, 9, 0, 0, 0),
268 DateTime::new(2026, 13, 9, 0, 0, 0),
269 DateTime::new(2026, 2, 29, 0, 0, 0),
270 DateTime::new(2026, 4, 31, 0, 0, 0),
271 DateTime::new(2026, 1, 1, 24, 0, 0),
272 DateTime::new(i32::MIN, 1, 1, 0, 0, 0),
273 DateTime::new(i32::MAX, u32::MAX, u32::MAX, u32::MAX, u32::MAX, u32::MAX),
274 ] {
275 assert_eq!(field_text("datetime2", None, bad), None, "{bad:?}");
276 }
277 assert!(is_date_field("datetimeFigureOut") && !is_date_field("slidenum"));
278 }
279
280 #[test]
281 fn the_clock_reads_a_real_date() {
282 if let Some(now) = DateTime::now_local() {
283 assert!(field_text("datetime1", None, now).is_some(), "{now:?}");
284 }
285 }
286}