1#![allow(dead_code)]
8#![cfg_attr(docsrs, feature(doc_cfg))]
9#![doc = include_str!("../README.md")]
10#![cfg_attr(docsrs, warn(missing_docs))]
12
13#[cfg(all(doc, feature = "chrono"))]
14use chrono::NaiveDate;
15
16pub(crate) mod common;
17pub(crate) mod helpers;
18mod level0;
19#[allow(missing_docs)]
20mod level2;
21pub mod level_1;
22use common::{UnvalidatedTime, UnvalidatedTz};
23pub use level0::api as level_0;
24#[doc(hidden)]
25pub use level2::api as level_2;
26
27#[cfg(feature = "chrono")]
28#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
29mod chrono_interop;
30
31#[cfg(feature = "serde")]
32#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
33mod serde_interop;
34
35use core::convert::TryInto;
36use core::num::NonZeroU8;
37
38#[derive(Debug, Copy, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum ParseError {
42 OutOfRange,
44
45 Invalid,
47}
48
49impl std::error::Error for ParseError {}
50
51use core::fmt;
52impl fmt::Display for ParseError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(f, "{:?}", self)
55 }
56}
57
58#[allow(rustdoc::broken_intra_doc_links)]
59#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
71pub struct DateTime {
72 pub(crate) date: DateComplete,
73 pub(crate) time: Time,
74}
75
76#[cfg(feature = "chrono")]
77fn chrono_tz_datetime<Tz: chrono::TimeZone>(
78 tz: &Tz,
79 date: &DateComplete,
80 time: &Time,
81) -> chrono::DateTime<Tz> {
82 tz.ymd(date.year, date.month.get() as u32, date.day.get() as u32)
83 .and_hms(time.hh as u32, time.mm as u32, time.ss as u32)
84}
85
86impl DateTime {
87 pub fn date(&self) -> DateComplete {
89 self.date
90 }
91 pub fn time(&self) -> Time {
93 self.time
94 }
95
96 #[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
97 pub fn offset(&self) -> TzOffset {
102 self.time.offset()
103 }
104
105 #[cfg(feature = "chrono")]
106 #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
107 #[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
108 pub fn to_chrono_naive(&self) -> chrono::NaiveDateTime {
110 let date = self.date.to_chrono();
111 let time = self.time.to_chrono_naive();
112 date.and_time(time)
113 }
114
115 #[cfg(feature = "chrono")]
130 #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
131 pub fn to_chrono<Tz>(&self, tz: &Tz) -> chrono::DateTime<Tz>
132 where
133 Tz: chrono::TimeZone,
134 {
135 let DateTime { date, time } = self;
136 match time.tz {
137 TzOffset::Unspecified => chrono_tz_datetime(tz, date, time),
138 TzOffset::Utc => {
139 let utc = chrono_tz_datetime(&chrono::Utc, date, time);
140 utc.with_timezone(tz)
141 }
142 TzOffset::Hours(hours) => {
143 let fixed_zone = chrono::FixedOffset::east_opt(hours * 3600)
144 .expect("time zone offset out of bounds");
145 let fixed_dt = chrono_tz_datetime(&fixed_zone, date, time);
146 fixed_dt.with_timezone(tz)
147 }
148 TzOffset::Minutes(signed_min) => {
149 let fixed_zone = chrono::FixedOffset::east_opt(signed_min * 60)
150 .expect("time zone offset out of bounds");
151 let fixed_dt = chrono_tz_datetime(&fixed_zone, date, time);
152 fixed_dt.with_timezone(tz)
153 }
154 }
155 }
156}
157
158#[derive(Copy, Clone, PartialEq, Eq, Hash)]
161pub struct DateComplete {
162 pub(crate) year: i32,
163 pub(crate) month: NonZeroU8,
164 pub(crate) day: NonZeroU8,
165}
166
167impl fmt::Debug for DateComplete {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 <Self as fmt::Display>::fmt(self, f)
170 }
171}
172impl fmt::Display for DateComplete {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 let DateComplete { year, month, day } = *self;
175 let sign = helpers::sign_str_if_neg(year);
176 let year = year.abs();
177 write!(f, "{}{:04}", sign, year)?;
178 write!(f, "-{:02}", month)?;
179 write!(f, "-{:02}", day)?;
180 Ok(())
181 }
182}
183
184impl DateComplete {
185 pub fn from_ymd(year: i32, month: u32, day: u32) -> Self {
187 Self::from_ymd_opt(year, month, day).expect("invalid complete date")
188 }
189 pub fn from_ymd_opt(year: i32, month: u32, day: u32) -> Option<Self> {
193 let month = month.try_into().ok().map(NonZeroU8::new)??;
194 let day = day.try_into().ok().map(NonZeroU8::new)??;
195 Self { year, month, day }.validate().ok()
196 }
197
198 pub fn year(&self) -> i32 {
200 self.year
201 }
202 pub fn month(&self) -> u32 {
204 self.month.get() as u32
205 }
206 pub fn day(&self) -> u32 {
208 self.day.get() as u32
209 }
210}
211
212#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
214pub struct Time {
215 pub(crate) hh: u8,
216 pub(crate) mm: u8,
217 pub(crate) ss: u8,
218 pub(crate) tz: TzOffset,
219}
220
221impl Time {
222 pub fn from_hmsz(hh: u32, mm: u32, ss: u32, tz: TzOffset) -> Self {
224 Self::from_hmsz_opt(hh, mm, ss, tz).expect("out of range in Time::from_hmsz")
225 }
226 pub fn from_hmsz_opt(hh: u32, mm: u32, ss: u32, tz: TzOffset) -> Option<Self> {
228 let unval = UnvalidatedTime {
229 hh: hh.try_into().ok()?,
230 mm: mm.try_into().ok()?,
231 ss: ss.try_into().ok()?,
232 tz: UnvalidatedTz::Unspecified,
233 };
234 let mut time = unval.validate().ok()?;
235 let tz = match tz {
236 TzOffset::Unspecified => tz,
237 TzOffset::Hours(x) if x.abs() < 24 => tz,
238 TzOffset::Minutes(x) if x.abs() < 24 * 60 => tz,
239 TzOffset::Utc => tz,
240 _ => return None,
241 };
242 time.tz = tz;
243 Some(time)
244 }
245 pub fn hour(&self) -> u32 {
247 self.hh as u32
248 }
249 pub fn minute(&self) -> u32 {
251 self.mm as u32
252 }
253 pub fn second(&self) -> u32 {
255 self.ss as u32
256 }
257 #[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
258 pub fn offset(&self) -> TzOffset {
263 self.tz
264 }
265 #[cfg(feature = "chrono")]
266 #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
267 pub fn to_chrono_naive(&self) -> chrono::NaiveTime {
269 chrono::NaiveTime::from_hms(self.hour(), self.minute(), self.second())
270 }
271}
272
273#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
274#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
280pub enum TzOffset {
281 #[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
282 Unspecified,
288 Utc,
290 Hours(i32),
298 Minutes(i32),
301}
302
303#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
304pub trait GetTimezone {
312 fn tz_offset(&self) -> TzOffset;
319}