use std::fmt;
use crate::GNU_MAX_YEAR;
const SECONDS_PER_DAY: i64 = 86_400;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtendedDateTime {
pub year: u32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub nanosecond: u32,
pub offset_seconds: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DateParts {
pub year: u32,
pub month: u8,
pub day: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimeParts {
pub hour: u8,
pub minute: u8,
pub second: u8,
pub nanosecond: u32,
}
impl ExtendedDateTime {
pub fn new(
date: DateParts,
time: TimeParts,
offset_seconds: i32,
) -> Result<Self, &'static str> {
let DateParts { year, month, day } = date;
let TimeParts {
hour,
minute,
second,
nanosecond,
} = time;
if year > GNU_MAX_YEAR {
return Err("year must be no greater than 2147485547");
}
if !(1..=12).contains(&month) {
return Err("month must be between 1 and 12");
}
let dim = days_in_month(year, month);
if day == 0 || day > dim {
return Err("day is not valid for the given month");
}
if hour > 23 {
return Err("hour must be between 0 and 23");
}
if minute > 59 {
return Err("minute must be between 0 and 59");
}
if second > 59 {
return Err("second must be between 0 and 59");
}
if nanosecond >= 1_000_000_000 {
return Err("nanosecond must be between 0 and 999999999");
}
if offset_seconds.unsigned_abs() > 24 * 3600 {
return Err("offset must be between -24:00 and +24:00");
}
Ok(Self {
year,
month,
day,
hour,
minute,
second,
nanosecond,
offset_seconds,
})
}
pub fn from_unix_seconds(
unix_seconds: i64,
nanosecond: u32,
offset_seconds: i32,
) -> Result<Self, &'static str> {
if nanosecond >= 1_000_000_000 {
return Err("nanosecond must be between 0 and 999999999");
}
if offset_seconds.unsigned_abs() > 24 * 3600 {
return Err("offset must be between -24:00 and +24:00");
}
let local = unix_seconds
.checked_add(offset_seconds as i64)
.ok_or("timestamp overflow")?;
let days = local.div_euclid(SECONDS_PER_DAY);
let sod = local.rem_euclid(SECONDS_PER_DAY);
let (year, month, day) = civil_from_days(days);
let year: u32 = year.try_into().map_err(|_| "year must be non-negative")?;
let month: u8 = month.try_into().map_err(|_| "month is invalid")?;
let day: u8 = day.try_into().map_err(|_| "day is invalid")?;
let hour = (sod / 3600) as u8;
let minute = ((sod % 3600) / 60) as u8;
let second = (sod % 60) as u8;
Self::new(
DateParts { year, month, day },
TimeParts {
hour,
minute,
second,
nanosecond,
},
offset_seconds,
)
}
pub fn with_date(self, year: u32, month: u8, day: u8) -> Result<Self, &'static str> {
Self::new(
DateParts { year, month, day },
TimeParts {
hour: self.hour,
minute: self.minute,
second: self.second,
nanosecond: self.nanosecond,
},
self.offset_seconds,
)
}
pub fn with_time(
self,
hour: u8,
minute: u8,
second: u8,
nanosecond: u32,
) -> Result<Self, &'static str> {
Self::new(
DateParts {
year: self.year,
month: self.month,
day: self.day,
},
TimeParts {
hour,
minute,
second,
nanosecond,
},
self.offset_seconds,
)
}
pub fn with_offset(self, offset_seconds: i32) -> Result<Self, &'static str> {
Self::new(
DateParts {
year: self.year,
month: self.month,
day: self.day,
},
TimeParts {
hour: self.hour,
minute: self.minute,
second: self.second,
nanosecond: self.nanosecond,
},
offset_seconds,
)
}
pub fn checked_add_days(self, days: i64) -> Result<Self, &'static str> {
let unix = self.unix_seconds();
let delta = days
.checked_mul(SECONDS_PER_DAY)
.ok_or("seconds overflow")?;
let unix = unix.checked_add(delta).ok_or("timestamp overflow")?;
Self::from_unix_seconds(unix, self.nanosecond, self.offset_seconds)
}
pub fn checked_add_hours(self, hours: i64) -> Result<Self, &'static str> {
self.checked_add_seconds(hours.checked_mul(3600).ok_or("seconds overflow")?, 0)
}
pub fn checked_add_minutes(self, minutes: i64) -> Result<Self, &'static str> {
self.checked_add_seconds(minutes.checked_mul(60).ok_or("seconds overflow")?, 0)
}
pub fn checked_add_seconds(
self,
delta_seconds: i64,
delta_nanoseconds: u32,
) -> Result<Self, &'static str> {
let mut unix = self.unix_seconds();
unix = unix
.checked_add(delta_seconds)
.ok_or("timestamp overflow")?;
let mut ns = self
.nanosecond
.checked_add(delta_nanoseconds)
.ok_or("nanosecond overflow")?;
if ns >= 1_000_000_000 {
unix = unix.checked_add(1).ok_or("timestamp overflow")?;
ns -= 1_000_000_000;
}
Self::from_unix_seconds(unix, ns, self.offset_seconds)
}
pub fn checked_add_years(self, years: i32) -> Result<Self, &'static str> {
let year = (self.year as i64)
.checked_add(years as i64)
.ok_or("year overflow")?;
let year: u32 = year.try_into().map_err(|_| "year must be non-negative")?;
if year > GNU_MAX_YEAR {
return Err("year must be no greater than 2147485547");
}
let month = self.month;
let max_day = days_in_month(year, month);
let day = self.day.min(max_day);
let overflow_days = self.day.saturating_sub(day);
let next = self.with_date(year, month, day)?;
if overflow_days == 0 {
Ok(next)
} else {
next.checked_add_days(i64::from(overflow_days))
}
}
pub fn checked_add_months(self, months: i32) -> Result<Self, &'static str> {
let base_month = i64::from(self.month) - 1;
let total = i64::from(self.year)
.checked_mul(12)
.and_then(|v| v.checked_add(base_month))
.and_then(|v| v.checked_add(i64::from(months)))
.ok_or("month overflow")?;
if total < 0 {
return Err("year must be non-negative");
}
let year = u32::try_from(total / 12).map_err(|_| "year overflow")?;
if year > GNU_MAX_YEAR {
return Err("year must be no greater than 2147485547");
}
let month = u8::try_from((total % 12) + 1).map_err(|_| "month overflow")?;
let max_day = days_in_month(year, month);
let day = self.day.min(max_day);
let overflow_days = self.day.saturating_sub(day);
let next = self.with_date(year, month, day)?;
if overflow_days == 0 {
Ok(next)
} else {
next.checked_add_days(i64::from(overflow_days))
}
}
pub fn day_of_year(&self) -> u16 {
let mut doy = 0u16;
let mut month = 1u8;
while month < self.month {
doy += days_in_month(self.year, month) as u16;
month += 1;
}
doy + self.day as u16
}
pub fn unix_seconds(&self) -> i64 {
let days = days_from_civil(self.year as i64, self.month as i64, self.day as i64);
let daytime = (self.hour as i64) * 3600 + (self.minute as i64) * 60 + (self.second as i64);
days * SECONDS_PER_DAY + daytime - self.offset_seconds as i64
}
pub fn weekday_sunday0(&self) -> u8 {
let days = days_from_civil(self.year as i64, self.month as i64, self.day as i64);
(((days + 4).rem_euclid(7) + 7).rem_euclid(7)) as u8
}
pub fn weekday_monday0(&self) -> u8 {
(self.weekday_sunday0() + 6) % 7
}
}
impl fmt::Display for ExtendedDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sign = if self.offset_seconds < 0 { '-' } else { '+' };
let abs = self.offset_seconds.unsigned_abs();
let oh = abs / 3600;
let om = (abs % 3600) / 60;
if self.nanosecond != 0 {
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}{}{:02}:{:02}",
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
self.nanosecond,
sign,
oh,
om,
)
} else {
write!(
f,
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}{:02}:{:02}",
self.year, self.month, self.day, self.hour, self.minute, self.second, sign, oh, om,
)
}
}
}
pub fn is_leap_year(year: u32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn days_in_month(year: u32, month: u8) -> u8 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let y = year - if month <= 2 { 1 } else { 0 };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = month + if month > 2 { -3 } else { 9 };
let doy = (153 * mp + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe - 719468
}
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let mut year = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = mp + if mp < 10 { 3 } else { -9 };
year += if month <= 2 { 1 } else { 0 };
(year, month, day)
}
#[cfg(test)]
mod tests {
use crate::GNU_MAX_YEAR;
use super::{is_leap_year, DateParts, ExtendedDateTime, TimeParts};
#[test]
fn leap_year_rules() {
assert!(is_leap_year(2000));
assert!(!is_leap_year(2100));
assert!(is_leap_year(10000));
}
#[test]
fn unix_seconds_large_year() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert_eq!(dt.unix_seconds(), 253402300800);
}
#[test]
fn unix_roundtrip() {
let dt = ExtendedDateTime::new(
DateParts {
year: 2147485547,
month: 12,
day: 31,
},
TimeParts {
hour: 23,
minute: 59,
second: 59,
nanosecond: 123_456_789,
},
5 * 3600,
)
.unwrap();
let unix = dt.unix_seconds();
let rt =
ExtendedDateTime::from_unix_seconds(unix, dt.nanosecond, dt.offset_seconds).unwrap();
assert_eq!(dt, rt);
}
#[test]
fn add_years_overflows_like_gnu() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 2,
day: 29,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
let actual = dt.checked_add_years(1).unwrap();
assert_eq!((actual.year, actual.month, actual.day), (10001, 3, 1));
}
#[test]
fn add_months_overflows_like_gnu() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 31,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
let plus_2 = dt.clone().checked_add_months(2).unwrap();
assert_eq!((plus_2.year, plus_2.month, plus_2.day), (10000, 3, 31));
let plus_3 = dt.checked_add_months(3).unwrap();
assert_eq!((plus_3.year, plus_3.month, plus_3.day), (10000, 5, 1));
}
#[test]
fn constructor_validation_errors() {
assert!(ExtendedDateTime::new(
DateParts {
year: GNU_MAX_YEAR + 1,
month: 1,
day: 1
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 13,
day: 1
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 2,
day: 30
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1
},
TimeParts {
hour: 24,
minute: 0,
second: 0,
nanosecond: 0
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1
},
TimeParts {
hour: 0,
minute: 60,
second: 0,
nanosecond: 0
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1
},
TimeParts {
hour: 0,
minute: 0,
second: 60,
nanosecond: 0
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 1_000_000_000
},
0
)
.is_err());
assert!(ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0
},
86_401
)
.is_err());
}
#[test]
fn from_unix_seconds_validation() {
assert!(ExtendedDateTime::from_unix_seconds(0, 1_000_000_000, 0).is_err());
assert!(ExtendedDateTime::from_unix_seconds(0, 0, 86_401).is_err());
}
#[test]
fn checked_add_seconds_carries_nanoseconds() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 900_000_000,
},
0,
)
.unwrap();
let actual = dt.checked_add_seconds(0, 200_000_000).unwrap();
assert_eq!(
(
actual.year,
actual.month,
actual.day,
actual.hour,
actual.minute,
actual.second,
actual.nanosecond
),
(10000, 1, 1, 0, 0, 1, 100_000_000)
);
}
#[test]
fn checked_add_months_validation() {
let dt = ExtendedDateTime::new(
DateParts {
year: 0,
month: 1,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert!(dt.checked_add_months(-1).is_err());
let dt = ExtendedDateTime::new(
DateParts {
year: GNU_MAX_YEAR,
month: 12,
day: 31,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert!(dt.checked_add_months(1).is_err());
}
#[test]
fn checked_add_years_validation() {
let dt = ExtendedDateTime::new(
DateParts {
year: 0,
month: 1,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert!(dt.checked_add_years(-1).is_err());
let dt = ExtendedDateTime::new(
DateParts {
year: GNU_MAX_YEAR,
month: 1,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert!(dt.checked_add_years(1).is_err());
}
#[test]
fn checked_add_seconds_validation() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 1,
day: 1,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert!(dt.checked_add_seconds(i64::MAX, 0).is_err());
}
#[test]
fn day_of_year_and_weekday_helpers() {
let dt = ExtendedDateTime::new(
DateParts {
year: 10000,
month: 12,
day: 31,
},
TimeParts {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
},
0,
)
.unwrap();
assert_eq!(dt.day_of_year(), 366);
assert_eq!(dt.weekday_monday0(), (dt.weekday_sunday0() + 6) % 7);
}
#[test]
fn days_in_month_returns_zero_for_invalid_month() {
assert_eq!(super::days_in_month(10000, 0), 0);
assert_eq!(super::days_in_month(10000, 13), 0);
}
}