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
10#[cfg(feature = "chrono-tz")]
11use chrono_tz::Tz;
12
13#[cfg(not(feature = "chrono-tz"))]
14use crate::tzdb::offset_info_at_local;
15
16impl TimeParts {
17 pub fn to_chrono_naive_datetime(&self) -> Result<NaiveDateTime, DtErr> {
19 let date = self.build_naive_date()?;
20 let time = self.build_naive_time()?;
21
22 Ok(date.and_time(time))
23 }
24
25 fn build_naive_date(&self) -> Result<NaiveDate, DtErr> {
26 if let (Some(y), Some(m), Some(d)) = (self.year, self.month, self.day) {
28 let year_i32: i32 = y
29 .try_into()
30 .map_err(|e| an_err!(DtErrKind::InvalidNumber, "year: {}: {}", y, e))?;
31 return NaiveDate::from_ymd_opt(year_i32, m as u32, d as u32)
32 .ok_or_else(|| an_err!(DtErrKind::InvalidInput, "ymd: {}-{}-{}", year_i32, m, d));
33 }
34
35 if let (Some(y), Some(doy)) = (self.year, self.day_of_year) {
37 let year_i32: i32 = y
38 .try_into()
39 .map_err(|e| an_err!(DtErrKind::InvalidNumber, "year: {}: {}", y, e))?;
40 return NaiveDate::from_yo_opt(year_i32, doy as u32)
41 .ok_or_else(|| an_err!(DtErrKind::InvalidInput, "ydoy: {}-{}", y, doy));
42 }
43
44 let jdn_to_naive_date = |jdn: i64| -> Result<NaiveDate, DtErr> {
46 let days_from_ce: i32 = (jdn - 1721425)
47 .try_into()
48 .map_err(|e| an_err!(DtErrKind::InvalidInput, "jdn: {}: {}", jdn, e))?;
49 NaiveDate::from_num_days_from_ce_opt(days_from_ce)
50 .ok_or_else(|| an_err!(DtErrKind::InvalidInput, "days_from_ce: {}", days_from_ce))
51 };
52
53 if let (Some(iso_y), Some(w)) = (self.iso_week_year, self.iso_week) {
55 let wd = self.weekday.unwrap_or(Weekday::Monday);
56 let jdn = Dt::ymd_to_jdn_from_iso_week(iso_y, w, wd);
57 return jdn_to_naive_date(jdn);
58 }
59
60 if let (Some(y), Some(w)) = (self.year, self.week_sun) {
62 let wd = self.weekday.unwrap_or(Weekday::Sunday);
63 let jdn = Dt::ymd_to_jdn_from_week_sun(y, w, wd);
64 return jdn_to_naive_date(jdn);
65 }
66
67 if let (Some(y), Some(w)) = (self.year, self.week_mon) {
69 let wd = self.weekday.unwrap_or(Weekday::Monday);
70 let jdn = Dt::ymd_to_jdn_from_week_mon(y, w, wd);
71 return jdn_to_naive_date(jdn);
72 }
73
74 Err(an_err!(DtErrKind::InvalidInput, "failed to convert"))
75 }
76
77 fn build_naive_time(&self) -> Result<NaiveTime, DtErr> {
78 let mut hour = self.hour.unwrap_or(0) as u32;
79 let minute = self.minute.unwrap_or(0) as u32;
80 let mut second = self.second.unwrap_or(0) as u32;
81
82 if let Some(meridiem) = self.meridiem {
83 match (hour, meridiem) {
84 (12, Meridiem::AM) => hour = 0,
85 (12, Meridiem::PM) => {}
86 (h, Meridiem::PM) if h < 12 => hour = h + 12,
87 _ => {}
88 }
89 }
90
91 let raw_ns_u64 = if let Some(attos) = self.attos {
92 attos / ATTOS_PER_NS
93 } else {
94 0
95 };
96
97 let is_leap = second == 60 || self.is_leap_second;
98 if !is_leap && raw_ns_u64 > 999_999_999 {
99 return Err(an_err!(DtErrKind::OutOfRange, "leap ns: {}", raw_ns_u64));
100 }
101
102 let mut subsec_nano: u32 = if raw_ns_u64 > 1_999_999_999 {
103 1_999_999_999
104 } else {
105 raw_ns_u64 as u32
106 };
107
108 if is_leap {
109 second = 59;
110 subsec_nano = subsec_nano.saturating_add(1_000_000_000);
111 if subsec_nano > 1_999_999_999 {
112 subsec_nano = 1_999_999_999;
113 }
114 } else if second > 59 {
115 return Err(an_err!(DtErrKind::OutOfRange, "seconds: {}", second));
116 }
117
118 NaiveTime::from_hms_nano_opt(hour, minute, second, subsec_nano).ok_or_else(|| {
119 an_err!(
120 DtErrKind::InvalidInput,
121 "hms: {} {} {} {}",
122 hour,
123 minute,
124 second,
125 subsec_nano
126 )
127 })
128 }
129
130 fn to_chrono_offset(&self) -> Result<FixedOffset, DtErr> {
132 match self.offset {
133 Some(Offset::Fixed(secs)) => FixedOffset::east_opt(secs)
134 .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset, "offset secs: {}", secs)),
135 Some(Offset::Utc) | Some(Offset::None) | None => FixedOffset::east_opt(0)
136 .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset, "offset secs: 0")),
137 }
138 }
139
140 pub fn to_chrono_datetime(&self) -> Result<DateTime<FixedOffset>, DtErr> {
148 if let Some(secs) = self.unix_timestamp_seconds {
153 let subsec_nano = if let Some(attos) = self.attos {
154 let ns_u64 = attos / ATTOS_PER_NS;
155 if ns_u64 > 999_999_999 {
156 999_999_999
157 } else {
158 ns_u64 as u32
159 }
160 } else {
161 0
162 };
163
164 let utc_dt = DateTime::from_timestamp(secs, subsec_nano)
165 .ok_or_else(|| an_err!(DtErrKind::InvalidNumber, "timestamp: {:?}", secs))?;
166 let offset = FixedOffset::east_opt(0)
167 .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset))?;
168 return Ok(utc_dt.with_timezone(&offset));
169 }
170
171 let naive = self.to_chrono_naive_datetime()?;
175
176 if let Some(name) = &self.iana_name {
177 let name_str = name.as_str().map_err(|e| {
178 an_err!(
179 DtErrKind::InvalidBytes,
180 "invalid iana ascii: {:?}: {}",
181 name,
182 e
183 )
184 })?;
185 if !name_str.is_empty() {
186 #[cfg(feature = "chrono-tz")]
187 {
188 let tz: Tz = name_str.parse().map_err(|e| {
189 an_err!(
190 DtErrKind::InvalidTimezoneOffset,
191 "unknown IANA '{}': {}",
192 name_str,
193 e
194 )
195 })?;
196 let local = tz.from_local_datetime(&naive);
197 let zoned = local.single().ok_or_else(|| {
198 an_err!(
199 DtErrKind::InvalidTimezoneOffset,
200 "local time is ambiguous or invalid in timezone '{}'",
201 name_str
202 )
203 })?;
204 return Ok(zoned.fixed_offset());
205 }
206
207 #[cfg(not(feature = "chrono-tz"))]
208 {
209 let provisional_unix =
210 DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive, chrono::Utc)
211 .timestamp();
212
213 match crate::tzdb::offset_info_at_local(name_str, provisional_unix) {
214 Some(info) => {
215 let mut local_naive = naive;
216
217 if info.is_gap {
218 local_naive += chrono::Duration::seconds(info.gap_size as i64);
220 }
221
222 let offset = FixedOffset::east_opt(info.offset).ok_or_else(|| {
223 an_err!(DtErrKind::InvalidTimezoneOffset, "offset: {}", info.offset)
224 })?;
225
226 return offset
227 .from_local_datetime(&local_naive)
228 .single()
229 .ok_or_else(|| {
230 an_err!(
231 DtErrKind::InvalidTimezoneOffset,
232 "offset: {:?}",
233 offset
234 )
235 });
236 }
237 None => {
238 return Err(an_err!(
239 DtErrKind::InvalidTimezoneOffset,
240 "invalid iana: {}",
241 name_str
242 ));
243 }
244 }
245 }
246 }
247 }
248
249 let offset = self.to_chrono_offset()?;
251 offset
252 .from_local_datetime(&naive)
253 .single()
254 .ok_or_else(|| an_err!(DtErrKind::InvalidTimezoneOffset, "offset: {:?}", offset))
255 }
256
257 pub fn to_chrono_timestamp(&self) -> Result<i64, DtErr> {
266 if let Some(secs) = self.unix_timestamp_seconds {
267 return Ok(secs);
268 }
269 let dt = self.to_chrono_datetime()?;
270 Ok(dt.timestamp())
271 }
272}