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 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 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 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 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 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 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 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 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 pub fn to_chrono_datetime(&self) -> Result<DateTime<FixedOffset>, DtErr> {
138 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 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 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 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 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 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 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}