Skip to main content

deep_time/time_parts/
to_chrono.rs

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