Skip to main content

deep_time/time_parts/
to_deep_time.rs

1use crate::tzdb::offset_info_at_local;
2use crate::{
3    Dt, JD_2000_2_451_545, SEC_PER_DAYI64, Scale, TAI_SECS_1970_MIDNIGHT_TO_2000_NOON, an_err,
4    error::{DtErr, DtErrKind},
5    {Meridiem, Offset, TimeParts, Weekday},
6};
7
8impl TimeParts {
9    pub fn to_time_point(&self) -> Result<Dt, DtErr> {
10        // ──────────────────────────────────────────────────────────────
11        // Fast path: explicit Unix timestamp
12        // ──────────────────────────────────────────────────────────────
13        if let Some(unix_secs) = self.unix_timestamp_seconds {
14            let sec = (unix_secs as i64) - TAI_SECS_1970_MIDNIGHT_TO_2000_NOON;
15            let subsec = self.attos.unwrap_or(0);
16            return Ok(Dt::from(sec, subsec, Scale::UTC));
17        }
18
19        // ──────────────────────────────────────────────────────────────
20        // Resolve 12-hour time + meridiem (AM/PM) to 24-hour hour
21        // ──────────────────────────────────────────────────────────────
22        let hour = match (self.hour, self.meridiem) {
23            (Some(h), Some(m)) => {
24                if !(1..=12).contains(&h) {
25                    return Err(an_err!(DtErrKind::OutOfRange, "hour: {}", h));
26                }
27                match (h, m) {
28                    (12, Meridiem::AM) => 0,
29                    (12, Meridiem::PM) => 12,
30                    (h, Meridiem::AM) => h,
31                    (h, Meridiem::PM) => h + 12,
32                }
33            }
34            (Some(h), None) => h,
35            (None, _) => 0,
36        };
37
38        // ──────────────────────────────────────────────────────────────
39        // Civil date path
40        // ──────────────────────────────────────────────────────────────
41        if self.year.is_none() && self.iso_week_year.is_none() {
42            return Err(an_err!(DtErrKind::Incomplete, "no year"));
43        }
44
45        let minute = self.minute.unwrap_or(0);
46        let second = self.second.unwrap_or(0);
47        let subsec = self.attos.unwrap_or(0);
48        let mut jdn: Option<i64> = None;
49
50        if let Some(year) = self.year {
51            if let (Some(m), Some(d)) = (self.month, self.day) {
52                // Classic YMD – highest priority + full validation
53                if !Dt::is_valid_ymd(year, m, d) {
54                    return Err(an_err!(DtErrKind::InvalidInput, "ymd"));
55                }
56                jdn = Some(Dt::ymd_to_jdn(year, m, d));
57            } else if let Some(doy) = self.day_of_year {
58                // Ordinal date (%j) – already validated
59                if doy == 0 || doy > 366 || (doy == 366 && !Dt::is_leap_year(year)) {
60                    return Err(an_err!(DtErrKind::OutOfRange, "day of year"));
61                }
62                jdn = Some(Dt::ydoy_to_jdn(year, doy));
63            }
64        }
65
66        if jdn.is_none() {
67            if let (Some(iso_y), Some(iso_w)) = (self.iso_week_year, self.iso_week) {
68                // ISO week date (%G/%V)
69                if iso_w == 0 || iso_w > 53 {
70                    return Err(an_err!(DtErrKind::OutOfRange, "iso week"));
71                }
72                if iso_w == 53 && !Dt::has_iso_week_53(iso_y) {
73                    return Err(an_err!(DtErrKind::InvalidItem, "iso week"));
74                }
75                let wd = self.weekday.unwrap_or(Weekday::Monday);
76                jdn = Some(Dt::ymd_to_jdn_from_iso_week(iso_y, iso_w, wd));
77            } else if let (Some(y), Some(w)) = (self.year, self.week_sun) {
78                // Sunday-based week (%U)
79                if w > 53 {
80                    return Err(an_err!(DtErrKind::OutOfRange, "week number"));
81                }
82                let wd = self.weekday.unwrap_or(Weekday::Sunday);
83                jdn = Some(Dt::ymd_to_jdn_from_week_sun(y, w, wd));
84            } else if let (Some(y), Some(w)) = (self.year, self.week_mon) {
85                // Monday-based week (%W)
86                if w > 53 {
87                    return Err(an_err!(DtErrKind::OutOfRange, "week number"));
88                }
89                let wd = self.weekday.unwrap_or(Weekday::Monday);
90                jdn = Some(Dt::ymd_to_jdn_from_week_mon(y, w, wd));
91            }
92        }
93
94        let Some(jdn) = jdn else {
95            return Err(an_err!(DtErrKind::InvalidInput, "could not create julian"));
96        };
97        let days_since_j2000 = jdn - JD_2000_2_451_545;
98        let seconds_from_noon_utc =
99            (hour as i64 - 12) * 3600 + (minute as i64) * 60 + (second as i64);
100        let mut sec_utc = days_since_j2000 * SEC_PER_DAYI64 + seconds_from_noon_utc;
101
102        // ──────────────────────────────────────────────────────────────
103        // Apply timezone correction (IANA or Fixed offset)
104        // ──────────────────────────────────────────────────────────────
105
106        if let Some(name) = &self.iana_name {
107            let name_str = name.as_str().map_err(|e| {
108                an_err!(
109                    DtErrKind::InvalidBytes,
110                    "invalid iana ascii: {:?}: {}",
111                    name,
112                    e
113                )
114            })?;
115
116            if !name_str.is_empty() {
117                let provisional_unix = sec_utc + TAI_SECS_1970_MIDNIGHT_TO_2000_NOON;
118                match offset_info_at_local(name_str, provisional_unix) {
119                    Some(info) => {
120                        if info.is_gap {
121                            // Non-existent time (spring-forward gap) — shift forward
122                            sec_utc += info.gap_size as i64; // shift local time into the valid post-gap period
123                            sec_utc -= info.offset as i64; // apply the post-jump offset
124                        } else {
125                            sec_utc -= info.offset as i64;
126                        }
127                    }
128                    None => {
129                        return Err(an_err!(
130                            DtErrKind::InvalidTimezoneOffset,
131                            "invalid iana: {}",
132                            name_str
133                        ));
134                    }
135                }
136            }
137        } else if let Some(Offset::Fixed(offset)) = self.offset {
138            sec_utc -= offset as i64; // local civil time → true UTC instant
139        }
140        Ok(Dt::from(sec_utc, subsec, Scale::UTC))
141    }
142}