Skip to main content

deep_time/time_parts/
to_deep_time.rs

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