Skip to main content

deep_time/ymdhms/
mod.rs

1use crate::{ATTOS_PER_SEC_I128, Dt, Scale};
2
3mod to_str;
4
5/// Combined Gregorian date + wall time with subsecond precision.
6///
7/// Has some basic calendar aware math, but not time zone aware.
8///
9/// ## Examples
10///
11/// **Creating a** [`YmdHms`].
12///
13/// ```rust
14/// use deep_time::{Dt, Scale};
15///
16/// // clamped to 29
17/// let x = Dt::from_ymd(2000, 2, 30, 0, 0, 0, 0, Scale::UTC).to_ymd();
18///
19/// assert_eq!(x.day(), 29);
20/// ```
21///
22/// **Adding a year.** 2000 is a leap year and Feb. 29th is possible, but
23/// 2001 isn't a leap year so the day is clamped to the 28th.
24///
25/// ```rust
26/// use deep_time::{Dt, Scale};
27///
28/// let x = Dt::from_ymd(2000, 2, 29, 0, 0, 0, 0, Scale::UTC).to_ymd();
29/// let x = x.add_yr(1);
30///
31/// assert_eq!(x.day(), 28);
32/// ```
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[cfg_attr(feature = "js", derive(tsify::Tsify))]
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct YmdHms {
37    pub(crate) yr: i64,
38    pub(crate) mo: u8,
39    pub(crate) day: u8,
40    pub(crate) hr: u8,
41    pub(crate) min: u8,
42    pub(crate) sec: u8,    // 0–60 (60 only during leap seconds)
43    pub(crate) attos: u64, // attoseconds (0 ≤ subsec < 10¹⁸)
44    pub(crate) scale: Scale,
45}
46
47impl YmdHms {
48    /// Reconstructs a [`Dt`].
49    #[inline]
50    pub fn to_dt(&self) -> Dt {
51        Dt::from_ymd(
52            self.yr, self.mo, self.day, self.hr, self.min, self.sec, self.attos, self.scale,
53        )
54    }
55
56    #[inline(always)]
57    fn reconstruct(
58        yr: i64,
59        mo: u8,
60        day: u8,
61        hr: u8,
62        min: u8,
63        sec: u8,
64        attos: u64,
65        scale: Scale,
66    ) -> Self {
67        Dt::from_ymd(yr, mo, day, hr, min, sec, attos, scale).to_ymd()
68    }
69
70    /// Adds (or subtracts) whole years, preserving month and day-of-month.
71    /// Negative values subtract years. Uses standard last-day-of-month clamping.
72    pub fn add_yr(&self, years: i64) -> Self {
73        if years == 0 {
74            return *self;
75        }
76        let new_yr = self.yr.saturating_add(years);
77        let max_day = Dt::days_in_month(new_yr, self.mo);
78        let new_day = Dt::clamp_u8(self.day, 1, max_day);
79        Self::reconstruct(
80            new_yr, self.mo, new_day, self.hr, self.min, self.sec, self.attos, self.scale,
81        )
82    }
83
84    /// Adds (or subtracts) whole months. Negative values subtract months.
85    /// Uses `i128` total-month arithmetic to avoid overflow at extreme years.
86    pub fn add_mo(&self, months: i64) -> Self {
87        if months == 0 {
88            return *self;
89        }
90        let yr = self.yr as i128;
91        let mo = self.mo as i128;
92        let delta = months as i128;
93
94        let total_months = yr * 12 + (mo - 1) + delta;
95
96        let new_yr = Dt::i128_to_i64(total_months.div_euclid(12));
97        let new_mo = Dt::clamp_u8((total_months.rem_euclid(12) + 1) as u8, 1, 12);
98
99        let max_day = Dt::days_in_month(new_yr, new_mo);
100        let new_day = Dt::clamp_u8(self.day, 1, max_day);
101
102        Self::reconstruct(
103            new_yr, new_mo, new_day, self.hr, self.min, self.sec, self.attos, self.scale,
104        )
105    }
106
107    /// Adds (or subtracts) calendar days using Julian Day arithmetic.
108    /// Negative values subtract days.
109    pub fn add_days(&self, days: i64) -> Self {
110        if days == 0 {
111            return *self;
112        }
113        let jd = Dt::ymd_to_jd(self.yr, self.mo, self.day);
114        let new_jd = jd.saturating_add(days);
115        let (new_yr, new_mo, new_day) = Dt::jd_to_ymd(new_jd);
116        Self::reconstruct(
117            new_yr, new_mo, new_day, self.hr, self.min, self.sec, self.attos, self.scale,
118        )
119    }
120
121    #[inline]
122    pub fn add_wk(&self, weeks: i64) -> Self {
123        self.add_days(weeks.saturating_mul(7))
124    }
125
126    #[inline(never)]
127    fn _add_attos(&self, attos_delta: i128) -> Self {
128        let tai = Dt::from_ymd(
129            self.yr, self.mo, self.day, self.hr, self.min, self.sec, self.attos, self.scale,
130        );
131        let new_tai = tai.add(Dt::span(attos_delta));
132        new_tai.to_ymd()
133    }
134
135    #[inline]
136    pub fn add_attos(&self, n: i128) -> Self {
137        self._add_attos(n)
138    }
139
140    #[inline]
141    pub fn add_sec(&self, n: i64) -> Self {
142        self._add_attos((n as i128).saturating_mul(ATTOS_PER_SEC_I128))
143    }
144
145    #[inline]
146    pub fn add_min(&self, n: i64) -> Self {
147        let delta = (n as i128)
148            .saturating_mul(60)
149            .saturating_mul(ATTOS_PER_SEC_I128);
150        self._add_attos(delta)
151    }
152
153    #[inline]
154    pub fn add_hr(&self, n: i64) -> Self {
155        let delta = (n as i128)
156            .saturating_mul(3600)
157            .saturating_mul(ATTOS_PER_SEC_I128);
158        self._add_attos(delta)
159    }
160
161    #[inline]
162    pub fn yr(&self) -> i64 {
163        self.yr
164    }
165
166    #[inline]
167    pub fn mo(&self) -> u8 {
168        self.mo
169    }
170
171    #[inline]
172    pub fn day(&self) -> u8 {
173        self.day
174    }
175
176    #[inline]
177    pub fn hr(&self) -> u8 {
178        self.hr
179    }
180
181    #[inline]
182    pub fn min(&self) -> u8 {
183        self.min
184    }
185
186    #[inline]
187    pub fn sec(&self) -> u8 {
188        self.sec
189    }
190
191    #[inline]
192    pub fn attos(&self) -> u64 {
193        self.attos
194    }
195
196    /// The time scale that the object was created on.
197    #[inline]
198    pub fn scale(&self) -> Scale {
199        self.scale
200    }
201
202    #[inline]
203    pub fn iso_yr(&self) -> i64 {
204        let (iso_yr, _, _) = Dt::_to_iso_wk_date(self.yr, self.mo, self.day);
205        iso_yr
206    }
207
208    #[inline]
209    pub fn iso_wk(&self) -> u8 {
210        let (_, iso_wk, _) = Dt::_to_iso_wk_date(self.yr, self.mo, self.day);
211        iso_wk
212    }
213
214    #[inline]
215    pub fn day_of_yr(&self) -> u16 {
216        Dt::_day_of_yr(self.yr, self.mo, self.day)
217    }
218
219    #[inline]
220    pub fn wkday(&self) -> u8 {
221        let jd = Dt::ymd_to_jd(self.yr, self.mo, self.day);
222        Dt::jd_to_wkday(jd)
223    }
224
225    #[inline]
226    pub fn wk_of_yr_sun(&self) -> u8 {
227        Dt::_wk_sun(self.yr, self.day_of_yr())
228    }
229
230    #[inline]
231    pub fn wk_of_yr_mon(&self) -> u8 {
232        Dt::_wk_mon(self.yr, self.day_of_yr())
233    }
234}