mod utilities;
use utilities::Template;
pub struct Instant {
utc_secs: u64,
utc_offset: i32,
}
impl Instant {
pub fn now_utc() -> Self {
use std::time::SystemTime;
let time = SystemTime::now();
let since = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
let utc_secs = since.as_secs();
Self::from_utc_secs(utc_secs)
}
pub fn now_local() -> Self {
use std::time::SystemTime;
let time = SystemTime::now();
let since = time.duration_since(SystemTime::UNIX_EPOCH).unwrap();
let utc_secs = since.as_secs();
let utc_offset = system_utc_offset_seconds();
Self {
utc_secs,
utc_offset,
}
}
pub fn from_utc_secs(utc_secs: u64) -> Self {
Self {
utc_secs,
utc_offset: 0,
}
}
pub fn from_system_time(time: std::time::SystemTime) -> Self {
let secs = time
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
Self::from_utc_secs(secs)
}
pub fn format(&self, template: &str) -> String {
let template = Template::new(template, '%');
let secs = self.seconds();
let day_of_week_str = self.day_of_week_str();
let (day_of_year, offset_year) = self.day_of_year_and_offset_year();
let year = BASE_YEAR + offset_year;
let date_prefixes = if is_leap_year(year) {
LEAP_YEAR_MONTHS_PREFIX_SUM
} else {
NON_LEAP_YEAR_MONTHS_PREFIX_SUM
};
let result = date_prefixes
.iter()
.enumerate()
.rev()
.find(|(_, (_, _, acc))| *acc <= day_of_year);
let Some((month, (month_name, _, day_sum))) = result else {
panic!("bad day of year {day_of_year}");
};
let date = day_of_year - day_sum + 1; let month = month + 1;
let week = self.week_of_year();
template.interpolate(|item| {
use std::borrow::Cow;
fn hour_zero_to_twelve(i: u64) -> u64 {
if i == 0 { 12 } else { i }
}
match item {
"second" => Cow::Owned(format!("{second:02}", second = secs % 60)),
"minute" => Cow::Owned(format!("{minute:02}", minute = (secs / MINUTE) % 60)),
"hour" | "hour24" => Cow::Owned(format!("{hour:02}", hour = (secs / HOUR) % 24)),
"hour12" => Cow::Owned(format!(
"{hour:02}",
hour = hour_zero_to_twelve((secs / HOUR) % 12)
)),
"week" => Cow::Owned(week.to_string()),
"week_day" => Cow::Borrowed(day_of_week_str),
"week_day_short" => Cow::Borrowed(&day_of_week_str[..3]),
"date_suffix" => Cow::Borrowed(number_index_suffix(date as usize)),
"date" => Cow::Owned(format!("{date:02}")),
"month_name" => Cow::Borrowed(month_name),
"month_name_short" => Cow::Borrowed(&month_name[..3]),
"month" => Cow::Owned(format!("{month:02}")),
"full_year" => Cow::Owned(format!("{year}", year = year % 100)),
"year" => Cow::Owned(format!("{year}")),
"offset" => {
fn offset(prefix: char, utc_offset: u64) -> String {
let hour = utc_offset / HOUR;
let minute = (utc_offset / MINUTE) % 60;
format!("{prefix}{hour:02}{minute:02}")
}
if self.utc_offset < 0 {
Cow::Owned(offset('-', -self.utc_offset as u64))
} else {
Cow::Owned(offset('+', self.utc_offset as u64))
}
}
name => {
panic!("unknown interpolation {name}");
}
}
})
}
pub fn seconds(&self) -> u64 {
if self.utc_offset > 0 {
self.utc_secs + self.utc_offset as u64
} else {
self.utc_secs - self.utc_offset as u64
}
}
pub fn new(year: u64, month: u64, day: u64, hour: u64, minute: u64, second: u64) -> Self {
let date_prefixes = if is_leap_year(year) {
LEAP_YEAR_MONTHS_PREFIX_SUM
} else {
NON_LEAP_YEAR_MONTHS_PREFIX_SUM
};
let year = year - BASE_YEAR;
let leap_years = (BASE_YEAR..)
.take(year as usize)
.filter(|year| is_leap_year(*year))
.count() as u64;
let year = year * NON_LEAP_YEAR + leap_years * DAY;
let days = (day - 1 + date_prefixes[month as usize - 1].2) * DAY;
Self::from_utc_secs(year + days + hour * HOUR + minute * MINUTE + second)
}
pub fn parse_english<S: AsRef<str>>(
mut words: impl Iterator<Item = S>,
) -> Result<Self, &'static str> {
let Some(mut date) = words.next() else {
return Err("no date");
};
if let Some((day, month_and_year)) = date.as_ref().split_once('/') {
let (month, year) = month_and_year.split_once('/').unwrap();
let day = str::parse::<u64>(day).unwrap();
let month = str::parse::<u64>(month).unwrap();
let year = str::parse::<u64>(year).unwrap();
let year = if year < 100 { year + 2000 } else { year };
let next = words.next();
let (hour, minute, second) = if let Some(next) = next
&& let Some(triple) = parse_time(next.as_ref())
{
triple
} else {
(12, 0, 0)
};
Ok(Instant::new(year, month, day, hour, minute, second))
} else if let Some((hour, minute, second)) = parse_time(date.as_ref()) {
Ok(Instant::now_local().set_time(hour, minute, second))
} else {
if date.as_ref().eq_ignore_ascii_case("the") {
if let Some(next) = words.next() {
date = next;
} else {
return Err("nothing after the");
}
}
let Some(mut month) = words.next() else {
return Err("no month");
};
if month.as_ref().eq_ignore_ascii_case("of") {
if let Some(next) = words.next() {
month = next;
} else {
return Err("nothing after of");
}
}
let year = words.next();
let year = if let Some(year) = year {
let Ok(year) = year.as_ref().parse() else {
return Err("invalid year");
};
if year < 100 { year + 2000 } else { year }
} else {
Instant::now_local().year()
};
let month = month.as_ref();
let months = if is_leap_year(year) {
LEAP_YEAR_MONTHS_PREFIX_SUM
} else {
NON_LEAP_YEAR_MONTHS_PREFIX_SUM
};
let month = if month.len() == 3 {
months
.iter()
.position(|(name, ..)| name[..3].eq_ignore_ascii_case(month))
} else {
months
.iter()
.position(|(name, ..)| name.eq_ignore_ascii_case(month))
};
let month = if let Some(month) = month {
month + 1
} else {
return Err("cannot find month");
};
let date = {
let date = date.as_ref();
let suffixed = date.ends_with("st")
|| date.ends_with("nd")
|| date.ends_with("rd")
|| date.ends_with("th");
if suffixed {
&date[..(date.len() - 2)]
} else {
date
}
};
let Ok(day) = date.parse() else {
return Err("invalid date");
};
let (hour, minute, seconds) = if let Some(other) = words.next() {
if let Some(triple) = parse_time(other.as_ref()) {
triple
} else {
return Err("invalid time");
}
} else {
(12, 0, 0)
};
Ok(Self::new(year, month as u64, day, hour, minute, seconds))
}
}
pub fn difference(&self, other: Instant) -> Result<Duration, Duration> {
let difference = self.seconds().checked_sub(other.seconds());
match difference {
Some(secs) => Ok(Duration { secs }),
None => Err(Duration {
secs: other.seconds() - self.seconds(),
}),
}
}
#[must_use]
pub fn set_time(mut self, hours: u64, minutes: u64, seconds: u64) -> Self {
self.utc_secs -= self.utc_secs % DAY;
self.utc_secs -= self.utc_secs % HOUR;
self.utc_secs -= self.utc_secs % MINUTE;
self.utc_secs += hours * HOUR;
self.utc_secs += minutes * MINUTE;
self.utc_secs += seconds;
self
}
#[must_use]
pub fn year(&self) -> u64 {
let non_leap_years = self.seconds() / NON_LEAP_YEAR;
let accounting_for_leap_years =
(self.seconds() - DAY * leap_years_since(non_leap_years)) / NON_LEAP_YEAR;
BASE_YEAR + accounting_for_leap_years
}
#[must_use]
pub fn day_of_week(&self) -> u8 {
((self.seconds() / DAY + 3) % 7) as u8
}
#[must_use]
pub fn day_of_week_str(&self) -> &str {
DAYS[self.day_of_week() as usize]
}
#[must_use]
pub fn week_of_year(&self) -> u64 {
let (day_of_year, _) = self.day_of_year_and_offset_year();
let offset = Self::utc_start_of_year(self.year()).day_of_week() as u64;
((day_of_year + offset) / 7) + 1
}
fn day_of_year_and_offset_year(&self) -> (u64, u64) {
let secs = self.seconds();
let offset_year = secs / NON_LEAP_YEAR;
let day_of_year = (secs / DAY) % 365;
let leap_years = leap_years_since(offset_year);
if leap_years > day_of_year {
let year = offset_year - 1;
let day = (count_of_days_in_year(BASE_YEAR + year) + day_of_year) - leap_years;
(day, year)
} else {
(day_of_year - leap_years, offset_year)
}
}
pub fn month(&self) -> (&'static str, u64, u64) {
let (day_of_year, offset_year) = self.day_of_year_and_offset_year();
let year = BASE_YEAR + offset_year;
let date_prefixes = if is_leap_year(year) {
LEAP_YEAR_MONTHS_PREFIX_SUM
} else {
NON_LEAP_YEAR_MONTHS_PREFIX_SUM
};
let result = date_prefixes
.iter()
.enumerate()
.rev()
.find(|(_, (_, _, acc))| *acc <= day_of_year);
let Some((month, (month_name, _, day_sum))) = result else {
panic!("bad day of year {day_of_year}");
};
let date = day_of_year - day_sum + 1; (month_name, month as u64, date)
}
#[must_use]
pub fn utc_start_of_year(year: u64) -> Self {
let year = year - BASE_YEAR;
Self {
utc_secs: year * NON_LEAP_YEAR + leap_years_since(year) * DAY,
utc_offset: 0,
}
}
}
fn parse_time(on: &str) -> Option<(u64, u64, u64)> {
if let Some((hour, minute_and_seconds)) = on.split_once(':') {
let hour = str::parse::<u64>(hour).unwrap();
if let Some((minute, second)) = minute_and_seconds.split_once(':') {
let minute = str::parse::<u64>(minute).unwrap();
let second = str::parse::<u64>(second).unwrap();
Some((hour, minute, second))
} else {
let minute = minute_and_seconds;
let minute = str::parse::<u64>(minute).unwrap();
Some((hour, minute, 0))
}
} else {
None
}
}
pub struct Month {
pub name: &'static str,
pub days: u8,
}
pub struct Year(u32);
impl Year {
pub fn new(year: u32) -> Self {
Self(year)
}
pub fn months(&self) -> impl Iterator<Item = Month> {
let months = if is_leap_year(self.0 as u64) {
LEAP_YEAR_MONTHS_PREFIX_SUM
} else {
NON_LEAP_YEAR_MONTHS_PREFIX_SUM
};
months.iter().map(|(name, days, _)| Month {
name,
days: *days as u8,
})
}
}
#[derive(Clone, Copy)]
pub struct Duration {
pub(crate) secs: u64,
}
impl Duration {
#[must_use]
pub const fn from_secs(secs: u64) -> Self {
Self { secs }
}
#[must_use]
pub const fn from_seconds(secs: u64) -> Self {
Self { secs }
}
#[must_use]
pub const fn from_minutes(minutes: u64) -> Self {
Self {
secs: minutes * MINUTE,
}
}
#[must_use]
pub const fn from_hours(hours: u64) -> Self {
Self { secs: hours * HOUR }
}
#[must_use]
pub const fn from_days(days: u64) -> Self {
Self { secs: days * DAY }
}
#[must_use]
pub const fn from_weeks(weeks: u64) -> Self {
Self { secs: weeks * WEEK }
}
#[must_use]
pub fn format(&self) -> String {
fn format_(secs: u64, precision: usize, buf: &mut String) {
use std::fmt::Write;
if secs == 0 {
return;
}
if !buf.is_empty() {
if precision > 1 {
buf.push_str(", ");
} else if precision == 1 {
buf.push_str(" and ");
} else if precision == 0 {
return;
}
}
if secs < MINUTE {
write!(buf, "{secs} seconds").unwrap();
} else if secs < HOUR {
let mins = secs / MINUTE;
write!(buf, "{mins} minutes").unwrap();
format_(secs % MINUTE, precision - 1, buf);
} else if secs < DAY {
let hours = secs / HOUR;
write!(buf, "{hours} hours").unwrap();
format_(secs % HOUR, precision - 1, buf);
} else if secs < WEEK {
let days = secs / DAY;
write!(buf, "{days} days").unwrap();
format_(secs % DAY, precision - 1, buf);
} else if secs < NON_LEAP_YEAR {
let weeks = secs / WEEK;
write!(buf, "{weeks} weeks").unwrap();
format_(secs % WEEK, precision - 1, buf);
} else {
const MONTH: u64 = DAY * 30;
let months = secs / MONTH;
write!(buf, "{months} months").unwrap();
}
}
let precision = 2;
let mut buf = String::new();
format_(self.secs, precision, &mut buf);
buf
}
}
impl std::ops::Add<Duration> for Instant {
type Output = Self;
fn add(mut self, duration: Duration) -> Self::Output {
self.utc_secs += duration.secs;
self
}
}
impl std::ops::AddAssign<Duration> for Instant {
fn add_assign(&mut self, duration: Duration) {
self.utc_secs += duration.secs;
}
}
impl std::ops::Sub<Duration> for Instant {
type Output = Self;
fn sub(mut self, duration: Duration) -> Self::Output {
self.utc_secs -= duration.secs;
self
}
}
impl std::ops::SubAssign<Duration> for Instant {
fn sub_assign(&mut self, duration: Duration) {
self.utc_secs -= duration.secs;
}
}
pub const MINUTE: u64 = 60;
pub const HOUR: u64 = 60 * MINUTE;
pub const DAY: u64 = 24 * HOUR;
pub const WEEK: u64 = 7 * DAY;
const NON_LEAP_YEAR: u64 = 365 * DAY;
const BASE_YEAR: u64 = 1970;
const NON_LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64, u64)] = &[
("January", 31, 0),
("February", 28, 31),
("March", 31, 59),
("April", 30, 90),
("May", 31, 120),
("June", 30, 151),
("July", 31, 181),
("August", 31, 212),
("September", 30, 243),
("October", 31, 273),
("November", 30, 304),
("December", 31, 334),
];
const LEAP_YEAR_MONTHS_PREFIX_SUM: &[(&str, u64, u64)] = &[
("January", 31, 0),
("February", 29, 31),
("March", 31, 60),
("April", 30, 91),
("May", 31, 121),
("June", 30, 152),
("July", 31, 182),
("August", 31, 213),
("September", 30, 244),
("October", 31, 274),
("November", 30, 305),
("December", 31, 335),
];
const DAYS: &[&str] = &[
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
pub fn count_of_days_in_year(year: u64) -> u64 {
365 + is_leap_year(year) as u64
}
fn is_leap_year(year: u64) -> bool {
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400))
}
fn leap_years_since(offset_year: u64) -> u64 {
(BASE_YEAR..)
.take(offset_year as usize)
.filter(|year| is_leap_year(*year))
.count() as u64
}
fn number_index_suffix(item: usize) -> &'static str {
match item % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
}
}
#[allow(non_snake_case)]
pub mod FORMATS {
pub const DATE_MONTH_YEAR: &str = "%date/%month/%year";
pub const MONTH_DATE_YEAR: &str = "%month/%date/%year";
pub const DATE_NAME_MONTH_YEAR: &str = "%week_day %date%date_suffix %month_name %year";
pub const TIME_DATE_NAME_MONTH_YEAR: &str =
"%hour:%minute %week_day %date%date_suffix %month_name %year";
pub const ENGLISH: &str = "%week_day the %date%date_suffix of %month_name %year";
pub const TIME: &str = "%hour:%minute";
pub const TIME_WITH_SECONDS: &str = "%hour:%minute:%second";
pub const WEEK: &str = "%week";
pub const FULL_MINIMAL: &str = "%hour:%minute:%second %date/%month/%year";
pub const FULL: &str = "%hour:%minute:%second %date/%month/%year";
pub const ISO_8601: &str = "%year-%month-%dateT%hour:%minute:%second";
pub const RFC_2822: &str = "%year-%month-%dateT%hour:%minute:%second";
}
#[cfg(unix)]
fn system_utc_offset_seconds() -> i32 {
use std::mem::MaybeUninit;
unsafe {
let mut now: libc::time_t = 0;
libc::time(&raw mut now);
let mut tm = MaybeUninit::<libc::tm>::uninit();
libc::localtime_r(&raw const now, tm.as_mut_ptr());
let tm = tm.assume_init();
i32::try_from(tm.tm_gmtoff).unwrap()
}
}
#[cfg(windows)]
fn system_utc_offset_seconds() -> i32 {
use std::mem::MaybeUninit;
use winapi::um::timezoneapi::GetTimeZoneInformation;
use winapi::um::timezoneapi::TIME_ZONE_INFORMATION;
unsafe {
let mut tzi = MaybeUninit::<TIME_ZONE_INFORMATION>::uninit();
let state = GetTimeZoneInformation(tzi.as_mut_ptr());
let tzi = tzi.assume_init();
let mut bias = tzi.Bias;
match state {
winapi::um::timezoneapi::TIME_ZONE_ID_DAYLIGHT => {
bias += tzi.DaylightBias;
}
winapi::um::timezoneapi::TIME_ZONE_ID_STANDARD => {
bias += tzi.StandardBias;
}
_ => {}
}
-bias * 60
}
}