Skip to main content

deep_time/time_parts/
to_deep_time.rs

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