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