| 1 | //! When the person asking's day began, in their own time zone (their |
| 2 | //! profile's IANA name, such as `America/Denver`): what "today" means in a |
| 3 | //! question to Ask. The runtime's `Intl` says the wall-clock time there; |
| 4 | //! the rest is arithmetic, tested on its own. |
| 5 | |
| 6 | use g1t_contracts::time::{parse_rfc3339, rfc3339}; |
| 7 | |
| 8 | /// Their day: its date, and when it began as UTC milliseconds. |
| 9 | #[derive(Debug, PartialEq, Eq)] |
| 10 | pub struct Day { |
| 11 | pub date: String, |
| 12 | pub started: u64, |
| 13 | } |
| 14 | |
| 15 | /// The day in `zone` at `now`, or none when the zone is unknown. |
| 16 | pub fn today(zone: &str, now: u64) -> Option<Day> { |
| 17 | from_wallclock(&wallclock(zone, now)?, now) |
| 18 | } |
| 19 | |
| 20 | /// From the wall-clock time in the zone at `now`, written |
| 21 | /// `MM/DD/YYYY, HH:MM:SS` (en-US, 24-hour), the day it is there and when it |
| 22 | /// began. |
| 23 | pub fn from_wallclock(text: &str, now: u64) -> Option<Day> { |
| 24 | let (date, time) = text.split_once(", ")?; |
| 25 | let mut day = date.split('/'); |
| 26 | let (month, day_of, year) = (day.next()?, day.next()?, day.next()?); |
| 27 | let date = format!("{year:0>4}-{month:0>2}-{day_of:0>2}"); |
| 28 | let wall = parse_rfc3339(&format!("{date}T{}Z", time.trim()))?; |
| 29 | // The zone's offset from UTC, to the minute. |
| 30 | let offset = (wall as i64 - now as i64) / 60_000 * 60_000; |
| 31 | let midnight = parse_rfc3339(&format!("{date}T00:00:00Z"))? as i64; |
| 32 | let started = u64::try_from(midnight - offset).ok()?; |
| 33 | Some(Day { date, started }) |
| 34 | } |
| 35 | |
| 36 | /// The day's start as RFC 3339, to the second. |
| 37 | pub fn started_at(day: &Day) -> String { |
| 38 | format!("{}Z", rfc3339(day.started).chars().take(19).collect::<String>()) |
| 39 | } |
| 40 | |
| 41 | #[cfg(target_arch = "wasm32")] |
| 42 | fn wallclock(zone: &str, now: u64) -> Option<String> { |
| 43 | use worker::js_sys::{Array, Date, Function, Object, Reflect, global}; |
| 44 | use worker::wasm_bindgen::{JsCast, JsValue}; |
| 45 | let options = Object::new(); |
| 46 | for (key, value) in [ |
| 47 | ("timeZone", zone), |
| 48 | ("hourCycle", "h23"), |
| 49 | ("year", "numeric"), |
| 50 | ("month", "2-digit"), |
| 51 | ("day", "2-digit"), |
| 52 | ("hour", "2-digit"), |
| 53 | ("minute", "2-digit"), |
| 54 | ("second", "2-digit"), |
| 55 | ] { |
| 56 | Reflect::set(&options, &key.into(), &value.into()).ok()?; |
| 57 | } |
| 58 | // Constructed through Reflect so an unknown zone is an error, not a trap. |
| 59 | let intl = Reflect::get(&global(), &"Intl".into()).ok()?; |
| 60 | let class: Function = Reflect::get(&intl, &"DateTimeFormat".into()).ok()?.dyn_into().ok()?; |
| 61 | let formatter = Reflect::construct(&class, &Array::of2(&"en-US".into(), &options)).ok()?; |
| 62 | let format: Function = Reflect::get(&formatter, &"format".into()).ok()?.dyn_into().ok()?; |
| 63 | format.call1(&formatter, &Date::new(&JsValue::from_f64(now as f64))).ok()?.as_string() |
| 64 | } |
| 65 | |
| 66 | #[cfg(not(target_arch = "wasm32"))] |
| 67 | fn wallclock(_zone: &str, _now: u64) -> Option<String> { |
| 68 | None |
| 69 | } |
| 70 | |
| 71 | #[cfg(test)] |
| 72 | mod tests { |
| 73 | use super::*; |
| 74 | |
| 75 | #[test] |
| 76 | fn a_day_begins_at_local_midnight() { |
| 77 | // 05:20 UTC on the 13th is 23:20 on the 12th in Denver (UTC−6). |
| 78 | let now = parse_rfc3339("2026-10-13T05:20:00Z").unwrap(); |
| 79 | let day = from_wallclock("10/12/2026, 23:20:00", now).unwrap(); |
| 80 | assert_eq!(day.date, "2026-10-12"); |
| 81 | assert_eq!(started_at(&day), "2026-10-12T06:00:00Z"); |
| 82 | // East of UTC: 05:20 UTC is 14:20 in Tokyo (UTC+9), whose day began at 15:00 UTC the day before. |
| 83 | let day = from_wallclock("10/13/2026, 14:20:00", now).unwrap(); |
| 84 | assert_eq!((day.date.as_str(), started_at(&day).as_str()), ("2026-10-13", "2026-10-12T15:00:00Z")); |
| 85 | // Half-hour zones keep their minutes: India (UTC+5:30). |
| 86 | let day = from_wallclock("10/13/2026, 10:50:00", now).unwrap(); |
| 87 | assert_eq!(started_at(&day), "2026-10-12T18:30:00Z"); |
| 88 | assert!(from_wallclock("nonsense", now).is_none()); |
| 89 | } |
| 90 | } |