Skip to main content

orbital_date_pickers/shared/
clock_time.rs

1use orbital_base_components::{DatetimeTimezone, OrbitalDateTime};
2
3/// Calendar day anchor for time-only clock surfaces.
4pub fn resolve_anchor(
5    value: Option<OrbitalDateTime>,
6    reference_date: OrbitalDateTime,
7    timezone: DatetimeTimezone,
8) -> OrbitalDateTime {
9    let base = value.unwrap_or(reference_date);
10    OrbitalDateTime::from_instant(base.instant(), timezone).start_of_day()
11}
12
13/// Snap a minute value to the nearest step increment (minimum step 1).
14pub fn snap_minute(minute: u32, step: u32) -> u32 {
15    let step = step.max(1);
16    let snapped = ((minute + step / 2) / step) * step;
17    snapped.min(59)
18}
19
20/// Apply hour/minute/second to an anchor day in the given timezone.
21pub fn commit_time(
22    anchor: OrbitalDateTime,
23    hour: u32,
24    minute: u32,
25    second: u32,
26    timezone: DatetimeTimezone,
27) -> Option<OrbitalDateTime> {
28    let anchor = OrbitalDateTime::from_instant(anchor.instant(), timezone).start_of_day();
29    anchor.apply_hms(hour, minute, second)
30}
31
32/// Generate `(hour, minute)` slots for a full day at the given step.
33pub fn generate_time_slots(step_minutes: u32) -> Vec<(u32, u32)> {
34    let step = step_minutes.max(1);
35    let mut slots = Vec::new();
36    let mut total = 0u32;
37    while total < 24 * 60 {
38        slots.push((total / 60, total % 60));
39        total += step;
40    }
41    slots
42}
43
44/// Format a time slot label for list display.
45pub fn format_slot_label(hour: u32, minute: u32, ampm: bool) -> String {
46    if ampm {
47        let (display_hour, period) = to_twelve_hour(hour);
48        format!("{display_hour}:{minute:02} {period}")
49    } else {
50        format!("{hour:02}:{minute:02}")
51    }
52}
53
54/// Convert 24h hour to (12h display, AM/PM).
55pub fn to_twelve_hour(hour: u32) -> (u32, &'static str) {
56    let period = if hour >= 12 { "PM" } else { "AM" };
57    let h = hour % 12;
58    let display = if h == 0 { 12 } else { h };
59    (display, period)
60}
61
62/// Convert 12h display hour + meridiem to 24h hour.
63pub fn to_twenty_four_hour(display_hour: u32, is_pm: bool) -> u32 {
64    let h = display_hour % 12;
65    match (h, is_pm) {
66        (0, false) => 0,
67        (0, true) => 12,
68        (hour, false) => hour,
69        (hour, true) => hour + 12,
70    }
71}
72
73/// Hour markers for the analog dial (12 at top for 12-hour mode).
74pub fn hour_markers(ampm: bool) -> Vec<u32> {
75    if ampm {
76        std::iter::once(12).chain(1..=11).collect()
77    } else {
78        (0..24).collect()
79    }
80}
81
82/// Minute markers at the given step increment.
83pub fn minute_markers(step: u32) -> Vec<u32> {
84    let step = step.max(1);
85    (0..60).step_by(step as usize).collect()
86}
87
88/// Current wall-clock time on the anchor day as `(hour, minute, is_pm)`.
89pub fn now_on_anchor(anchor: OrbitalDateTime) -> (u32, u32, bool) {
90    let now = OrbitalDateTime::utc_now(anchor.timezone());
91    let (hour, minute, _) = now.hour_minute_second().unwrap_or((0, 0, 0));
92    (hour, minute, hour >= 12)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use orbital_base_components::{DatetimeTimezone, TryFromUnixSeconds};
99
100    fn sample_ref() -> OrbitalDateTime {
101        OrbitalDateTime::try_from_unix_seconds(1_735_689_600, DatetimeTimezone::Local)
102            .expect("valid reference")
103    }
104
105    #[test]
106    fn snap_minute_rounds_to_step() {
107        assert_eq!(snap_minute(7, 5), 5);
108        assert_eq!(snap_minute(8, 5), 10);
109        assert_eq!(snap_minute(0, 0), 0);
110    }
111
112    #[test]
113    fn generate_time_slots_respects_step() {
114        let slots = generate_time_slots(30);
115        assert_eq!(slots.first(), Some(&(0, 0)));
116        assert!(slots.contains(&(9, 30)));
117        assert_eq!(slots.last(), Some(&(23, 30)));
118    }
119
120    #[test]
121    fn commit_time_applies_hms() {
122        let anchor = sample_ref().start_of_day();
123        let committed = commit_time(anchor, 14, 30, 0, DatetimeTimezone::Local).expect("commit");
124        assert_eq!(committed.hour_minute_second(), Some((14, 30, 0)));
125    }
126
127    #[test]
128    fn resolve_anchor_uses_value_when_present() {
129        let anchor = sample_ref().start_of_day();
130        let with_time = commit_time(anchor, 9, 15, 0, DatetimeTimezone::Local).expect("time");
131        let resolved = resolve_anchor(Some(with_time), sample_ref(), DatetimeTimezone::Local);
132        assert_eq!(resolved.start_of_day(), anchor);
133    }
134
135    #[test]
136    fn to_twelve_hour_converts_correctly() {
137        assert_eq!(to_twelve_hour(0), (12, "AM"));
138        assert_eq!(to_twelve_hour(12), (12, "PM"));
139        assert_eq!(to_twelve_hour(15), (3, "PM"));
140    }
141
142    #[test]
143    fn to_twenty_four_hour_converts_correctly() {
144        assert_eq!(to_twenty_four_hour(12, false), 0);
145        assert_eq!(to_twenty_four_hour(12, true), 12);
146        assert_eq!(to_twenty_four_hour(3, true), 15);
147    }
148}