Skip to main content

deep_time/time_parts/
to_deep_time.rs

1use crate::leap_seconds::leap_sec;
2use crate::tz::offset_for_local;
3use crate::{
4    Dt, JD_2000_2_451_545, SEC_PER_DAYI64, TAI_SECS_1970_MIDNIGHT_TO_2000_NOON, an_err,
5    error::{DtErr, DtErrKind},
6    {Meridiem, Offset, TimeParts, Weekday},
7};
8
9impl TimeParts {
10    /// Converts [`TimeParts`] → [`Dt`].
11    /// - Resulting [`Dt`] is on the TAI timescale.
12    /// - If this [`TimeParts`] has a unix timestamp then it is used
13    ///   instead of anything else.
14    pub fn to_dt(&self) -> Result<Dt, DtErr> {
15        // ──────────────────────────────────────────────────────────────
16        // Fast path: explicit Unix timestamp
17        // ──────────────────────────────────────────────────────────────
18        if let Some(unix_secs) = self.unix_timestamp_seconds {
19            let total_sec = unix_secs.saturating_sub(TAI_SECS_1970_MIDNIGHT_TO_2000_NOON);
20            return Ok(Dt::from_sec_and_attos(
21                total_sec,
22                self.attos.unwrap_or(0),
23                self.scale,
24            ));
25        }
26
27        // ──────────────────────────────────────────────────────────────
28        // Civil date path
29        // ──────────────────────────────────────────────────────────────
30        let mut jd: Option<i64> = None;
31
32        if let Some(year) = self.yr {
33            if let (Some(m), Some(d)) = (self.mo, self.day) {
34                // Classic YMD – highest priority + full validation
35                if !Dt::is_valid_ymd(year, m, d) {
36                    return Err(an_err!(DtErrKind::InvalidInput, "ymd"));
37                }
38                jd = Some(Dt::ymd_to_jd(year, m, d));
39            } else if let Some(doy) = self.day_of_yr {
40                // Ordinal date (%j) – already validated
41                if doy == 0 || doy > 366 || (doy == 366 && !Dt::is_leap_yr(year)) {
42                    return Err(an_err!(DtErrKind::OutOfRange, "day of year"));
43                }
44                jd = Some(Dt::ydoy_to_jd(year, doy));
45            }
46        }
47
48        if jd.is_none() {
49            if let (Some(iso_y), Some(iso_w)) = (self.iso_wk_yr, self.iso_wk) {
50                // ISO week date (%G/%V)
51                if iso_w == 0 || iso_w > 53 {
52                    return Err(an_err!(DtErrKind::OutOfRange, "iso week"));
53                }
54                if iso_w == 53 && !Dt::has_iso_wk_53(iso_y) {
55                    return Err(an_err!(DtErrKind::InvalidItem, "iso week"));
56                }
57                let wd = self.wkday.unwrap_or(Weekday::Monday);
58                jd = Some(Dt::iso_wk_to_jd(iso_y, iso_w, wd));
59            } else if let (Some(y), Some(w)) = (self.yr, self.wk_sun) {
60                // Sunday-based week (%U)
61                if w > 53 {
62                    return Err(an_err!(DtErrKind::OutOfRange, "week number"));
63                }
64                let wd = self.wkday.unwrap_or(Weekday::Sunday);
65                jd = Some(Dt::wk_sun_to_jd(y, w, wd));
66            } else if let (Some(y), Some(w)) = (self.yr, self.wk_mon) {
67                // Monday-based week (%W)
68                if w > 53 {
69                    return Err(an_err!(DtErrKind::OutOfRange, "week number"));
70                }
71                let wd = self.wkday.unwrap_or(Weekday::Monday);
72                jd = Some(Dt::wk_mon_to_jd(y, w, wd));
73            }
74        }
75
76        let Some(jd) = jd else {
77            if self.yr.is_none() && self.iso_wk_yr.is_none() {
78                return Err(an_err!(DtErrKind::Incomplete, "no year"));
79            } else {
80                return Err(an_err!(DtErrKind::InvalidInput, "could not create julian"));
81            }
82        };
83
84        // ──────────────────────────────────────────────────────────────
85        // Resolve 12-hour time + meridiem (AM/PM) to 24-hour hour
86        // ──────────────────────────────────────────────────────────────
87        let hour = match (self.hr, self.meridiem) {
88            (Some(h), Some(m)) => {
89                if !(1..=12).contains(&h) {
90                    return Err(an_err!(DtErrKind::OutOfRange, "hour: {}", h));
91                }
92                match (h, m) {
93                    (12, Meridiem::AM) => 0,
94                    (12, Meridiem::PM) => 12,
95                    (h, Meridiem::AM) => h,
96                    (h, Meridiem::PM) => h + 12,
97                }
98            }
99            (Some(h), None) => h,
100            (None, _) => 0,
101        };
102
103        let minute = self.min.unwrap_or(0) as i64;
104        let mut second = self.sec.unwrap_or(0) as i64;
105        let sec_is_60 = second == 60;
106        if sec_is_60 {
107            second = second.saturating_sub(1)
108        }
109
110        let days_since_j2000 = jd.saturating_sub(JD_2000_2_451_545);
111        let seconds_from_noon_utc = (hour as i64 - 12) * 3600 + minute * 60 + second;
112        let mut total_sec: i64 = days_since_j2000
113            .saturating_mul(SEC_PER_DAYI64)
114            .saturating_add(seconds_from_noon_utc);
115
116        // ──────────────────────────────────────────────────────────────
117        // Apply timezone correction (IANA or Fixed offset)
118        // ──────────────────────────────────────────────────────────────
119        if let Some(name) = &self.iana_name {
120            let name_str = name.as_str().map_err(|e| {
121                an_err!(
122                    DtErrKind::InvalidBytes,
123                    "invalid iana ascii: {:?}: {}",
124                    name,
125                    e
126                )
127            })?;
128
129            if !name_str.is_empty() {
130                let provisional_unix =
131                    total_sec.saturating_add(TAI_SECS_1970_MIDNIGHT_TO_2000_NOON);
132                match offset_for_local(name_str, provisional_unix) {
133                    Some(info) => {
134                        if info.is_gap {
135                            // Non-existent time (spring-forward gap) — shift forward
136                            total_sec = total_sec.saturating_add(info.gap_size);
137                            total_sec = total_sec.saturating_sub(info.offset as i64);
138                        } else {
139                            total_sec = total_sec.saturating_sub(info.offset as i64);
140                        }
141                    }
142                    None => {
143                        return Err(an_err!(
144                            DtErrKind::InvalidTimezoneOffset,
145                            "invalid iana: {}",
146                            name_str
147                        ));
148                    }
149                }
150            }
151        } else if let Some(Offset::Fixed(offset)) = self.offset {
152            // local civil time → true UTC instant
153            total_sec = total_sec.saturating_sub(offset as i64);
154        }
155
156        // ──────────────────────────────────────────────────────────────
157        // Final construction
158        // ──────────────────────────────────────────────────────────────
159        if !sec_is_60 {
160            Ok(Dt::from_sec_and_attos(
161                total_sec,
162                self.attos.unwrap_or(0),
163                self.scale,
164            ))
165        } else {
166            if self.scale.uses_leap_seconds() {
167                let t = Dt::from_sec_and_attos(total_sec, self.attos.unwrap_or(0), self.scale);
168                let is_leap_sec = match leap_sec(total_sec.saturating_add(1), true) {
169                    Some(info) => info.is_leap_sec,
170                    None => false,
171                };
172                if is_leap_sec { Ok(t.add_sec(1)) } else { Ok(t) }
173            } else {
174                Ok(Dt::from_sec_and_attos(
175                    total_sec,
176                    self.attos.unwrap_or(0),
177                    self.scale,
178                ))
179            }
180        }
181    }
182}