Skip to main content

teksilo_widgets/common/datetime/
types.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Stable type aliases for the date/time types used across the four
5//! datetime widgets. Routes through `jiff` today; swap-friendly because
6//! the widgets never name `jiff` directly.
7
8pub use jiff::civil::{Date, DateTime, Time, Weekday};
9
10/// Year + month pair, with no day component. Used by `Calendar` to track
11/// the visible month independently of the current selection.
12///
13/// Stored as a `Date` anchored on the first of the month; the widget
14/// never reads the day field.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct YearMonth {
17    year: i16,
18    month: i8,
19}
20
21impl YearMonth {
22    /// Construct a `YearMonth`. Year clamps to jiff's `-9999..=9999`
23    /// range; month clamps to `1..=12`.
24    pub fn new(year: i16, month: i8) -> Self {
25        let year = year.clamp(-9999, 9999);
26        let month = month.clamp(1, 12);
27        Self { year, month }
28    }
29
30    /// Year as i16. Range matches jiff: `-9999..=9999`.
31    pub fn year(self) -> i16 {
32        self.year
33    }
34
35    /// Month as i8 in `1..=12`.
36    pub fn month(self) -> i8 {
37        self.month
38    }
39
40    /// First day of this month as a `Date`.
41    pub fn first_day(self) -> Date {
42        Date::constant(self.year, self.month, 1)
43    }
44
45    /// Last day of this month as a `Date`.
46    pub fn last_day(self) -> Date {
47        self.first_day().last_of_month()
48    }
49
50    /// `YearMonth` containing the given date.
51    pub fn from_date(d: Date) -> Self {
52        Self {
53            year: d.year(),
54            month: d.month(),
55        }
56    }
57
58    /// The next calendar month (wraps year on December → January).
59    pub fn next_month(self) -> Self {
60        if self.month == 12 {
61            Self::new(self.year.saturating_add(1), 1)
62        } else {
63            Self::new(self.year, self.month + 1)
64        }
65    }
66
67    /// The previous calendar month (wraps year on January → December).
68    pub fn prev_month(self) -> Self {
69        if self.month == 1 {
70            Self::new(self.year.saturating_sub(1), 12)
71        } else {
72            Self::new(self.year, self.month - 1)
73        }
74    }
75
76    /// `n` months later (negative for earlier).
77    pub fn offset_months(self, n: i32) -> Self {
78        let total = self.year as i32 * 12 + (self.month as i32 - 1) + n;
79        let year = (total.div_euclid(12)).clamp(-9999, 9999) as i16;
80        let month = (total.rem_euclid(12) + 1) as i8;
81        Self::new(year, month)
82    }
83}
84
85/// `Weekday::from_monday_zero_offset` clamped to `0..=6` and
86/// infallible — the rest of the date code only ever traffics in valid
87/// offsets, and a panic here would point at a bug, not a user error.
88pub fn weekday_from_monday_zero(offset: i8) -> Weekday {
89    Weekday::from_monday_zero_offset(offset.rem_euclid(7))
90        .expect("monday-zero offset already in 0..=6")
91}
92
93/// Today's date in the system's local time zone.
94///
95/// Single source of truth for "today" across the calendar / date
96/// widgets — used by today-ring rendering, the today-button, the `T`
97/// keyboard shortcut, and any default-when-no-value selection. Falls
98/// back to a fixed sentinel only if the platform refuses to give us a
99/// local zone (jiff returns `Zoned::now` infallibly on Linux/macOS/
100/// Windows; other platforms with broken tzdb fall back to UTC then to
101/// the sentinel).
102pub fn today_local() -> Date {
103    jiff::Zoned::now().date()
104}