Skip to main content

deep_time/time_parts/
to_chrono.rs

1use crate::tz::offset_for_local;
2use crate::{
3    ATTOS_PER_NS, Dt, an_err,
4    error::{DtErr, DtErrKind},
5    {Meridiem, Offset, TimeParts, Weekday},
6};
7use chrono::{
8    DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone as ChronoTimeZone,
9};
10
11impl TimeParts {
12    /// Converts [`TimeParts`] → [`chrono::NaiveDateTime`] (civil time, no TZ).
13    pub fn to_chrono_naive_datetime(&self) -> Result<NaiveDateTime, DtErr> {
14        let date = self.build_naive_date()?;
15        let time = self.build_naive_time()?;
16
17        Ok(date.and_time(time))
18    }
19
20    fn build_naive_date(&self) -> Result<NaiveDate, DtErr> {
21        // YMD (highest priority, matches Jiff fast-path)
22        if let (Some(y), Some(m), Some(d)) = (self.yr, self.mo, self.day) {
23            let year_i32: i32 = y
24                .try_into()
25                .map_err(|e| an_err!(DtErrKind::InvalidNumber, "year: {}: {}", y, e))?;
26            return NaiveDate::from_ymd_opt(year_i32, m as u32, d as u32)
27                .ok_or_else(|| an_err!(DtErrKind::InvalidInput, "ymd: {}-{}-{}", year_i32, m, d));
28        }
29
30        // Ordinal date (%j)
31        if let (Some(y), Some(doy)) = (self.yr, self.day_of_yr) {
32            let year_i32: i32 = y
33                .try_into()
34                .map_err(|e| an_err!(DtErrKind::InvalidNumber, "year: {}: {}", y, e))?;
35            return NaiveDate::from_yo_opt(year_i32, doy as u32)
36                .ok_or_else(|| an_err!(DtErrKind::InvalidInput, "ydoy: {}-{}", y, doy));
37        }
38
39        // Small helper: JD → chrono NaiveDate
40        let jd_to_naive_date = |jd: i64| -> Result<NaiveDate, DtErr> {
41            let days_from_ce: i32 = (jd - 1721425)
42                .try_into()
43                .map_err(|e| an_err!(DtErrKind::InvalidInput, "jd: {}: {}", jd, e))?;
44            NaiveDate::from_num_days_from_ce_opt(days_from_ce)
45                .ok_or_else(|| an_err!(DtErrKind::InvalidInput, "days_from_ce: {}", days_from_ce))
46        };
47
48        // ISO week date (%G/%V + weekday)
49        if let (Some(iso_y), Some(w)) = (self.iso_wk_yr, self.iso_wk) {
50            let wd = self.wkday.unwrap_or(Weekday::Monday);
51            let jd = Dt::iso_wk_to_jd(iso_y, w, wd);
52            return jd_to_naive_date(jd);
53        }
54
55        // Sunday-based week number (%U)
56        if let (Some(y), Some(w)) = (self.yr, self.wk_sun) {
57            let wd = self.wkday.unwrap_or(Weekday::Sunday);
58            let jd = Dt::wk_sun_to_jd(y, w, wd);
59            return jd_to_naive_date(jd);
60        }
61
62        // Monday-based week number (%W)
63        if let (Some(y), Some(w)) = (self.yr, self.wk_mon) {
64            let wd = self.wkday.unwrap_or(Weekday::Monday);
65            let jd = Dt::wk_mon_to_jd(y, w, wd);
66            return jd_to_naive_date(jd);
67        }
68
69        Err(an_err!(DtErrKind::InvalidInput, "failed to convert"))
70    }
71
72    fn build_naive_time(&self) -> Result<NaiveTime, DtErr> {
73        let mut hour = self.hr.unwrap_or(0) as u32;
74        let minute = self.min.unwrap_or(0) as u32;
75        let mut second = self.sec.unwrap_or(0) as u32;
76
77        if let Some(meridiem) = self.meridiem {
78            match (hour, meridiem) {
79                (12, Meridiem::AM) => hour = 0,
80                (12, Meridiem::PM) => {}
81                (h, Meridiem::PM) if h < 12 => hour = h + 12,
82                _ => {}
83            }
84        }
85
86        let raw_ns_u64 = if let Some(attos) = self.attos {
87            attos / ATTOS_PER_NS
88        } else {
89            0
90        };
91
92        let is_leap = second == 60 || self.is_leap_sec;
93        if !is_leap && raw_ns_u64 > 999_999_999 {
94            return Err(an_err!(DtErrKind::OutOfRange, "leap ns: {}", raw_ns_u64));
95        }
96
97        let mut subsec_nano: u32 = if raw_ns_u64 > 1_999_999_999 {
98            1_999_999_999
99        } else {
100            raw_ns_u64 as u32
101        };
102
103        if is_leap {
104            second = 59;
105            subsec_nano = subsec_nano.saturating_add(1_000_000_000);
106            if subsec_nano > 1_999_999_999 {
107                subsec_nano = 1_999_999_999;
108            }
109        } else if second > 59 {
110            return Err(an_err!(DtErrKind::OutOfRange, "seconds: {}", second));
111        }
112
113        NaiveTime::from_hms_nano_opt(hour, minute, second, subsec_nano).ok_or_else(|| {
114            an_err!(
115                DtErrKind::InvalidInput,
116                "hms: {} {} {} {}",
117                hour,
118                minute,
119                second,
120                subsec_nano
121            )
122        })
123    }
124
125    /// Helper: resolve fixed offset / UTC only.
126    fn to_chrono_offset(&self) -> Result<FixedOffset, DtErr> {
127        match self.offset {
128            Some(Offset::Fixed(secs)) => FixedOffset::east_opt(secs)
129                .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset, "offset secs: {}", secs)),
130            Some(Offset::Utc) | Some(Offset::None) | None => FixedOffset::east_opt(0)
131                .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset, "offset secs: 0")),
132        }
133    }
134
135    /// Converts [`TimeParts`] → [`chrono::DateTime`].
136    /// - If this [`TimeParts`] has a unix timestamp then it is used
137    ///   instead of anything else, timezones are ignored in this route.
138    pub fn to_chrono_datetime(&self) -> Result<DateTime<FixedOffset>, DtErr> {
139        // ============================================================
140        // UNIX TIMESTAMP PATH
141        // Always UTC. Completely ignores offset + iana_name.
142        // ============================================================
143        if let Some(secs) = self.unix_timestamp_seconds {
144            let subsec_nano = if let Some(attos) = self.attos {
145                let ns_u64 = attos / ATTOS_PER_NS;
146                if ns_u64 > 999_999_999 {
147                    999_999_999
148                } else {
149                    ns_u64 as u32
150                }
151            } else {
152                0
153            };
154
155            let utc_dt = DateTime::from_timestamp(secs, subsec_nano)
156                .ok_or_else(|| an_err!(DtErrKind::InvalidNumber, "timestamp: {:?}", secs))?;
157            let offset = FixedOffset::east_opt(0)
158                .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset))?;
159            return Ok(utc_dt.with_timezone(&offset));
160        }
161
162        // ============================================================
163        // CIVIL TIME PATH (local time in the given zone)
164        // ============================================================
165        let naive = self.to_chrono_naive_datetime()?;
166
167        if let Some(name) = &self.iana_name {
168            let name_str = name.as_str().map_err(|e| {
169                an_err!(
170                    DtErrKind::InvalidBytes,
171                    "invalid iana ascii: {:?}: {}",
172                    name,
173                    e
174                )
175            })?;
176            if !name_str.is_empty() {
177                {
178                    let provisional_unix =
179                        DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive, chrono::Utc)
180                            .timestamp();
181
182                    match offset_for_local(name_str, provisional_unix) {
183                        Some(info) => {
184                            let mut local_naive = naive;
185
186                            if info.is_gap {
187                                // Non-existent time (spring-forward gap) — shift forward
188                                local_naive += chrono::Duration::seconds(info.gap_size as i64);
189                            }
190
191                            let offset = FixedOffset::east_opt(info.offset).ok_or_else(|| {
192                                an_err!(DtErrKind::InvalidTimezoneOffset, "offset: {}", info.offset)
193                            })?;
194
195                            return offset
196                                .from_local_datetime(&local_naive)
197                                .single()
198                                .ok_or_else(|| {
199                                    an_err!(
200                                        DtErrKind::InvalidTimezoneOffset,
201                                        "offset: {:?}",
202                                        offset
203                                    )
204                                });
205                        }
206                        None => {
207                            return Err(an_err!(
208                                DtErrKind::InvalidTimezoneOffset,
209                                "invalid iana: {}",
210                                name_str
211                            ));
212                        }
213                    }
214                }
215            }
216        }
217
218        // Fixed-offset civil path
219        let offset = self.to_chrono_offset()?;
220        offset
221            .from_local_datetime(&naive)
222            .single()
223            .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset, "offset: {:?}", offset))
224    }
225
226    /// Converts [`TimeParts`] → [`i64`].
227    /// - If this [`TimeParts`] has a unix timestamp then it is used
228    ///   instead of anything else, timezones are ignored in this route.
229    /// - Uses [`TimeParts::to_chrono_datetime`] internally.
230    pub fn to_chrono_timestamp(&self) -> Result<i64, DtErr> {
231        if let Some(secs) = self.unix_timestamp_seconds {
232            return Ok(secs);
233        }
234        let dt = self.to_chrono_datetime()?;
235        Ok(dt.timestamp())
236    }
237}