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