#![allow(dead_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, warn(missing_docs))]
#[cfg(all(doc, feature = "chrono"))]
use chrono::NaiveDate;
pub(crate) mod common;
pub(crate) mod helpers;
mod level0;
#[allow(missing_docs)]
mod level2;
pub mod level_1;
use common::{UnvalidatedTime, UnvalidatedTz};
pub use level0::api as level_0;
#[doc(hidden)]
pub use level2::api as level_2;
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
mod chrono_interop;
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
mod serde_interop;
use core::convert::TryInto;
use core::num::NonZeroU8;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
OutOfRange,
Invalid,
}
impl std::error::Error for ParseError {}
use core::fmt;
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[allow(rustdoc::broken_intra_doc_links)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct DateTime {
pub(crate) date: DateComplete,
pub(crate) time: Time,
}
#[cfg(feature = "chrono")]
fn chrono_tz_datetime<Tz: chrono::TimeZone>(
tz: &Tz,
date: &DateComplete,
time: &Time,
) -> chrono::DateTime<Tz> {
tz.ymd(date.year, date.month.get() as u32, date.day.get() as u32)
.and_hms(time.hh as u32, time.mm as u32, time.ss as u32)
}
impl DateTime {
pub fn date(&self) -> DateComplete {
self.date
}
pub fn time(&self) -> Time {
self.time
}
#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
pub fn offset(&self) -> TzOffset {
self.time.offset()
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
pub fn to_chrono_naive(&self) -> chrono::NaiveDateTime {
let date = self.date.to_chrono();
let time = self.time.to_chrono_naive();
date.and_time(time)
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
pub fn to_chrono<Tz>(&self, tz: &Tz) -> chrono::DateTime<Tz>
where
Tz: chrono::TimeZone,
{
let DateTime { date, time } = self;
match time.tz {
TzOffset::Unspecified => chrono_tz_datetime(tz, date, time),
TzOffset::Utc => {
let utc = chrono_tz_datetime(&chrono::Utc, date, time);
utc.with_timezone(tz)
}
TzOffset::Hours(hours) => {
let fixed_zone = chrono::FixedOffset::east_opt(hours * 3600)
.expect("time zone offset out of bounds");
let fixed_dt = chrono_tz_datetime(&fixed_zone, date, time);
fixed_dt.with_timezone(tz)
}
TzOffset::Minutes(signed_min) => {
let fixed_zone = chrono::FixedOffset::east_opt(signed_min * 60)
.expect("time zone offset out of bounds");
let fixed_dt = chrono_tz_datetime(&fixed_zone, date, time);
fixed_dt.with_timezone(tz)
}
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct DateComplete {
pub(crate) year: i32,
pub(crate) month: NonZeroU8,
pub(crate) day: NonZeroU8,
}
impl fmt::Debug for DateComplete {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<Self as fmt::Display>::fmt(self, f)
}
}
impl fmt::Display for DateComplete {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let DateComplete { year, month, day } = *self;
let sign = helpers::sign_str_if_neg(year);
let year = year.abs();
write!(f, "{}{:04}", sign, year)?;
write!(f, "-{:02}", month)?;
write!(f, "-{:02}", day)?;
Ok(())
}
}
impl DateComplete {
pub fn from_ymd(year: i32, month: u32, day: u32) -> Self {
Self::from_ymd_opt(year, month, day).expect("invalid complete date")
}
pub fn from_ymd_opt(year: i32, month: u32, day: u32) -> Option<Self> {
let month = month.try_into().ok().map(NonZeroU8::new)??;
let day = day.try_into().ok().map(NonZeroU8::new)??;
Self { year, month, day }.validate().ok()
}
pub fn year(&self) -> i32 {
self.year
}
pub fn month(&self) -> u32 {
self.month.get() as u32
}
pub fn day(&self) -> u32 {
self.day.get() as u32
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Time {
pub(crate) hh: u8,
pub(crate) mm: u8,
pub(crate) ss: u8,
pub(crate) tz: TzOffset,
}
impl Time {
pub fn from_hmsz(hh: u32, mm: u32, ss: u32, tz: TzOffset) -> Self {
Self::from_hmsz_opt(hh, mm, ss, tz).expect("out of range in Time::from_hmsz")
}
pub fn from_hmsz_opt(hh: u32, mm: u32, ss: u32, tz: TzOffset) -> Option<Self> {
let unval = UnvalidatedTime {
hh: hh.try_into().ok()?,
mm: mm.try_into().ok()?,
ss: ss.try_into().ok()?,
tz: UnvalidatedTz::Unspecified,
};
let mut time = unval.validate().ok()?;
let tz = match tz {
TzOffset::Unspecified => tz,
TzOffset::Hours(x) if x.abs() < 24 => tz,
TzOffset::Minutes(x) if x.abs() < 24 * 60 => tz,
TzOffset::Utc => tz,
_ => return None,
};
time.tz = tz;
Some(time)
}
pub fn hour(&self) -> u32 {
self.hh as u32
}
pub fn minute(&self) -> u32 {
self.mm as u32
}
pub fn second(&self) -> u32 {
self.ss as u32
}
#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
pub fn offset(&self) -> TzOffset {
self.tz
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
pub fn to_chrono_naive(&self) -> chrono::NaiveTime {
chrono::NaiveTime::from_hms(self.hour(), self.minute(), self.second())
}
}
#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum TzOffset {
#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
Unspecified,
Utc,
Hours(i32),
Minutes(i32),
}
#[cfg_attr(not(feature = "chrono"), allow(rustdoc::broken_intra_doc_links))]
pub trait GetTimezone {
fn tz_offset(&self) -> TzOffset;
}