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