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 second = self.sec.unwrap_or(0) as i64;
105        let days_since_j2000 = jd.saturating_sub(JD_2000_2_451_545);
106        let seconds_from_noon_utc = (hour as i64 - 12) * 3600 + minute * 60 + second;
107        let mut total_sec: i64 = days_since_j2000
108            .saturating_mul(SEC_PER_DAYI64)
109            .saturating_add(seconds_from_noon_utc);
110
111        // ──────────────────────────────────────────────────────────────
112        // Apply timezone correction (IANA or Fixed offset)
113        // ──────────────────────────────────────────────────────────────
114
115        if let Some(name) = &self.iana_name {
116            let name_str = name.as_str().map_err(|e| {
117                an_err!(
118                    DtErrKind::InvalidBytes,
119                    "invalid iana ascii: {:?}: {}",
120                    name,
121                    e
122                )
123            })?;
124
125            if !name_str.is_empty() {
126                let provisional_unix =
127                    total_sec.saturating_add(TAI_SECS_1970_MIDNIGHT_TO_2000_NOON);
128                match offset_for_local(name_str, provisional_unix) {
129                    Some(info) => {
130                        if info.is_gap {
131                            // Non-existent time (spring-forward gap) — shift forward
132                            total_sec = total_sec.saturating_add(info.gap_size);
133                            total_sec = total_sec.saturating_sub(info.offset as i64);
134                        } else {
135                            total_sec = total_sec.saturating_sub(info.offset as i64);
136                        }
137                    }
138                    None => {
139                        return Err(an_err!(
140                            DtErrKind::InvalidTimezoneOffset,
141                            "invalid iana: {}",
142                            name_str
143                        ));
144                    }
145                }
146            }
147        } else if let Some(Offset::Fixed(offset)) = self.offset {
148            // local civil time → true UTC instant
149            total_sec = total_sec.saturating_sub(offset as i64);
150        }
151
152        // ──────────────────────────────────────────────────────────────
153        // Final construction
154        // ──────────────────────────────────────────────────────────────
155        if second == 60 {
156            if self.scale.uses_leap_seconds() {
157                let t = Dt::from_sec_and_attos(
158                    total_sec.saturating_sub(1),
159                    self.attos.unwrap_or(0),
160                    self.scale,
161                );
162                let is_leap_sec = match leap_sec(total_sec, true) {
163                    Some(info) => info.is_leap_sec,
164                    None => false,
165                };
166                if is_leap_sec { Ok(t.add_sec(1)) } else { Ok(t) }
167            } else {
168                Ok(Dt::from_sec_and_attos(
169                    total_sec.saturating_sub(1),
170                    self.attos.unwrap_or(0),
171                    self.scale,
172                ))
173            }
174        } else {
175            Ok(Dt::from_sec_and_attos(
176                total_sec,
177                self.attos.unwrap_or(0),
178                self.scale,
179            ))
180        }
181    }
182}