Skip to main content

deep_time/ymdhms/
mod.rs

1use crate::{ATTOS_PER_SEC_I128, Dt, LiteStr, Scale, Weekday};
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/// ```
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/// ```
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) unix_attosec: i128,
45    pub(crate) scale: Scale,
46}
47
48impl YmdHms {
49    /// Reconstructs a [`Dt`].
50    #[inline]
51    pub fn to_dt(&self) -> Dt {
52        Dt::from_ymd(
53            self.yr, self.mo, self.day, self.hr, self.min, self.sec, self.attos, self.scale,
54        )
55    }
56
57    #[inline(always)]
58    fn reconstruct(
59        yr: i64,
60        mo: u8,
61        day: u8,
62        hr: u8,
63        min: u8,
64        sec: u8,
65        attos: u64,
66        scale: Scale,
67    ) -> Self {
68        Dt::from_ymd(yr, mo, day, hr, min, sec, attos, scale).to_ymd()
69    }
70
71    /// Adds (or subtracts) whole years, preserving month and day-of-month.
72    /// Negative values subtract years. Uses standard last-day-of-month clamping.
73    pub fn add_yr(&self, years: i64) -> Self {
74        if years == 0 {
75            return *self;
76        }
77        let new_yr = self.yr.saturating_add(years);
78        let max_day = Dt::days_in_month(new_yr, self.mo);
79        let new_day = Dt::clamp_u8(self.day, 1, max_day);
80        Self::reconstruct(
81            new_yr, self.mo, new_day, self.hr, self.min, self.sec, self.attos, self.scale,
82        )
83    }
84
85    /// Adds (or subtracts) whole months. Negative values subtract months.
86    /// Uses `i128` total-month arithmetic to avoid overflow at extreme years.
87    pub fn add_mo(&self, months: i64) -> Self {
88        if months == 0 {
89            return *self;
90        }
91        let yr = self.yr as i128;
92        let mo = self.mo as i128;
93        let delta = months as i128;
94
95        let total_months = yr * 12 + (mo - 1) + delta;
96
97        let new_yr = Dt::i128_to_i64(total_months.div_euclid(12));
98        let new_mo = Dt::clamp_u8((total_months.rem_euclid(12) + 1) as u8, 1, 12);
99
100        let max_day = Dt::days_in_month(new_yr, new_mo);
101        let new_day = Dt::clamp_u8(self.day, 1, max_day);
102
103        Self::reconstruct(
104            new_yr, new_mo, new_day, self.hr, self.min, self.sec, self.attos, self.scale,
105        )
106    }
107
108    /// Adds (or subtracts) calendar days using Julian Day arithmetic.
109    /// Negative values subtract days.
110    pub fn add_days(&self, days: i64) -> Self {
111        if days == 0 {
112            return *self;
113        }
114        let jd = Dt::ymd_to_jd(self.yr, self.mo, self.day);
115        let new_jd = jd.saturating_add(days);
116        let (new_yr, new_mo, new_day) = Dt::jd_to_ymd(new_jd);
117        Self::reconstruct(
118            new_yr, new_mo, new_day, self.hr, self.min, self.sec, self.attos, self.scale,
119        )
120    }
121
122    #[inline]
123    pub fn add_wk(&self, weeks: i64) -> Self {
124        self.add_days(weeks.saturating_mul(7))
125    }
126
127    #[inline(never)]
128    fn _add_attos(&self, attos_delta: i128) -> Self {
129        let tai = Dt::from_ymd(
130            self.yr, self.mo, self.day, self.hr, self.min, self.sec, self.attos, self.scale,
131        );
132        let new_tai = tai.add(Dt::span(attos_delta));
133        new_tai.to_ymd()
134    }
135
136    #[inline]
137    pub fn add_attos(&self, attos: i128) -> Self {
138        self._add_attos(attos)
139    }
140
141    #[inline]
142    pub fn add_sec(&self, sec: i64) -> Self {
143        self._add_attos(sec as i128 * ATTOS_PER_SEC_I128)
144    }
145
146    #[inline]
147    pub fn add_min(&self, min: i64) -> Self {
148        self._add_attos(min as i128 * 60 * ATTOS_PER_SEC_I128)
149    }
150
151    #[inline]
152    pub fn add_hr(&self, hr: i64) -> Self {
153        self._add_attos(hr as i128 * 3600 * ATTOS_PER_SEC_I128)
154    }
155
156    #[inline]
157    pub fn yr(&self) -> i64 {
158        self.yr
159    }
160
161    #[inline]
162    pub fn mo(&self) -> u8 {
163        self.mo
164    }
165
166    #[inline]
167    pub fn day(&self) -> u8 {
168        self.day
169    }
170
171    #[inline]
172    pub fn hr(&self) -> u8 {
173        self.hr
174    }
175
176    #[inline]
177    pub fn min(&self) -> u8 {
178        self.min
179    }
180
181    #[inline]
182    pub fn sec(&self) -> u8 {
183        self.sec
184    }
185
186    #[inline]
187    pub fn attos(&self) -> u64 {
188        self.attos
189    }
190
191    /// Attoseconds since 1970-01-01 midnight, on whatever time scale
192    /// the object was created on.
193    #[inline]
194    pub fn unix_attosec(&self) -> i128 {
195        self.unix_attosec
196    }
197
198    /// The time scale that the object was created on.
199    #[inline]
200    pub fn scale(&self) -> Scale {
201        self.scale
202    }
203
204    pub(crate) fn to_ymd_rich(
205        &self,
206        iso_yr: i64,
207        iso_wk: u8,
208        iso_wkday: Weekday,
209        day_of_yr: u16,
210        wkday: u8,
211        wk_of_yr_sun: u8,
212        wk_of_yr_mon: u8,
213    ) -> YmdHmsRich {
214        YmdHmsRich::new(
215            self.unix_attosec,
216            self.yr,
217            self.mo,
218            self.day,
219            self.hr,
220            self.min,
221            self.sec,
222            self.attos,
223            iso_yr,
224            iso_wk,
225            iso_wkday,
226            day_of_yr,
227            wkday,
228            wk_of_yr_sun,
229            wk_of_yr_mon,
230            self.scale,
231        )
232    }
233}
234
235/// Gregorian calendar and time-of-day components of a [`Dt`].
236#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
237#[cfg_attr(feature = "js", derive(tsify::Tsify))]
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub struct YmdHmsRich {
240    /// UNIX attoseconds counting from 1970 epoch
241    pub(crate) unix_attosec: i128,
242    /// Gregorian year (proleptic Gregorian calendar, supports negative years and year 0).
243    pub(crate) yr: i64,
244    /// Gregorian month in the range [1, 12].
245    pub(crate) mo: u8,
246    /// Gregorian day of the month in the range [1, 31].
247    pub(crate) day: u8,
248    /// Hour of the day in the range [0, 23].
249    pub(crate) hr: u8,
250    /// Minute in the range [0, 59].
251    pub(crate) min: u8,
252    /// Second in the range [0, 60] (60 only during UTC leap seconds).
253    pub(crate) sec: u8,
254    /// Fractional part of the second expressed in attoseconds (u64).
255    pub(crate) attos: u64,
256    /// ISO 8601 week year.
257    pub(crate) iso_yr: i64,
258    /// ISO 8601 week number in the range [1, 53].
259    pub(crate) iso_wk: u8,
260    /// ISO 8601 weekday enum e.g. Monday/Tuesday/...
261    pub(crate) iso_wkday: Weekday,
262    /// Ordinal day of the year (1-based).
263    pub(crate) day_of_yr: u16,
264    /// Weekday number (0 = Sunday … 6 = Saturday).
265    pub(crate) wkday: u8,
266    /// Sunday based week of year (Range: `0..=53`).
267    pub(crate) wk_of_yr_sun: u8,
268    /// Monday based week of year (Range: `0..=53`).
269    pub(crate) wk_of_yr_mon: u8,
270    /// Used for formatting (strftime).
271    /// A stored offset in seconds, used within the crate.
272    pub(crate) offset_sec: Option<i32>,
273    /// A stored IANA name, used within the crate, %Q.
274    pub(crate) tz: Option<LiteStr<49>>,
275    /// UTC, EST, %Z
276    pub(crate) tz_abbrev: Option<LiteStr<49>>,
277    /// Scale the instance was created on
278    pub(crate) scale: Scale,
279}
280
281impl YmdHmsRich {
282    /// Creates a new [`YmdHmsRich`] with all fields specified.
283    #[inline]
284    pub(crate) const fn new(
285        unix_attosec: i128,
286        yr: i64,
287        mo: u8,
288        day: u8,
289        hr: u8,
290        min: u8,
291        sec: u8,
292        attos: u64,
293        iso_yr: i64,
294        iso_wk: u8,
295        iso_wkday: Weekday,
296        day_of_yr: u16,
297        wkday: u8,
298        wk_of_yr_sun: u8,
299        wk_of_yr_mon: u8,
300        scale: Scale,
301    ) -> Self {
302        Self {
303            unix_attosec,
304            yr,
305            mo,
306            day,
307            hr,
308            min,
309            sec,
310            attos,
311            iso_yr,
312            iso_wk,
313            iso_wkday,
314            day_of_yr,
315            wkday,
316            wk_of_yr_sun,
317            wk_of_yr_mon,
318            offset_sec: None,
319            tz: None,
320            tz_abbrev: None,
321            scale,
322        }
323    }
324
325    /// Reconstructs a [`Dt`].
326    #[inline]
327    pub fn to_dt(&self) -> Dt {
328        Dt::from_ymd(
329            self.yr, self.mo, self.day, self.hr, self.min, self.sec, self.attos, self.scale,
330        )
331    }
332
333    /// Attoseconds since 1970-01-01 midnight, on whatever time scale
334    /// the object was created on.
335    #[inline]
336    pub const fn unix_attosec(&self) -> i128 {
337        self.unix_attosec
338    }
339
340    /// The time scale that the object was created on.
341    #[inline]
342    pub const fn scale(&self) -> Scale {
343        self.scale
344    }
345
346    /// Returns the Unix timestamp since 1970-01-01 00:00:00 as a tuple of
347    /// `(whole_seconds, attoseconds)`.
348    ///
349    /// - The timestamp will be on whatever [`Scale`] the [`DateTime`] was created on.
350    /// - `whole_seconds` can be negative (for dates before 1970).
351    /// - The fractional part (`attoseconds`) is always in the range `0..=999_999_999_999_999_999`.
352    #[inline]
353    pub const fn unix_timestamp(&self) -> (i64, u64) {
354        const ATTOS_PER_SEC_I128: i128 = 1_000_000_000_000_000_000;
355        let total = self.unix_attosec;
356        let secs = (total / ATTOS_PER_SEC_I128) as i64;
357        let frac = (total % ATTOS_PER_SEC_I128).unsigned_abs() as u64;
358        (secs, frac)
359    }
360
361    /// Gregorian year (proleptic Gregorian calendar, supports negative years and year 0).
362    #[inline]
363    pub const fn yr(&self) -> i64 {
364        self.yr
365    }
366
367    /// Gregorian month in the range [1, 12].
368    #[inline]
369    pub const fn mo(&self) -> u8 {
370        self.mo
371    }
372
373    /// Gregorian day of the month in the range [1, 31].
374    #[inline]
375    pub const fn day(&self) -> u8 {
376        self.day
377    }
378
379    /// Hour of the day in the range [0, 23].
380    #[inline]
381    pub const fn hr(&self) -> u8 {
382        self.hr
383    }
384
385    /// Minute in the range [0, 59].
386    #[inline]
387    pub const fn min(&self) -> u8 {
388        self.min
389    }
390
391    /// Second in the range [0, 60] (60 only during UTC leap seconds).
392    #[inline]
393    pub const fn sec(&self) -> u8 {
394        self.sec
395    }
396
397    /// Fractional part of the second expressed in attoseconds (`0 ≤ attos < 10¹⁸`).
398    #[inline]
399    pub const fn attos(&self) -> u64 {
400        self.attos
401    }
402
403    /// ISO 8601 week year.
404    #[inline]
405    pub const fn iso_yr(&self) -> i64 {
406        self.iso_yr
407    }
408
409    /// ISO 8601 week number in the range [1, 53].
410    #[inline]
411    pub const fn iso_wk(&self) -> u8 {
412        self.iso_wk
413    }
414
415    /// ISO 8601 weekday (Monday-based [`Weekday`] enum).
416    #[inline]
417    pub const fn iso_wkday(&self) -> Weekday {
418        self.iso_wkday
419    }
420
421    /// Ordinal day of the year (1-based).
422    #[inline]
423    pub const fn day_of_yr(&self) -> u16 {
424        self.day_of_yr
425    }
426
427    /// Weekday number (0 = Sunday … 6 = Saturday).
428    #[inline]
429    pub const fn wkday_sun(&self) -> u8 {
430        self.wkday
431    }
432
433    /// ISO 8601 weekday (0 = Monday ... 6 = Sunday).
434    #[inline]
435    pub const fn wkday_mon(&self) -> u8 {
436        self.iso_wkday.wk_mon()
437    }
438
439    /// Sunday based week of year (Range: `0..=53`).
440    #[inline]
441    pub const fn wk_of_yr_sun(&self) -> u8 {
442        self.wk_of_yr_sun
443    }
444
445    /// Monday based week of year (Range: `0..=53`).
446    #[inline]
447    pub const fn wk_of_yr_mon(&self) -> u8 {
448        self.wk_of_yr_mon
449    }
450
451    #[inline]
452    pub(crate) const fn offset_sec(&self) -> Option<i32> {
453        self.offset_sec
454    }
455
456    #[inline]
457    pub(crate) const fn tz(&self) -> Option<&LiteStr<49>> {
458        self.tz.as_ref()
459    }
460
461    #[inline]
462    pub(crate) const fn tz_abbrev(&self) -> Option<&LiteStr<49>> {
463        self.tz_abbrev.as_ref()
464    }
465
466    #[inline]
467    pub(crate) fn set_offset(&mut self, offset_sec: Option<i32>) -> &mut Self {
468        self.offset_sec = offset_sec;
469        self
470    }
471
472    #[inline]
473    pub(crate) fn set_tz(&mut self, tz: Option<&str>) -> &mut Self {
474        self.tz = tz.map(LiteStr::new);
475        self
476    }
477
478    #[inline]
479    pub(crate) fn set_tz_abbrev(&mut self, tz_abbrev: Option<&str>) -> &mut Self {
480        self.tz_abbrev = tz_abbrev.map(LiteStr::new);
481        self
482    }
483}