mod abacus;
mod fancy_fmt;
#[cfg(feature = "local")]
#[cfg_attr(docsrs, doc(cfg(feature = "local")))]
pub(super) mod local;
use crate::{
DateChar,
DAY_IN_SECONDS,
HOUR_IN_SECONDS,
JULIAN_OFFSET,
macros,
MINUTE_IN_SECONDS,
Month,
Period,
unixtime,
Utc2kError,
Utc2kFormatError,
Weekday,
Year,
YEAR_IN_DAYS_P2,
YEAR_IN_DAYS_P4,
};
use std::{
borrow::Cow,
cmp::Ordering,
ffi::OsStr,
fmt,
ops::{
Add,
AddAssign,
Sub,
SubAssign,
},
str::FromStr,
};
use abacus::Abacus;
#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct FmtUtc2k([DateChar; 19]);
impl AsRef<[u8]> for FmtUtc2k {
#[inline]
fn as_ref(&self) -> &[u8] { self.as_bytes() }
}
macros::as_ref_borrow_cast!(FmtUtc2k: as_str str);
impl Default for FmtUtc2k {
#[inline]
fn default() -> Self { Self::MIN }
}
macros::display_str!(as_str FmtUtc2k);
impl From<u32> for FmtUtc2k {
#[inline]
fn from(src: u32) -> Self { Self::from(Utc2k::from_unixtime(src)) }
}
impl From<&Utc2k> for FmtUtc2k {
#[inline]
fn from(src: &Utc2k) -> Self { Self::from(*src) }
}
impl From<Utc2k> for FmtUtc2k {
#[inline]
fn from(src: Utc2k) -> Self { Self::from_utc2k(src) }
}
impl From<FmtUtc2k> for String {
#[inline]
fn from(src: FmtUtc2k) -> Self { src.as_str().to_owned() }
}
impl FromStr for FmtUtc2k {
type Err = Utc2kError;
#[inline]
fn from_str(src: &str) -> Result<Self, Self::Err> { Self::try_from(src) }
}
impl PartialEq<str> for FmtUtc2k {
#[inline]
fn eq(&self, other: &str) -> bool { self.as_str() == other }
}
impl PartialEq<FmtUtc2k> for str {
#[inline]
fn eq(&self, other: &FmtUtc2k) -> bool { <FmtUtc2k as PartialEq<Self>>::eq(other, self) }
}
macro_rules! fmt_eq {
($($ty:ty)+) => ($(
impl PartialEq<$ty> for FmtUtc2k {
#[inline]
fn eq(&self, other: &$ty) -> bool { <Self as PartialEq<str>>::eq(self, other) }
}
impl PartialEq<FmtUtc2k> for $ty {
#[inline]
fn eq(&self, other: &FmtUtc2k) -> bool { <FmtUtc2k as PartialEq<str>>::eq(other, self) }
}
)+);
}
fmt_eq! { &str &String String &Cow<'_, str> Cow<'_, str> &Box<str> Box<str> }
macro_rules! fmt_try_from {
($($ty:ty)+) => ($(
impl TryFrom<$ty> for FmtUtc2k {
type Error = Utc2kError;
#[inline]
fn try_from(src: $ty) -> Result<Self, Self::Error> {
Utc2k::try_from(src).map(Self::from)
}
}
)+);
}
fmt_try_from! { &[u8] &OsStr &str }
impl FmtUtc2k {
pub const MIN: Self = Self([
DateChar::Digit2, DateChar::Digit0, DateChar::Digit0, DateChar::Digit0,
DateChar::Dash,
DateChar::Digit0, DateChar::Digit1,
DateChar::Dash,
DateChar::Digit0, DateChar::Digit1,
DateChar::Space,
DateChar::Digit0, DateChar::Digit0,
DateChar::Colon,
DateChar::Digit0, DateChar::Digit0,
DateChar::Colon,
DateChar::Digit0, DateChar::Digit0,
]);
pub const MAX: Self = Self([
DateChar::Digit2, DateChar::Digit0, DateChar::Digit9, DateChar::Digit9,
DateChar::Dash,
DateChar::Digit1, DateChar::Digit2,
DateChar::Dash,
DateChar::Digit3, DateChar::Digit1,
DateChar::Space,
DateChar::Digit2, DateChar::Digit3,
DateChar::Colon,
DateChar::Digit5, DateChar::Digit9,
DateChar::Colon,
DateChar::Digit5, DateChar::Digit9,
]);
pub const LEN: usize = 19;
}
impl FmtUtc2k {
#[must_use]
#[inline]
pub const fn from_ascii(src: &[u8]) -> Option<Self> {
if let Some(parts) = Utc2k::from_ascii(src) {
Some(Self::from_utc2k(parts))
}
else { None }
}
#[must_use]
#[inline]
pub const fn from_rfc2822(src: &[u8]) -> Option<Self> {
if let Some(parts) = Utc2k::from_rfc2822(src) {
Some(Self::from_utc2k(parts))
}
else { None }
}
#[must_use]
#[inline]
pub const fn from_unixtime(src: u32) -> Self {
Self::from_utc2k(Utc2k::from_unixtime(src))
}
#[must_use]
#[inline]
pub fn now() -> Self { Self::from_utc2k(Utc2k::now()) }
#[inline]
pub const fn set_datetime(&mut self, src: Utc2k) {
self.set_parts_unchecked(src.y, src.m, src.d, src.hh, src.mm, src.ss);
}
pub const fn set_parts(&mut self, y: u16, m: u8, d: u8, hh: u8, mm: u8, ss: u8) {
let (y, m, d, hh, mm, ss) = Abacus::new(y, m, d, hh, mm, ss).parts();
self.set_parts_unchecked(y, m, d, hh, mm, ss);
}
#[inline]
pub fn set_unixtime(&mut self, src: u32) { self.set_datetime(Utc2k::from(src)); }
}
impl FmtUtc2k {
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] { DateChar::as_bytes(self.0.as_slice()) }
#[inline]
#[must_use]
pub const fn as_str(&self) -> &str { DateChar::as_str(self.0.as_slice()) }
#[inline]
#[must_use]
pub const fn date(&self) -> &str {
let (out, _) = self.0.split_at(10);
DateChar::as_str(out)
}
#[inline]
#[must_use]
pub const fn year(&self) -> &str {
let (out, _) = self.0.split_at(4);
DateChar::as_str(out)
}
#[inline]
#[must_use]
pub const fn time(&self) -> &str {
let (_, out) = self.0.split_at(11);
DateChar::as_str(out)
}
}
impl FmtUtc2k {
#[must_use]
pub fn to_rfc2822(&self) -> String {
let utc = Utc2k::from_fmtutc2k(*self);
let mut out = String::with_capacity(31);
out.push_str(utc.weekday().abbreviation());
out.push_str(", ");
out.push(self.0[8].as_char());
out.push(self.0[9].as_char());
out.push(' ');
out.push_str(utc.month().abbreviation());
out.push(' ');
out.push_str(self.year());
out.push(' ');
out.push_str(self.time());
out.push_str(" +0000");
out
}
#[must_use]
pub fn to_rfc3339(&self) -> String {
let mut out = String::with_capacity(20);
out.push_str(self.date());
out.push('T');
out.push_str(self.time());
out.push('Z');
out
}
}
impl FmtUtc2k {
#[must_use]
const fn from_utc2k(src: Utc2k) -> Self {
Self([
DateChar::Digit2, DateChar::Digit0, DateChar::from_digit(src.y as u8 / 10), DateChar::from_digit(src.y as u8),
DateChar::Dash,
DateChar::from_digit(src.m as u8 / 10), DateChar::from_digit(src.m as u8),
DateChar::Dash,
DateChar::from_digit(src.d / 10), DateChar::from_digit(src.d),
DateChar::Space,
DateChar::from_digit(src.hh / 10), DateChar::from_digit(src.hh),
DateChar::Colon,
DateChar::from_digit(src.mm / 10), DateChar::from_digit(src.mm),
DateChar::Colon,
DateChar::from_digit(src.ss / 10), DateChar::from_digit(src.ss)
])
}
const fn set_parts_unchecked(&mut self, y: Year, m: Month, d: u8, hh: u8, mm: u8, ss: u8) {
self.0[2] = DateChar::from_digit(y as u8 / 10);
self.0[3] = DateChar::from_digit(y as u8);
self.0[5] = DateChar::from_digit(m as u8 / 10);
self.0[6] = DateChar::from_digit(m as u8);
self.0[8] = DateChar::from_digit(d / 10);
self.0[9] = DateChar::from_digit(d);
self.0[11] = DateChar::from_digit(hh / 10);
self.0[12] = DateChar::from_digit(hh);
self.0[14] = DateChar::from_digit(mm / 10);
self.0[15] = DateChar::from_digit(mm);
self.0[17] = DateChar::from_digit(ss / 10);
self.0[18] = DateChar::from_digit(ss);
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub struct Utc2k {
y: Year,
m: Month,
d: u8,
hh: u8,
mm: u8,
ss: u8,
}
impl Add<u32> for Utc2k {
type Output = Self;
#[inline]
fn add(self, other: u32) -> Self {
Self::from_abacus(Abacus::from_utc2k(self).plus_seconds(other))
}
}
impl AddAssign<u32> for Utc2k {
#[inline]
fn add_assign(&mut self, other: u32) { *self = *self + other; }
}
impl Default for Utc2k {
#[inline]
fn default() -> Self { Self::MIN }
}
impl fmt::Display for Utc2k {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<FmtUtc2k as fmt::Display>::fmt(&FmtUtc2k::from_utc2k(*self), f)
}
}
impl From<u32> for Utc2k {
#[inline]
fn from(src: u32) -> Self { Self::from_unixtime(src) }
}
impl From<&FmtUtc2k> for Utc2k {
#[inline]
fn from(src: &FmtUtc2k) -> Self { Self::from(*src) }
}
impl From<FmtUtc2k> for Utc2k {
#[inline]
fn from(src: FmtUtc2k) -> Self { Self::from_fmtutc2k(src) }
}
impl From<Utc2k> for String {
#[inline]
fn from(src: Utc2k) -> Self { Self::from(FmtUtc2k::from_utc2k(src)) }
}
impl FromStr for Utc2k {
type Err = Utc2kError;
#[inline]
fn from_str(src: &str) -> Result<Self, Self::Err> { Self::try_from(src) }
}
impl Ord for Utc2k {
#[inline]
fn cmp(&self, other: &Self) -> Ordering { Self::cmp(*self, *other) }
}
impl PartialOrd for Utc2k {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Sub<u32> for Utc2k {
type Output = Self;
#[inline]
fn sub(self, other: u32) -> Self {
Self::from_unixtime(self.unixtime().saturating_sub(other))
}
}
impl SubAssign<u32> for Utc2k {
#[inline]
fn sub_assign(&mut self, other: u32) { *self = *self - other; }
}
impl TryFrom<&OsStr> for Utc2k {
type Error = Utc2kError;
#[inline]
fn try_from(src: &OsStr) -> Result<Self, Self::Error> {
let src: &str = src.to_str().ok_or(Utc2kError::Invalid)?;
Self::try_from(src)
}
}
impl TryFrom<&[u8]> for Utc2k {
type Error = Utc2kError;
#[inline]
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
Abacus::from_ascii(src)
.map(Self::from_abacus)
.ok_or(Utc2kError::Invalid)
}
}
impl TryFrom<&str> for Utc2k {
type Error = Utc2kError;
#[inline]
fn try_from(src: &str) -> Result<Self, Self::Error> {
Self::try_from(src.as_bytes())
}
}
impl From<Utc2k> for u32 {
#[inline]
fn from(src: Utc2k) -> Self { src.unixtime() }
}
impl Utc2k {
pub const MIN: Self = Self { y: Year::Y2k00, m: Month::January, d: 1, hh: 0, mm: 0, ss: 0 };
pub const MAX: Self = Self { y: Year::Y2k99, m: Month::December, d: 31, hh: 23, mm: 59, ss: 59 };
pub const MIN_UNIXTIME: u32 = 946_684_800;
pub const MAX_UNIXTIME: u32 = 4_102_444_799;
}
impl Utc2k {
#[inline]
#[must_use]
pub const fn new(y: u16, m: u8, d: u8, hh: u8, mm: u8, ss: u8) -> Self {
Self::from_abacus(Abacus::new(y, m, d, hh, mm, ss))
}
#[must_use]
pub const fn from_ascii(src: &[u8]) -> Option<Self> {
if let Some(parts) = Abacus::from_ascii(src) {
Some(Self::from_abacus(parts))
}
else { None }
}
#[must_use]
pub const fn from_rfc2822(src: &[u8]) -> Option<Self> {
if let Some(parts) = Abacus::from_rfc2822(src) {
Some(Self::from_abacus(parts))
}
else { None }
}
#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
#[expect(clippy::many_single_char_names, reason = "For readability.")]
#[must_use]
pub const fn from_unixtime(src: u32) -> Self {
if Self::MIN_UNIXTIME < src {
if src < Self::MAX_UNIXTIME {
let z = src.wrapping_div(DAY_IN_SECONDS) + JULIAN_OFFSET;
let h: u32 = 100 * z - 25;
let mut a: u32 = h.wrapping_div(YEAR_IN_DAYS_P4);
a -= a.wrapping_div(4);
let mut year: u32 = (100 * a + h).wrapping_div(YEAR_IN_DAYS_P2);
a = a + z - 365 * year - year.wrapping_div(4);
let mut month: u32 = (5 * a + 456).wrapping_div(153);
let d: u8 = (a - (153 * month - 457).wrapping_div(5)) as u8;
if 12 < month {
year += 1;
month -= 12;
}
let y = Year::from_u8((year - 2000) as u8);
let m = Month::from_u8(month as u8);
let mut src = src % DAY_IN_SECONDS;
let hh =
if let Some(more) = abacus::ss_split_off_hours(&mut src) {
more.get() as u8
}
else { 0 };
let mm =
if let Some(more) = abacus::ss_split_off_minutes(&mut src) {
more.get() as u8
}
else { 0 };
Self { y, m, d, hh, mm, ss: src as u8 }
}
else { Self::MAX }
}
else { Self::MIN }
}
#[inline]
#[must_use]
pub fn now() -> Self { Self::from_unixtime(unixtime()) }
#[inline]
#[must_use]
pub fn tomorrow() -> Self { Self::from_unixtime(unixtime() + DAY_IN_SECONDS) }
#[inline]
#[must_use]
pub fn yesterday() -> Self { Self::from_unixtime(unixtime() - DAY_IN_SECONDS) }
}
impl Utc2k {
#[inline]
#[must_use]
pub const fn parts(self) -> (u16, u8, u8, u8, u8, u8) {
(
self.year(),
self.m as u8,
self.d,
self.hh,
self.mm,
self.ss,
)
}
#[inline]
#[must_use]
pub const fn ymd(self) -> (u16, u8, u8) { (self.year(), self.m as u8, self.d) }
#[inline]
#[must_use]
pub const fn hms(self) -> (u8, u8, u8) { (self.hh, self.mm, self.ss) }
#[inline]
#[must_use]
pub const fn year(self) -> u16 { self.y.full() }
#[inline]
#[must_use]
pub const fn month(self) -> Month { self.m }
#[inline]
#[must_use]
pub const fn day(self) -> u8 { self.d }
#[inline]
#[must_use]
pub const fn hour(self) -> u8 { self.hh }
#[inline]
#[must_use]
pub const fn hour_12(self) -> u8 {
if self.hh == 0 { 12 }
else if 12 < self.hh { self.hh - 12 }
else { self.hh }
}
#[inline]
#[must_use]
pub const fn hour_period(self) -> Period {
if self.hh < 12 { Period::Am }
else { Period::Pm }
}
#[inline]
#[must_use]
pub const fn minute(self) -> u8 { self.mm }
#[inline]
#[must_use]
pub const fn second(self) -> u8 { self.ss }
}
impl Utc2k {
#[must_use]
pub const fn leap_year(self) -> bool { self.y.leap() }
#[must_use]
pub const fn month_size(self) -> u8 {
if matches!(self.m, Month::February) && self.y.leap() { 29 }
else { self.month().days() }
}
#[inline]
#[must_use]
pub const fn ordinal(self) -> u16 {
self.d as u16 +
self.m.ordinal() +
(2 < (self.m as u8) && self.y.leap()) as u16
}
#[inline]
#[must_use]
pub const fn seconds_from_midnight(self) -> u32 {
self.ss as u32 +
self.mm as u32 * MINUTE_IN_SECONDS +
self.hh as u32 * HOUR_IN_SECONDS
}
#[must_use]
pub const fn weekday(self) -> Weekday {
Weekday::from_u8(self.y.weekday() as u8 + ((self.ordinal() - 1) % 7) as u8)
}
}
impl Utc2k {
#[inline]
#[must_use]
pub const fn formatted(self) -> FmtUtc2k { FmtUtc2k::from_utc2k(self) }
#[must_use]
pub const fn to_midnight(self) -> Self {
Self {
y: self.y,
m: self.m,
d: self.d,
hh: 0,
mm: 0,
ss: 0,
}
}
#[must_use]
pub fn to_rfc2822(&self) -> String {
let mut out = String::with_capacity(31);
macro_rules! push {
($($expr:expr),+) => ($( out.push(((($expr) % 10) | b'0') as char); )+);
}
out.push_str(self.weekday().abbreviation());
out.push_str(", ");
push!(self.d / 10, self.d);
out.push(' ');
out.push_str(self.month().abbreviation());
out.push_str(self.y.as_str()); push!(self.hh / 10, self.hh);
out.push(':');
push!(self.mm / 10, self.mm);
out.push(':');
push!(self.ss / 10, self.ss);
out.push_str(" +0000");
out
}
#[inline]
#[must_use]
pub fn to_rfc3339(&self) -> String { FmtUtc2k::from_utc2k(*self).to_rfc3339() }
#[inline]
#[must_use]
pub const fn unixtime(self) -> u32 {
let time = self.y.unixtime() +
self.m.ordinal_seconds() +
self.seconds_from_midnight() +
DAY_IN_SECONDS * (self.d as u32 - 1);
if 2 < (self.m as u8) && self.y.leap() { time + DAY_IN_SECONDS }
else { time }
}
#[must_use]
pub const fn with_time(self, hh: u8, mm: u8, ss: u8) -> Self {
Self::from_abacus(Abacus::new(self.year(), self.m as u8, self.d, hh, mm, ss))
}
}
macro_rules! fancy_docs {
(
$(
(
$comp:literal
$desc1:literal $expected1:literal,
$( $prop:literal $desc:literal $expected:literal, )*
),
)+
) => (
concat!(
"| Component | Modifier | Description | Example |\n",
"| --------- | -------- | ----------- | ------- |\n",
$(
concat!(
"| `", $comp, "` | | ", $desc1, " | `\"", $expected1, "\"` |\n",
$(
concat!(
"| | `", $prop, "` | ", $desc, " | `\"", $expected, "\"` |\n",
),
)*
),
)+
"\n\n## Examples\n\n",
"```\n",
"use utc2k::Utc2k;\n\n",
$(
concat!(
"# assert_eq!(Utc2k::MIN.formatted_custom(\"[", $comp, "]\").unwrap(), \"", $expected1, "\");\n",
$(
concat!(
"# assert_eq!(Utc2k::MIN.formatted_custom(\"[", $comp, " ", $prop, "]\").unwrap(), \"", $expected, "\");\n",
),
)*
),
)+
)
)
}
impl Utc2k {
#[doc = fancy_docs!(
(
"year"
"Four-digit year." "2000",
"@2" "Two-digit year." "00",
"@2 @space" "Two-digit year, space-padded." " 0",
"@2 @trim" "Two-digit year, unpadded." "0",
),
(
"month"
"Two-digit month." "01",
"@space" "Two-digit month, space-padded." " 1",
"@trim" "Two-digit month, unpadded." "1",
"@name" "Month name." "January",
"@abbr" "Month abbreviation." "Jan",
),
(
"day"
"Two-digit day." "01",
"@space" "Two-digit day, space-padded." " 1",
"@trim" "Two-digit day, unpadded." "1",
"@name" "Weekday." "Saturday",
"@abbr" "Weekday abbreviation." "Sat",
),
(
"hour"
"Two-digit hour (24)." "00",
"@space" "Two-digit hour (24), space-padded." " 0",
"@trim" "Two-digit hour (24), unpadded." "0",
"@12" "Two-digit hour (12)." "12",
"@12 @space" "Two-digit hour (12), space-padded." "12",
"@12 @trim" "Two-digit hour (12), unpadded." "12",
),
(
"minute"
"Two-digit minute." "00",
"@space" "Two-digit minute, space-padded." " 0",
"@trim" "Two-digit minute, unpadded." "0",
),
(
"second"
"Two-digit second." "00",
"@space" "Two-digit second, space-padded." " 0",
"@trim" "Two-digit second, unpadded." "0",
),
(
"ordinal"
"Ordinal." "001",
"@space" "Ordinal, space-padded." " 1",
"@trim" "Ordinal, unpadded." "1",
),
(
"period"
"Lowercase." "am",
"@ap" "AP Style (punctuated)." "a.m.",
"@upper" "UPPERCASE." "AM",
),
(
"unixtime"
"Unix timestamp." "946684800",
),
)]
pub fn formatted_custom(self, fmt: &str) -> Result<String, Utc2kFormatError> {
fancy_fmt::Component::format_date(self, fmt)
}
}
impl Utc2k {
#[must_use]
pub const fn checked_add(self, secs: u32) -> Option<Self> {
if
let Some(s) = self.unixtime().checked_add(secs) &&
s <= Self::MAX_UNIXTIME
{
Some(Self::from_unixtime(s))
}
else { None }
}
pub const fn checked_from_ascii(src: &[u8]) -> Result<Self, Utc2kError> {
if let Some(parts) = Abacus::from_ascii(src) {
match parts.parts_checked() {
Ok((y, m, d, hh, mm, ss)) => Ok(Self { y, m, d, hh, mm, ss }),
Err(e) => Err(e),
}
}
else { Err(Utc2kError::Invalid) }
}
pub const fn checked_from_unixtime(src: u32) -> Result<Self, Utc2kError> {
if src < Self::MIN_UNIXTIME { Err(Utc2kError::Underflow) }
else if src > Self::MAX_UNIXTIME { Err(Utc2kError::Overflow) }
else { Ok(Self::from_unixtime(src)) }
}
#[must_use]
pub const fn checked_sub(self, secs: u32) -> Option<Self> {
if
let Some(s) = self.unixtime().checked_sub(secs) &&
Self::MIN_UNIXTIME <= s
{
Some(Self::from_unixtime(s))
}
else { None }
}
}
impl Utc2k {
#[must_use]
pub const fn abs_diff(self, other: Self) -> u32 {
self.unixtime().abs_diff(other.unixtime())
}
#[must_use]
pub const fn cmp(a: Self, b: Self) -> Ordering {
match Self::cmp_date(a, b) {
Ordering::Equal => Self::cmp_time(a, b),
cmp => cmp,
}
}
#[must_use]
pub const fn cmp_date(a: Self, b: Self) -> Ordering {
match Year::cmp(a.y, b.y) {
Ordering::Equal => match Month::cmp(a.m, b.m) {
Ordering::Equal =>
if a.d == b.d { Ordering::Equal }
else if a.d < b.d { Ordering::Less }
else { Ordering::Greater },
cmp => cmp,
},
cmp => cmp,
}
}
#[must_use]
pub const fn cmp_time(a: Self, b: Self) -> Ordering {
if a.hh == b.hh {
if a.mm == b.mm {
if a.ss == b.ss { Ordering::Equal }
else if a.ss < b.ss { Ordering::Less }
else { Ordering::Greater }
}
else if a.mm < b.mm { Ordering::Less }
else { Ordering::Greater }
}
else if a.hh < b.hh { Ordering::Less }
else { Ordering::Greater }
}
}
impl Utc2k {
#[must_use]
const fn from_abacus(src: Abacus) -> Self {
let (y, m, d, hh, mm, ss) = src.parts();
Self { y, m, d, hh, mm, ss }
}
#[must_use]
const fn from_fmtutc2k(src: FmtUtc2k) -> Self {
Self {
y: Year::from_u8(src.0[2].as_digit() * 10 + src.0[3].as_digit()),
m: Month::from_u8(src.0[5].as_digit() * 10 + src.0[6].as_digit()),
d: src.0[8].as_digit() * 10 + src.0[9].as_digit(),
hh: src.0[11].as_digit() * 10 + src.0[12].as_digit(),
mm: src.0[14].as_digit() * 10 + src.0[15].as_digit(),
ss: src.0[17].as_digit() * 10 + src.0[18].as_digit(),
}
}
#[must_use]
pub(crate) const fn from_ym(y: Year, m: Month) -> Self {
Self { y, m, d: 1, hh: 0, mm: 0, ss: 0 }
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::OffsetDateTime;
#[cfg(not(miri))]
const SAMPLE_SIZE: usize = 1_000_000;
#[cfg(miri)]
const SAMPLE_SIZE: usize = 1000;
macro_rules! range_test {
($i:ident, $buf:ident, $format:ident) => (
let u = Utc2k::from($i);
let f = FmtUtc2k::from(u);
let c = OffsetDateTime::from_unix_timestamp($i as i64)
.expect("Unable to create time::OffsetDateTime.");
assert_eq!($i, u.unixtime(), "Timestamp out does not match timestamp in!");
assert_eq!(
FmtUtc2k::from($i),
f,
"Fmt from Utc different than from {}", $i,
);
assert_eq!(
Utc2k::from(f),
u,
"Fmt/Utc back-and-forth failed for {}", $i,
);
assert_eq!(
Some(u),
Utc2k::from_ascii(f.as_bytes()),
"Fmt/Utc back-and-forth (bytes) failed for {}", $i,
);
assert_eq!(
Some(u),
Utc2k::from_rfc2822(u.to_rfc2822().as_bytes()),
"RFC2822 back-and-forth failed for {}", $i,
);
assert_eq!(
Some(u),
Utc2k::from_ascii(u.to_rfc3339().as_bytes()),
"RFC3339 back-and-forth failed for {}", $i,
);
assert_eq!(
Some(f),
FmtUtc2k::from_rfc2822(f.to_rfc2822().as_bytes()),
"Fmt RFC2822 back-and-forth failed for {}", $i,
);
assert_eq!(
Some(f),
FmtUtc2k::from_ascii(f.to_rfc3339().as_bytes()),
"Fmt RFC3339 back-and-forth failed for {}", $i,
);
assert_eq!(u.year(), c.year() as u16, "Year mismatch for unixtime {}", $i);
assert_eq!(u.month(), u8::from(c.month()), "Month mismatch for unixtime {}", $i);
assert_eq!(u.day(), c.day(), "Day mismatch for unixtime {}", $i);
assert_eq!(u.hour(), c.hour(), "Hour mismatch for unixtime {}", $i);
assert_eq!(u.minute(), c.minute(), "Minute mismatch for unixtime {}", $i);
assert_eq!(u.second(), c.second(), "Second mismatch for unixtime {}", $i);
assert_eq!(u.ordinal(), c.ordinal(), "Ordinal mismatch for unixtime {}", $i);
assert_eq!(u.weekday().as_str(), c.weekday().to_string());
assert_eq!(
u.leap_year(),
time::util::is_leap_year(u.year() as i32),
"Leap year mismatch for {}", u.year(),
);
$buf.truncate(0);
c.format_into(&mut $buf, &$format).expect("Unable to format datetime.");
assert_eq!(
Ok(f.as_str()),
std::str::from_utf8($buf.as_slice()),
"Date mismatch for unixtime {}", $i,
);
);
}
#[test]
fn limited_unixtime() {
let format = time::format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second]",
).expect("Unable to parse datetime format.");
let mut rng = fastrand::Rng::new();
let mut buf = Vec::new();
for i in std::iter::repeat_with(|| rng.u32(Utc2k::MIN_UNIXTIME..=Utc2k::MAX_UNIXTIME)).take(SAMPLE_SIZE).chain(std::iter::once(1_583_037_365)) {
range_test!(i, buf, format);
}
}
#[test]
fn t_leap_years() {
for y in 2000..2100 {
let date = Utc2k::new(y, 1, 1, 0, 0, 0);
assert_eq!(date.year(), y);
assert_eq!(
date.leap_year(),
y.trailing_zeros() >= 2 && (! y.is_multiple_of(100) || y.is_multiple_of(400))
);
}
}
#[test]
fn t_min_max() {
assert_eq!(Utc2k::MIN, Utc2k::from(Utc2k::MIN_UNIXTIME));
assert_eq!(Utc2k::MAX, Utc2k::from(Utc2k::MAX_UNIXTIME));
assert_eq!(Utc2k::MIN.unixtime(), Utc2k::MIN_UNIXTIME);
assert_eq!(Utc2k::MAX.unixtime(), Utc2k::MAX_UNIXTIME);
assert_eq!(Utc2k::MIN, Utc2k::from(FmtUtc2k::MIN));
assert_eq!(Utc2k::MAX, Utc2k::from(FmtUtc2k::MAX));
assert_eq!(FmtUtc2k::MIN, FmtUtc2k::from(Utc2k::MIN));
assert_eq!(FmtUtc2k::MAX, FmtUtc2k::from(Utc2k::MAX));
}
#[test]
fn t_ordering() {
let expected = vec![
Utc2k::try_from("2000-01-01 00:00:00").unwrap(),
Utc2k::try_from("2010-05-31 01:02:03").unwrap(),
Utc2k::try_from("2010-05-31 02:02:03").unwrap(),
Utc2k::try_from("2020-10-10 10:10:10").unwrap(),
Utc2k::try_from("2020-10-10 10:11:10").unwrap(),
Utc2k::try_from("2020-10-10 10:11:11").unwrap(),
];
let mut shuffled = vec![
Utc2k::try_from("2010-05-31 01:02:03").unwrap(),
Utc2k::try_from("2020-10-10 10:11:11").unwrap(),
Utc2k::try_from("2010-05-31 02:02:03").unwrap(),
Utc2k::try_from("2000-01-01 00:00:00").unwrap(),
Utc2k::try_from("2020-10-10 10:11:10").unwrap(),
Utc2k::try_from("2020-10-10 10:10:10").unwrap(),
];
let f_expected: Vec<FmtUtc2k> = expected.iter().copied().map(FmtUtc2k::from).collect();
let mut f_shuffled: Vec<FmtUtc2k> = shuffled.iter().copied().map(FmtUtc2k::from).collect();
assert_ne!(expected, shuffled);
assert_ne!(f_expected, f_shuffled);
shuffled.sort();
f_shuffled.sort();
assert_eq!(expected, shuffled);
assert_eq!(f_expected, f_shuffled);
}
#[test]
fn t_cmp_date() {
let set = vec![
Utc2k::new(2024, 1, 1, 0, 0, 0),
Utc2k::new(2024, 1, 2, 0, 0, 0),
Utc2k::new(2024, 2, 1, 0, 0, 0),
Utc2k::new(2024, 2, 2, 0, 0, 0),
Utc2k::new(2025, 1, 1, 0, 0, 0),
Utc2k::new(2025, 1, 2, 0, 0, 0),
Utc2k::new(2025, 2, 1, 0, 0, 0),
Utc2k::new(2025, 2, 2, 0, 0, 0),
];
let mut sorted = set.clone();
sorted.sort();
sorted.dedup();
assert_eq!(set, sorted);
for pair in set.windows(2) {
let &[a, b] = pair else { panic!("Windows is broken?!"); };
assert!(Utc2k::cmp_date(a, a).is_eq());
assert!(Utc2k::cmp_date(b, b).is_eq());
assert!(Utc2k::cmp_date(a, a.with_time(1, 2, 3)).is_eq());
assert!(Utc2k::cmp_date(b, b.with_time(3, 2, 1)).is_eq());
assert!(Utc2k::cmp_date(a.with_time(1, 2, 3), a).is_eq());
assert!(Utc2k::cmp_date(b.with_time(3, 2, 1), b).is_eq());
assert!(Utc2k::cmp_date(a, b).is_lt());
assert!(Utc2k::cmp_date(b, a).is_gt());
assert!(Utc2k::cmp_date(a, b.with_time(5, 6, 7)).is_lt());
assert!(Utc2k::cmp_date(b, a.with_time(8, 9, 3)).is_gt());
assert!(Utc2k::cmp_date(a.with_time(5, 6, 7), b).is_lt());
assert!(Utc2k::cmp_date(b.with_time(8, 9, 3), a).is_gt());
}
}
#[test]
fn t_cmp_time() {
let set = vec![
Utc2k::new(2027, 6, 5, 0, 0, 0),
Utc2k::new(2027, 6, 5, 0, 0, 1),
Utc2k::new(2027, 6, 5, 0, 1, 0),
Utc2k::new(2027, 6, 5, 0, 1, 1),
Utc2k::new(2027, 6, 5, 1, 0, 0),
Utc2k::new(2027, 6, 5, 1, 0, 1),
Utc2k::new(2027, 6, 5, 1, 1, 0),
Utc2k::new(2027, 6, 5, 1, 1, 1),
];
let mut sorted = set.clone();
sorted.sort();
sorted.dedup();
assert_eq!(set, sorted);
for pair in set.windows(2) {
let &[a, b] = pair else { panic!("Windows is broken?!"); };
assert!(Utc2k::cmp_time(a, a).is_eq());
assert!(Utc2k::cmp_time(b, b).is_eq());
let c = a + crate::YEAR_IN_SECONDS;
assert!(Utc2k::cmp_time(a, c).is_eq());
assert!(Utc2k::cmp_time(c, a).is_eq());
let d = b + crate::YEAR_IN_SECONDS;
assert!(Utc2k::cmp_time(b, d).is_eq());
assert!(Utc2k::cmp_time(d, b).is_eq());
assert!(Utc2k::cmp_time(a, b).is_lt());
assert!(Utc2k::cmp_time(b, a).is_gt());
assert!(Utc2k::cmp_time(a, d).is_lt());
assert!(Utc2k::cmp_time(b, c).is_gt());
assert!(Utc2k::cmp_time(c, b).is_lt());
assert!(Utc2k::cmp_time(d, a).is_gt());
}
}
#[cfg(not(debug_assertions))]
macro_rules! century_test {
(@count $odd:tt) => ( 1 );
(@count $odd:tt $($a:tt $b:tt)+) => ( (century_test!(@count $($a)+) * 2) + 1 );
(@count $($a:tt $b:tt)+) => ( century_test!(@count $($a)+) * 2 );
(@build $step:expr; $($fn:ident $offset:literal),+) => ($(
#[test]
#[ignore = "testing every second takes a long time"]
fn $fn() {
let format = time::format_description::parse(
"[year]-[month]-[day] [hour]:[minute]:[second]",
).expect("Unable to parse datetime format.");
let mut buf = Vec::new();
for i in (Utc2k::MIN_UNIXTIME + $offset..=Utc2k::MAX_UNIXTIME).step_by($step) {
range_test!(i, buf, format);
}
}
)+);
($($fn:ident $offset:literal),+ $(,)?) => (
century_test! { @build century_test!(@count $($offset)+); $($fn $offset),+ }
);
}
#[cfg(not(debug_assertions))]
century_test! {
full_unixtime_0 0,
full_unixtime_1 1,
full_unixtime_2 2,
full_unixtime_3 3,
full_unixtime_4 4,
full_unixtime_5 5,
full_unixtime_6 6,
full_unixtime_7 7,
full_unixtime_8 8,
full_unixtime_9 9,
}
}