use super::Timestamp;
use crate::c_bindings;
use crate::constant::{
DAY_NAMES, DAYS_IN_MONTH_COMMON_YEAR, DAYS_IN_MONTH_LEAP_YEAR, MONTH_NAMES, MONTH_TO_DAYS_COMMON_YEAR, MONTH_TO_DAYS_LEAP_YEAR, TIMEZONE_UTC, U8_DAYS_IN_WEEK, U8_HOURS_IN_DAY, U8_MINUTES_IN_HOUR,
U8_MONTHS_IN_YEAR, U8_SECONDS_IN_MINUTE, U16_DAYS_IN_COMMON_YEAR, U16_DAYS_IN_LEAP_YEAR, U16_MILLIS_IN_SECOND, U16_SECONDS_IN_HOUR, U16_SECONDS_IN_MINUTE, U16_UNIX_EPOCH_YEAR, U32_NANOS_IN_MILLI,
U64_LEAP_YEARS_BEFORE_EPOCH, U64_SECONDS_IN_COMMON_YEAR, U64_SECONDS_IN_DAY, U64_SECONDS_IN_HOUR, U64_SECONDS_IN_MINUTE,
};
#[derive(Clone, Debug, PartialEq)]
pub struct TimestampParts<'l> {
pub nanoseconds: u32,
pub milliseconds: u16,
pub seconds: u8,
pub minutes: u8,
pub hour: u8,
pub month_day: u8,
pub month: u8,
pub year: u16,
pub week_day: u8,
pub year_day: u16,
pub gmt_offset_negative: bool,
pub gmt_offset_hours: u8,
pub gmt_offset_minutes: u8,
pub timezone: &'l str,
}
impl<'i> TimestampParts<'i> {
pub fn epoch() -> Self {
TimestampParts {
nanoseconds: 0,
milliseconds: 0,
seconds: 0,
minutes: 0,
hour: 0,
month_day: 1,
month: 1,
year: U16_UNIX_EPOCH_YEAR,
week_day: 4,
year_day: 1,
gmt_offset_negative: false,
gmt_offset_hours: 0,
gmt_offset_minutes: 0,
timezone: TIMEZONE_UTC,
}
}
pub fn utc(ts: &Timestamp) -> Self {
let (seconds, nanos) = ts.epoch_offset();
let ts = seconds as c_bindings::CTime;
let tm = match c_bindings::c_time_to_utc_tm(ts) {
Some(tm) => tm,
None => panic!("failed to parse UTC parts for timestamp={seconds}s"),
};
TimestampParts {
nanoseconds: (nanos % U32_NANOS_IN_MILLI) as _,
milliseconds: (nanos / U32_NANOS_IN_MILLI) as _,
seconds: tm.tm_sec as _,
minutes: tm.tm_min as _,
hour: tm.tm_hour as _,
month_day: tm.tm_mday as _,
month: (1 + tm.tm_mon) as _,
year: (1900 + tm.tm_year) as _,
week_day: (1 + tm.tm_wday) as _,
year_day: (1 + tm.tm_yday) as _,
gmt_offset_negative: false,
gmt_offset_hours: 0 as _,
gmt_offset_minutes: 0 as _,
timezone: TIMEZONE_UTC,
}
}
pub fn local(ts: &Timestamp) -> Self {
let (seconds, nanos) = ts.epoch_offset();
let ts = seconds as c_bindings::CTime;
let tm = match c_bindings::c_time_to_local_tm(ts) {
Some(tm) => tm,
None => panic!("failed to parse local parts for timestamp={seconds}s"),
};
let gmt_offset_secs: i16;
let timezone: &str;
#[cfg(not(target_env = "msvc"))]
{
gmt_offset_secs = tm.tm_gmtoff as _;
timezone = c_bindings::c_timezone_from_tm(&tm);
}
#[cfg(target_env = "msvc")]
{
(timezone, gmt_offset_secs) = c_bindings::c_tz_info();
}
let (gmt_offset_negative, gmt_offset_hours, gmt_offset_minutes) = Self::_gmt_offset_parts(gmt_offset_secs);
TimestampParts {
nanoseconds: (nanos % U32_NANOS_IN_MILLI) as _,
milliseconds: (nanos / U32_NANOS_IN_MILLI) as _,
seconds: tm.tm_sec as _,
minutes: tm.tm_min as _,
hour: tm.tm_hour as _,
month_day: tm.tm_mday as _,
month: (1 + tm.tm_mon) as _,
year: (1900 + tm.tm_year) as _,
week_day: (1 + tm.tm_wday) as _,
year_day: (1 + tm.tm_yday) as _,
gmt_offset_negative: gmt_offset_negative,
gmt_offset_hours: gmt_offset_hours,
gmt_offset_minutes: gmt_offset_minutes,
timezone: timezone,
}
}
fn _gmt_offset_parts(gmt_offset_secs: i16) -> (bool, u8, u8) {
let secs: u16;
let negative: bool;
if gmt_offset_secs >= 0 {
negative = false;
secs = gmt_offset_secs as u16;
} else {
negative = true;
secs = -gmt_offset_secs as u16;
}
let hours = (secs / U16_SECONDS_IN_HOUR) as u8;
let mins = ((secs % U16_SECONDS_IN_HOUR) / U16_SECONDS_IN_MINUTE) as u8;
(negative, hours, mins)
}
fn year_day(&self) -> u16 {
let month_to_days = (if self.is_leap_year() { MONTH_TO_DAYS_LEAP_YEAR } else { MONTH_TO_DAYS_COMMON_YEAR })[(self.month - 1) as usize];
month_to_days + (self.month_day as u16)
}
pub fn gmt_offset_sign(&self) -> &'i str {
if self.gmt_offset_negative { "-" } else { "+" }
}
pub fn day_name(&self) -> &str {
if self.week_day == 0 {
panic!("invalid week day for {self:?}");
}
DAY_NAMES[((self.week_day - 1) % U8_DAYS_IN_WEEK) as usize]
}
pub fn month_name(&self) -> &str {
if self.week_day == 0 {
panic!("invalid month for {self:?}");
}
MONTH_NAMES[((self.month - 1) % U8_MONTHS_IN_YEAR) as usize]
}
pub fn is_leap_year(&self) -> bool {
(self.year % 4 == 0 && self.year % 100 != 0) || (self.year % 400 == 0)
}
pub fn leap_years_since_epoch(&self) -> u64 {
if self.year < U16_UNIX_EPOCH_YEAR {
return 0;
}
let year = self.year as u64 - 1;
let leap_years = (year / 4) - (year / 100) + (year / 400);
leap_years - U64_LEAP_YEARS_BEFORE_EPOCH
}
fn validate_fields(&self) -> Result<(), &'i str> {
if self.nanoseconds >= U32_NANOS_IN_MILLI {
return Err("invalid nanoseconds field");
}
if self.milliseconds >= U16_MILLIS_IN_SECOND {
return Err("invalid milliseconds field");
}
if self.seconds >= U8_SECONDS_IN_MINUTE {
return Err("invalid seconds field");
}
if self.minutes >= U8_MINUTES_IN_HOUR {
return Err("invalid minutes field");
}
if self.hour >= U8_HOURS_IN_DAY {
return Err("invalid hour field");
}
if self.month < 1 || self.month >= U8_MONTHS_IN_YEAR {
return Err("invalid month field");
}
if self.year < U16_UNIX_EPOCH_YEAR {
return Err("invalid year field");
}
if self.month_day < 1 || self.month_day > (if self.is_leap_year() { DAYS_IN_MONTH_LEAP_YEAR } else { DAYS_IN_MONTH_COMMON_YEAR })[(self.month - 1) as usize] {
return Err("invalid month_day field");
};
if self.week_day < 1 || self.week_day > U8_DAYS_IN_WEEK {
return Err("invalid week_day field");
}
if self.year_day < 1 || self.year_day > (if self.is_leap_year() { U16_DAYS_IN_LEAP_YEAR } else { U16_DAYS_IN_COMMON_YEAR }) {
return Err("invalid year_day field");
}
if self.gmt_offset_hours >= U8_HOURS_IN_DAY {
return Err("invalid gmt_offset_hours field");
}
if self.gmt_offset_minutes > U8_MINUTES_IN_HOUR {
return Err("invalid gmt_offset_minutes field");
}
Ok(())
}
pub fn validate(&self) -> Result<(), &'i str> {
if let Err(e) = self.validate_fields() {
return Err(e);
}
let expected_year_day = self.year_day();
if self.year_day != expected_year_day {
return Err("year_day field doesn't match year + month + month_day");
}
Ok(())
}
pub fn to_timestamp(&self) -> Result<Timestamp, &'i str> {
if let Err(e) = self.validate_fields() {
return Err(e);
}
let tm_sec = self.seconds as u64;
let tm_min = self.minutes as u64;
let tm_hour = self.hour as u64;
let tm_year = (self.year - U16_UNIX_EPOCH_YEAR) as u64;
let tm_yday = (self.year_day() - 1) as u64;
let leap_years = self.leap_years_since_epoch() as u64;
let mut secs = tm_sec + tm_min * U64_SECONDS_IN_MINUTE + tm_hour * U64_SECONDS_IN_HOUR + tm_yday * U64_SECONDS_IN_DAY;
secs += tm_year * U64_SECONDS_IN_COMMON_YEAR + leap_years * U64_SECONDS_IN_DAY;
let gmt_offset_secs = (self.gmt_offset_minutes as u64) * U64_SECONDS_IN_MINUTE + (self.gmt_offset_hours as u64) * U64_SECONDS_IN_HOUR;
if self.gmt_offset_negative {
secs += gmt_offset_secs
} else {
secs -= gmt_offset_secs
};
let nanos = self.nanoseconds + ((self.milliseconds as u32) * U32_NANOS_IN_MILLI);
Ok(super::Timestamp::new(secs, nanos))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn gmt_offset_parts() {
assert_eq!(TimestampParts::_gmt_offset_parts(30600), (false, 8, 30));
assert_eq!(TimestampParts::_gmt_offset_parts(-13500), (true, 3, 45));
}
#[test]
fn is_leap_year() {
let mut parts = TimestampParts {
nanoseconds: 0,
milliseconds: 0,
seconds: 0,
minutes: 0,
hour: 0,
month_day: 0,
month: 0,
year: 0,
week_day: 0,
year_day: 0,
gmt_offset_negative: false,
gmt_offset_hours: 0,
gmt_offset_minutes: 0,
timezone: "none",
};
parts.year = 1900;
assert!(!parts.is_leap_year());
parts.year = 2000;
assert!(parts.is_leap_year());
parts.year = 2001;
assert!(!parts.is_leap_year());
parts.year = 2020;
assert!(parts.is_leap_year());
parts.year = 2026;
assert!(!parts.is_leap_year());
}
#[test]
fn leap_years_since_epoch() {
let mut parts = TimestampParts::epoch();
assert_eq!(parts.leap_years_since_epoch(), 0);
parts.year = 1971;
assert_eq!(parts.leap_years_since_epoch(), 0);
parts.year = 1972;
assert_eq!(parts.leap_years_since_epoch(), 0);
parts.year = 1973;
assert_eq!(parts.leap_years_since_epoch(), 1);
parts.year = 2026;
assert_eq!(parts.leap_years_since_epoch(), 14);
parts.year = 2028;
assert_eq!(parts.leap_years_since_epoch(), 14);
parts.year = 2029;
assert_eq!(parts.leap_years_since_epoch(), 15);
}
#[test]
fn validation() {
let mut parts = TimestampParts::epoch();
parts.nanoseconds = 1000037;
assert_eq!(parts.validate(), Err("invalid nanoseconds field"));
parts.nanoseconds = 10037;
parts.milliseconds = 2196;
assert_eq!(parts.validate(), Err("invalid milliseconds field"));
parts.milliseconds = 761;
parts.seconds = 77;
assert_eq!(parts.validate(), Err("invalid seconds field"));
parts.seconds = 19;
parts.minutes = 77;
assert_eq!(parts.validate(), Err("invalid minutes field"));
parts.minutes = 58;
parts.hour = 24;
assert_eq!(parts.validate(), Err("invalid hour field"));
parts.hour = 22;
parts.month = 0;
assert_eq!(parts.validate(), Err("invalid month field"));
parts.month = 15;
assert_eq!(parts.validate(), Err("invalid month field"));
parts.month = 2;
parts.year = 1969;
assert_eq!(parts.validate(), Err("invalid year field"));
parts.year = 2026;
parts.month_day = 0;
assert_eq!(parts.validate(), Err("invalid month_day field"));
parts.month_day = 44;
assert_eq!(parts.validate(), Err("invalid month_day field"));
parts.month_day = 29;
assert_eq!(parts.validate(), Err("invalid month_day field"));
parts.month_day = 6;
parts.week_day = 0;
assert_eq!(parts.validate(), Err("invalid week_day field"));
parts.week_day = 9;
assert_eq!(parts.validate(), Err("invalid week_day field"));
parts.week_day = 6;
parts.year_day = 0;
assert_eq!(parts.validate(), Err("invalid year_day field"));
parts.year_day = 390;
assert_eq!(parts.validate(), Err("invalid year_day field"));
parts.year_day = 12;
assert_eq!(parts.validate(), Err("year_day field doesn't match year + month + month_day"));
parts.year_day = 37;
parts.gmt_offset_hours = 25;
assert_eq!(parts.validate(), Err("invalid gmt_offset_hours field"));
parts.gmt_offset_hours = 3;
parts.gmt_offset_minutes = 79;
assert_eq!(parts.validate(), Err("invalid gmt_offset_minutes field"));
parts.gmt_offset_minutes = 45;
assert_eq!(parts.validate(), Ok(()));
}
#[test]
fn parts_to_timestamp() {
let mut parts = TimestampParts {
nanoseconds: 123456,
milliseconds: 320,
seconds: 15,
minutes: 22,
hour: 5,
month_day: 8,
month: 3,
year: 2026,
week_day: 1,
year_day: 67,
gmt_offset_negative: false,
gmt_offset_hours: 0,
gmt_offset_minutes: 0,
timezone: TIMEZONE_UTC,
};
assert_eq!(parts.to_timestamp(), Ok(Timestamp::from_nanos(1772947335320123456)));
parts.gmt_offset_negative = true;
parts.gmt_offset_hours = 9;
parts.gmt_offset_minutes = 30;
parts.timezone = "Pacific/Marquesas";
assert_eq!(parts.to_timestamp(), Ok(Timestamp::from_nanos(1772981535320123456)));
}
#[test]
fn utc_to_and_from_parts() {
let ts = Timestamp::from_utc_date(2026, 03, 24, 18, 47, 31, 111, 222).expect("invalid parts");
let parts = ts.as_utc_parts();
assert_eq!(
parts,
TimestampParts {
nanoseconds: 222,
milliseconds: 111,
seconds: 31,
minutes: 47,
hour: 18,
month_day: 24,
month: 3,
year: 2026,
week_day: 3,
year_day: 83,
gmt_offset_negative: false,
gmt_offset_hours: 0,
gmt_offset_minutes: 0,
timezone: "UTC",
}
);
let from_parts: Timestamp = parts.to_timestamp().expect("invalid TimestampParts casted from Timestamp");
assert_eq!(ts, from_parts);
}
}