Skip to main content

deep_time/dt/
gregorian.rs

1use crate::{
2    ATTOS_PER_SEC, Dt, GregorianTime, SEC_PER_DAYI64, Scale, Weekday, YmdHms,
3    leap_seconds::get_leap_seconds,
4};
5
6impl Dt {
7    /// Returns this [`Dt`] but as a unix timestamp
8    /// where the:
9    /// - `.sec` field is seconds since the UNIX epoch
10    /// (1970-01-01 00:00:00).
11    /// - `.attos` field is remaining fractional seconds.
12    ///
13    /// ### Notes:
14    /// - Assumes this [`Dt`] is from the 2000-01-01 noon epoch.
15    #[inline]
16    pub const fn to_unix(&self, current: Scale, target: Scale) -> Dt {
17        self.to(current, target)
18            .to_diff_raw(Dt::UNIX_EPOCH.to_internal(target))
19    }
20
21    /// Converts a Unix timestamp (seconds since 1970-01-01 00:00:00 UTC)
22    /// to a proleptic Gregorian date (year, month, day).
23    #[inline]
24    pub const fn unix_sec_to_ymd(unix_sec: i64) -> (i64, u8, u8) {
25        let days_since_1970 = unix_sec.div_euclid(SEC_PER_DAYI64);
26        // 1970-01-01 00:00:00 UTC is JD 2440588.0
27        let jdn = days_since_1970.saturating_add(2440588);
28        Self::jdn_to_ymd(jdn)
29    }
30
31    pub const fn to_gregorian_time(&self, current: Scale) -> GregorianTime {
32        let ymdhms = self.to_ymdhms(current);
33        let (iso_yr, iso_wk, iso_wkday) =
34            self.to_iso_week_date(current, Some((ymdhms.yr, ymdhms.mo, ymdhms.day)));
35        let day_of_yr = self.day_of_year(current, Some((ymdhms.yr, ymdhms.mo, ymdhms.day)));
36        let jdn = Self::ymd_to_jdn(ymdhms.yr, ymdhms.mo, ymdhms.day);
37        let wkday = Self::jdn_to_weekday(jdn);
38        let wk_of_yr_sun = self.wk_sun(
39            current,
40            Some((ymdhms.yr, ymdhms.mo, ymdhms.day)),
41            Some(day_of_yr),
42        );
43        let wk_of_yr_mon = self.wk_mon(
44            current,
45            Some((ymdhms.yr, ymdhms.mo, ymdhms.day)),
46            Some(day_of_yr),
47        );
48
49        GregorianTime {
50            unix_attosec: ymdhms.unix_attosec,
51            yr: ymdhms.yr,
52            mo: ymdhms.mo,
53            day: ymdhms.day,
54            hr: ymdhms.hr,
55            min: ymdhms.min,
56            sec: ymdhms.sec,
57            attos: ymdhms.attos,
58            iso_yr,
59            iso_wk,
60            iso_wkday,
61            day_of_yr,
62            wkday,
63            wk_of_yr_sun,
64            wk_of_yr_mon,
65            offset_sec: None,
66            tz: None,
67            tz_abbrev: None,
68        }
69    }
70
71    /// Returns the Gregorian date and wall time for this instant.
72    ///
73    /// - For `Scale::UTC`: Uses a direct Unix-timestamp-based path (fast and clean).
74    /// - For all other scales: Uses the standard TT-based JD path.
75    #[inline]
76    pub const fn to_ymdhms(&self, current: Scale) -> YmdHms {
77        // tai knows whether the seconds lie exactly on a leap second
78        let tai = if current.is_tai() {
79            *self
80        } else {
81            self.to(current, Scale::TAI)
82        };
83        let canon = tai.to_scale_and_then_diff(Scale::UTC, Dt::UNIX_EPOCH);
84
85        let unix_sec = canon.sec;
86        let attos = canon.attos;
87
88        let is_leap_second = get_leap_seconds(&tai, false).is_leap_second;
89
90        // For the date we always use the previous second when on a leap second
91        // (so 23:59:60 stays on the correct civil day).
92        let unix_sec_for_date = if is_leap_second {
93            unix_sec - 1
94        } else {
95            unix_sec
96        };
97
98        let (yr, mo, day) = Self::unix_sec_to_ymd(unix_sec_for_date);
99
100        // Only the hour/minute/second fields differ for a leap second.
101        let (hr, min, sec) = if is_leap_second {
102            (23, 59, 60)
103        } else {
104            let seconds_since_midnight = unix_sec.rem_euclid(SEC_PER_DAYI64);
105            let hr = (seconds_since_midnight / 3600) as u8;
106            let min = ((seconds_since_midnight % 3600) / 60) as u8;
107            let sec = (seconds_since_midnight % 60) as u8;
108            (hr, min, sec)
109        };
110
111        YmdHms {
112            unix_attosec: canon.to_attos(),
113            yr,
114            mo,
115            day,
116            hr,
117            min,
118            sec,
119            attos,
120        }
121    }
122
123    /// Converts a proleptic Gregorian calendar date+time to a Unix timestamp
124    /// (seconds since 1970-01-01 00:00:00 UTC).
125    ///
126    /// This version is correct for the full i64 range, including negative years.
127    pub const fn ymdhms_to_unix_sec(yr: i64, mo: u8, day: u8, hr: u8, min: u8, sec: u8) -> i64 {
128        let jdn = Self::ymd_to_jdn(yr, mo, day);
129        // 1970-01-01 00:00:00 UTC corresponds to JD 2440588
130        let days_since_1970 = jdn.saturating_sub(2440588);
131        let time_of_day = (hr as i64) * 3600 + (min as i64) * 60 + (sec as i64);
132        days_since_1970
133            .saturating_mul(SEC_PER_DAYI64)
134            .saturating_add(time_of_day)
135    }
136
137    /// Converts a Julian Day Number (JDN) to a proleptic Gregorian calendar date.
138    ///
139    /// Returns `(year, month, day)` where `month` ∈ [1, 12] and `day` ∈ [1, 31]
140    /// (standard 1-based Gregorian values).
141    ///
142    /// This is the inverse of [`Self::ymd_to_jdn`]. Supports the full `i64`
143    /// range, including negative years and year zero.
144    pub const fn jdn_to_ymd(jdn: i64) -> (i64, u8, u8) {
145        // Use i128 internally to avoid overflow on full i64 JDN range
146        let j = jdn as i128;
147
148        // Floor division helper (required for negative JDNs)
149        const fn floor_div(a: i128, b: i128) -> i128 {
150            let q = a / b;
151            let r = a % b;
152            if (r > 0 && b < 0) || (r < 0 && b > 0) {
153                q - 1
154            } else {
155                q
156            }
157        }
158
159        let a = j + 32044;
160        let b = floor_div(4 * a + 3, 146097);
161        let c = a - floor_div(b * 146097, 4);
162        let d = floor_div(4 * c + 3, 1461);
163        let e = c - floor_div(1461 * d, 4);
164        let m = floor_div(5 * e + 2, 153);
165        let day = (e - floor_div(153 * m + 2, 5) + 1) as u8;
166        let month = (m + 3 - 12 * floor_div(m, 10)) as u8;
167        let year = b * 100 + d - 4800 + floor_div(m, 10);
168
169        debug_assert!(day >= 1 && day <= 31);
170        debug_assert!(month >= 1 && month <= 12);
171
172        (year as i64, month, day)
173    }
174
175    /// Computes the Julian Day Number (JDN) for a proleptic Gregorian calendar date at noon UT.
176    ///
177    /// The algorithm matches the standard astronomical convention used throughout the library
178    /// (`ymd_to_jdn(2000, 1, 1) == 2451545`).
179    pub const fn ymd_to_jdn(year: i64, month: u8, day: u8) -> i64 {
180        let a = (14 - month as i64) / 12;
181        let y = year.saturating_add(4800).saturating_sub(a);
182        let m = month as i64 + 12 * a - 3;
183
184        const fn floor_div(a: i64, b: i64) -> i64 {
185            let q = a / b;
186            let r = a % b;
187            if (r > 0 && b < 0) || (r < 0 && b > 0) {
188                q - 1
189            } else {
190                q
191            }
192        }
193
194        let y4 = floor_div(y, 4);
195        let y100 = floor_div(y, 100);
196        let y400 = floor_div(y, 400);
197
198        let result = (day as i64)
199            .saturating_add((153i64 * m + 2) / 5)
200            .saturating_add(365i64 * y)
201            .saturating_add(y4)
202            .saturating_sub(y100)
203            .saturating_add(y400)
204            .saturating_sub(32045);
205
206        result
207    }
208
209    /// Returns `true` if the given year is a Gregorian leap year under proleptic rules.
210    #[inline]
211    pub const fn is_leap_year(year: i64) -> bool {
212        year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
213    }
214
215    /// Creates a `Dt` at the specified civil UTC instant with full
216    /// attosecond precision on the proleptic Gregorian calendar, then converts
217    /// it to the requested [`Scale`].
218    ///
219    /// All input components are clamped to their valid ranges:
220    /// - `mo`   → 0..=12
221    /// - `day`  → 0..=31
222    /// - `hr`   → 0..=23
223    /// - `min`  → 0..=59
224    /// - `sec`  → 0..=60 (permits leap seconds)
225    /// - `attos` → values ≥ 10¹⁸ are carried into the seconds field
226    #[inline]
227    pub const fn from_ymdhms(
228        yr: i64,
229        mo: u8,
230        day: u8,
231        hr: u8,
232        min: u8,
233        sec: u8,
234        attos: u64,
235    ) -> Self {
236        Dt::from_ymdhms_on(yr, mo, day, hr, min, sec, attos, Scale::UTC)
237    }
238
239    pub const fn from_ymdhms_on(
240        yr: i64,
241        mo: u8,
242        day: u8,
243        hr: u8,
244        min: u8,
245        sec: u8,
246        attos: u64,
247        scale: Scale,
248    ) -> Self {
249        let mo = if mo > 12 { 12 } else { mo };
250        let day = if day > 31 { 31 } else { day };
251        let h = if hr > 23 { 23 } else { hr };
252        let m = if min > 59 { 59 } else { min };
253        let s = if sec > 60 { 60 } else { sec };
254
255        let extra_sec = (attos / ATTOS_PER_SEC) as i64;
256        let final_attos = attos % ATTOS_PER_SEC;
257
258        // For an exact leap second (sec==60 with no sub-second carry), compute
259        // the civil Unix timestamp using 23:59:59, create that instant, then
260        // add exactly 1 physical second. This lands on the correct internal TAI
261        // slot (matching LEAP_SECS.tai_sec) while preserving the library's
262        // convention that to_epoch_attos(UTC) for the leap second returns the
263        // "following midnight" civil value. On non-leap days or with carry,
264        // the normal rollover path is used and to_ymdhms_utc will display
265        // correctly because is_leap_second only triggers on exact tai_sec match.
266        let is_exact_leap_second = s == 60 && extra_sec == 0;
267        let s_for_unix = if is_exact_leap_second { 59 } else { s };
268
269        let civil_unix_sec = Self::ymdhms_to_unix_sec(yr, mo, day, h, m, s_for_unix) + extra_sec;
270
271        let tp =
272            Self::from_diff_and_scale(Dt::new(civil_unix_sec, final_attos), Dt::UNIX_EPOCH, scale);
273        if is_exact_leap_second {
274            tp.add(Dt::from_sec(1, Scale::TAI))
275        } else {
276            tp
277        }
278    }
279
280    /// Creates a [`Dt`] representing **00:00:00 UTC** on the given proleptic Gregorian date.
281    #[inline]
282    pub const fn from_ymd(yr: i64, mo: u8, day: u8) -> Self {
283        let unix_sec = Self::ymdhms_to_unix_sec(yr, mo, day, 0, 0, 0);
284        Self::from_diff_and_scale(Dt::new(unix_sec, 0), Dt::UNIX_EPOCH, Scale::UTC)
285    }
286
287    /// Creates a [`Dt`] representing **00:00:00 on the given [`Scale`]** on the given proleptic Gregorian date.
288    #[inline]
289    pub const fn from_ymd_on(yr: i64, mo: u8, day: u8, scale: Scale) -> Self {
290        let unix_sec = Self::ymdhms_to_unix_sec(yr, mo, day, 0, 0, 0);
291        Self::from_diff_and_scale(Dt::new(unix_sec, 0), Dt::UNIX_EPOCH, scale)
292    }
293
294    /// Computes the Julian Day Number from a Gregorian year and ordinal day-of-year.
295    #[inline]
296    pub const fn ydoy_to_jdn(year: i64, day_of_year: u16) -> i64 {
297        let jdn_jan1 = Self::ymd_to_jdn(year, 1, 1);
298        jdn_jan1.saturating_add(day_of_year as i64 - 1)
299    }
300
301    /// Converts a Julian Day Number to the corresponding weekday number (0 = Sunday … 6 = Saturday).
302    #[inline]
303    pub const fn jdn_to_weekday(jdn: i64) -> u8 {
304        let rem = ((jdn as i128) + 1) % 7;
305        let positive = if rem < 0 { rem + 7 } else { rem };
306        positive as u8
307    }
308
309    /// Computes the Julian Day Number from an ISO week date (Monday-based week).
310    pub const fn ymd_to_jdn_from_iso_week(iso_year: i64, iso_week: u8, weekday: Weekday) -> i64 {
311        let jan4_jdn = Self::ymd_to_jdn(iso_year, 1, 4);
312        let wd_jan4 = Self::jdn_to_weekday(jan4_jdn);
313
314        let days_to_monday = {
315            let tmp = (wd_jan4 as i64).saturating_add(6);
316            let rem = tmp % 7;
317            if rem < 0 { rem + 7 } else { rem }
318        };
319
320        let monday_week1 = jan4_jdn.saturating_sub(days_to_monday);
321
322        let monday_requested =
323            monday_week1.saturating_add(((iso_week as i64).saturating_sub(1)).saturating_mul(7));
324
325        let wd_offset = match weekday {
326            Weekday::Monday => 0,
327            Weekday::Tuesday => 1,
328            Weekday::Wednesday => 2,
329            Weekday::Thursday => 3,
330            Weekday::Friday => 4,
331            Weekday::Saturday => 5,
332            Weekday::Sunday => 6,
333        };
334
335        monday_requested.saturating_add(wd_offset as i64)
336    }
337
338    /// Computes the Julian Day Number from a Sunday-based week-of-year (`%U`).
339    pub const fn ymd_to_jdn_from_week_sun(year: i64, week: u8, weekday: Weekday) -> i64 {
340        let jan1_jdn = Self::ymd_to_jdn(year, 1, 1);
341        let wd_jan1 = Self::jdn_to_weekday(jan1_jdn);
342
343        let days_to_first_sunday = ((7u8 - wd_jan1) % 7u8) as i64;
344        let first_sunday_jdn = jan1_jdn.saturating_add(days_to_first_sunday);
345
346        let sunday_of_week =
347            first_sunday_jdn.saturating_add(((week as i64).saturating_sub(1)).saturating_mul(7));
348
349        let wd_offset = match weekday {
350            Weekday::Sunday => 0,
351            Weekday::Monday => 1,
352            Weekday::Tuesday => 2,
353            Weekday::Wednesday => 3,
354            Weekday::Thursday => 4,
355            Weekday::Friday => 5,
356            Weekday::Saturday => 6,
357        };
358
359        sunday_of_week.saturating_add(wd_offset as i64)
360    }
361
362    /// Computes the Julian Day Number from a Monday-based week-of-year (`%W`).
363    pub const fn ymd_to_jdn_from_week_mon(year: i64, week: u8, weekday: Weekday) -> i64 {
364        let jan1_jdn = Self::ymd_to_jdn(year, 1, 1);
365        let wd_jan1 = Self::jdn_to_weekday(jan1_jdn);
366
367        let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
368        let first_monday_jdn = jan1_jdn.saturating_add(days_to_first_monday);
369
370        let monday_of_week =
371            first_monday_jdn.saturating_add(((week as i64).saturating_sub(1)).saturating_mul(7));
372
373        let wd_offset = match weekday {
374            Weekday::Monday => 0,
375            Weekday::Tuesday => 1,
376            Weekday::Wednesday => 2,
377            Weekday::Thursday => 3,
378            Weekday::Friday => 4,
379            Weekday::Saturday => 5,
380            Weekday::Sunday => 6,
381        };
382
383        monday_of_week.saturating_add(wd_offset as i64)
384    }
385
386    /// Returns `true` if the supplied values form a valid proleptic Gregorian calendar date.
387    pub const fn is_valid_ymd(year: i64, month: u8, day: u8) -> bool {
388        if month < 1 || month > 12 || day < 1 {
389            return false;
390        }
391        let days = match month {
392            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31u8,
393            4 | 6 | 9 | 11 => 30u8,
394            2 => {
395                if Self::is_leap_year(year) {
396                    29
397                } else {
398                    28
399                }
400            }
401            _ => return false,
402        };
403        day <= days
404    }
405
406    /// Returns `true` if the given Gregorian year contains an ISO week 53.
407    pub const fn has_iso_week_53(year: i64) -> bool {
408        let jan1_jdn = Self::ymd_to_jdn(year, 1, 1);
409        let wd_jan1 = Self::jdn_to_weekday(jan1_jdn);
410        wd_jan1 == 4 || (Self::is_leap_year(year) && wd_jan1 == 3)
411    }
412
413    /// Returns the ordinal day of the year (1-based).
414    ///
415    /// January 1 is day `1`; December 31 is day `365` or `366` (in leap years).
416    /// Uses the proleptic Gregorian calendar.
417    pub const fn day_of_year(&self, current: Scale, ymd: Option<(i64, u8, u8)>) -> u16 {
418        let (year, month, day) = if let Some(ymd) = ymd {
419            ymd
420        } else {
421            let g = self.to_ymdhms(current);
422            (g.yr, g.mo, g.day)
423        };
424        let jdn = Self::ymd_to_jdn(year, month, day);
425        let jdn_jan1 = Self::ymd_to_jdn(year, 1, 1);
426
427        let doy = jdn.saturating_sub(jdn_jan1).saturating_add(1);
428        doy as u16
429    }
430
431    /// Sunday-based week number (`%U` in strftime).
432    ///
433    /// Range: `0..=53`.
434    /// - Week 0 contains the days *before* the first Sunday of the year.
435    /// - Week 1 begins on the first Sunday of the year.
436    ///
437    /// The optional `ymd` and `doy` arguments are performance optimisations
438    /// (same pattern used throughout the file for `day_of_year`, `to_iso_week_date`, etc.).
439    /// Pass whichever you already have; the function will use the fastest path.
440    pub const fn wk_sun(&self, current: Scale, ymd: Option<(i64, u8, u8)>, doy: Option<u16>) -> u8 {
441        let (year, _, _) = if let Some(ymd) = ymd {
442            ymd
443        } else {
444            let g = self.to_ymdhms(current);
445            (g.yr, g.mo, g.day)
446        };
447        let doy = if let Some(doy) = doy {
448            doy
449        } else {
450            self.day_of_year(current, ymd)
451        };
452        let jan1_jdn = Self::ymd_to_jdn(year, 1, 1);
453        let wd_jan1 = Self::jdn_to_weekday(jan1_jdn);
454        let days_to_first_sunday = (7u8 - wd_jan1) % 7u8;
455        let first_sunday_doy = days_to_first_sunday as u16 + 1;
456        if doy < first_sunday_doy {
457            0
458        } else {
459            let days_since_first_sunday = doy.saturating_sub(first_sunday_doy);
460            ((days_since_first_sunday / 7) + 1) as u8
461        }
462    }
463
464    /// Monday-based week number (`%W` in strftime).
465    ///
466    /// Range: `0..=53`.
467    /// - Week 0 contains the days *before* the first Monday of the year.
468    /// - Week 1 begins on the first Monday of the year.
469    ///
470    /// The optional `ymd` and `doy` arguments are performance optimisations
471    /// (same pattern as `wk_sun`, `day_of_year`, `to_iso_week_date`, etc.).
472    pub const fn wk_mon(&self, current: Scale, ymd: Option<(i64, u8, u8)>, doy: Option<u16>) -> u8 {
473        let (year, _, _) = if let Some(ymd) = ymd {
474            ymd
475        } else {
476            let g = self.to_ymdhms(current);
477            (g.yr, g.mo, g.day)
478        };
479        let doy = if let Some(doy) = doy {
480            doy
481        } else {
482            self.day_of_year(current, ymd)
483        };
484        let jan1_jdn = Self::ymd_to_jdn(year, 1, 1);
485        let wd_jan1 = Self::jdn_to_weekday(jan1_jdn);
486        let days_to_first_monday = (1i64 - wd_jan1 as i64).rem_euclid(7);
487        let first_monday_doy = days_to_first_monday as u16 + 1;
488        if doy < first_monday_doy {
489            0
490        } else {
491            let days_since_first_monday = doy.saturating_sub(first_monday_doy);
492            ((days_since_first_monday / 7) + 1) as u8
493        }
494    }
495
496    /// Returns the ISO 8601 week date for this `Dt`.
497    ///
498    /// Returns `(iso_year, iso_week, weekday)` where:
499    /// - `iso_year` is the ISO week year (may differ from the Gregorian year near
500    ///   year boundaries),
501    /// - `iso_week` is the week number in the range `1..=53`,
502    /// - `weekday` is a [`Weekday`] value (Monday-based week).
503    ///
504    /// Follows the ISO 8601 standard: weeks start on Monday and week 1 is the
505    /// week containing January 4.
506    ///
507    /// The optional `ymd` argument is a performance optimization. If provided,
508    /// it is used directly; otherwise [`to_gregorian_ymd`](Self::to_gregorian_ymd)
509    /// is called internally.
510    pub const fn to_iso_week_date(
511        &self,
512        current: Scale,
513        ymd: Option<(i64, u8, u8)>,
514    ) -> (i64, u8, Weekday) {
515        let (year, month, day) = if let Some(ymd) = ymd {
516            ymd
517        } else {
518            let g = self.to_ymdhms(current);
519            (g.yr, g.mo, g.day)
520        };
521        let jdn = Self::ymd_to_jdn(year, month, day);
522        let wd = Self::jdn_to_weekday(jdn);
523        let wd_iso = if wd == 0 { 7 } else { wd };
524
525        let jan4_jdn = Self::ymd_to_jdn(year, 1, 4);
526        let wd_jan4 = Self::jdn_to_weekday(jan4_jdn);
527        let days_to_monday = {
528            let tmp = (wd_jan4 as i64) + 6;
529            let rem = tmp % 7;
530            if rem < 0 { rem + 7 } else { rem }
531        };
532
533        let monday_week1 = jan4_jdn - days_to_monday;
534
535        let days_since = jdn - monday_week1;
536
537        let week = if days_since < 0 {
538            0u8
539        } else {
540            ((days_since / 7) + 1) as u8
541        };
542
543        let iso_year = if week == 0 {
544            year - 1
545        } else if (week == 53 || week > 53) && !Self::has_iso_week_53(year) {
546            year + 1
547        } else {
548            year
549        };
550
551        let iso_week = if week == 0 {
552            if Self::has_iso_week_53(year - 1) {
553                53
554            } else {
555                52
556            }
557        } else if week == 53 && !Self::has_iso_week_53(year) {
558            1
559        } else if week > 53 {
560            1
561        } else {
562            week
563        };
564
565        let weekday_enum = match wd_iso {
566            1 => Weekday::Monday,
567            2 => Weekday::Tuesday,
568            3 => Weekday::Wednesday,
569            4 => Weekday::Thursday,
570            5 => Weekday::Friday,
571            6 => Weekday::Saturday,
572            _ => Weekday::Sunday,
573        };
574
575        (iso_year, iso_week, weekday_enum)
576    }
577}