pub trait Clock: Send + Sync {
fn strftime(&self, format: &str) -> String;
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Civil {
pub year: i64,
pub month: u32, pub day: u32, pub hour: u32,
pub min: u32,
pub sec: u32,
pub weekday: u32, pub yday: u32, }
const MONTHS_FULL: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const MONTHS_ABBR: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
const DAYS_FULL: [&str; 7] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const DAYS_ABBR: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
pub(crate) fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; let year = if m <= 2 { y + 1 } else { y };
(year, m, d)
}
fn is_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn day_of_year(year: i64, month: u32, day: u32) -> u32 {
const CUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
let mut d = CUM[(month - 1) as usize] + day;
if month > 2 && is_leap(year) {
d += 1;
}
d
}
impl Civil {
pub(crate) fn from_unix_secs(secs: i64) -> Civil {
let days = secs.div_euclid(86_400);
let tod = secs.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
let weekday = ((days.rem_euclid(7) + 4) % 7) as u32;
Civil {
year,
month,
day,
hour: (tod / 3600) as u32,
min: ((tod % 3600) / 60) as u32,
sec: (tod % 60) as u32,
weekday,
yday: day_of_year(year, month, day),
}
}
pub(crate) fn strftime(&self, format: &str) -> String {
let mut out = String::with_capacity(format.len() + 8);
let mut chars = format.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
match chars.next() {
Some('Y') => out.push_str(&self.year.to_string()),
Some('y') => out.push_str(&format!("{:02}", self.year.rem_euclid(100))),
Some('m') => out.push_str(&format!("{:02}", self.month)),
Some('d') => out.push_str(&format!("{:02}", self.day)),
Some('e') => out.push_str(&format!("{:2}", self.day)),
Some('B') => out.push_str(MONTHS_FULL[(self.month - 1) as usize]),
Some('b') | Some('h') => out.push_str(MONTHS_ABBR[(self.month - 1) as usize]),
Some('A') => out.push_str(DAYS_FULL[self.weekday as usize]),
Some('a') => out.push_str(DAYS_ABBR[self.weekday as usize]),
Some('j') => out.push_str(&format!("{:03}", self.yday)),
Some('H') => out.push_str(&format!("{:02}", self.hour)),
Some('I') => {
let h12 = match self.hour % 12 {
0 => 12,
h => h,
};
out.push_str(&format!("{:02}", h12));
}
Some('M') => out.push_str(&format!("{:02}", self.min)),
Some('S') => out.push_str(&format!("{:02}", self.sec)),
Some('p') => out.push_str(if self.hour < 12 { "AM" } else { "PM" }),
Some('%') => out.push('%'),
Some(other) => {
out.push('%');
out.push(other);
}
None => out.push('%'),
}
}
out
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn strftime(&self, format: &str) -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
Civil::from_unix_secs(secs).strftime(format)
}
}
#[derive(Clone, Copy, Debug)]
pub struct FixedClock {
unix_secs: i64,
}
impl FixedClock {
pub fn from_unix_secs(unix_secs: i64) -> Self {
FixedClock { unix_secs }
}
pub fn from_ymd(year: i64, month: u32, day: u32) -> Option<Self> {
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = if month > 2 { month - 3 } else { month + 9 } as i64;
let doy = (153 * mp + 2) / 5 + day as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146_097 + doe - 719_468;
Some(FixedClock {
unix_secs: days * 86_400,
})
}
}
impl Clock for FixedClock {
fn strftime(&self, format: &str) -> String {
Civil::from_unix_secs(self.unix_secs).strftime(format)
}
}
#[cfg(feature = "strftime")]
#[derive(Clone, Copy, Debug, Default)]
pub struct LocalClock;
#[cfg(feature = "strftime")]
impl Clock for LocalClock {
fn strftime(&self, format: &str) -> String {
use chrono::{Datelike, Local, Timelike};
let now = Local::now();
let civil = Civil {
year: now.year() as i64,
month: now.month(),
day: now.day(),
hour: now.hour(),
min: now.minute(),
sec: now.second(),
weekday: now.weekday().num_days_from_sunday(),
yday: now.ordinal(), };
civil.strftime(format)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_clock_formats_a_known_date() {
let clk = FixedClock::from_ymd(2024, 7, 4).unwrap();
assert_eq!(clk.strftime("%B %d, %Y"), "July 04, 2024");
assert_eq!(clk.strftime("%Y-%m-%d"), "2024-07-04");
assert_eq!(clk.strftime("%A"), "Thursday");
}
#[cfg(feature = "strftime")]
#[test]
fn local_clock_reads_local_time() {
use chrono::{Datelike, Local};
let got = LocalClock.strftime("%Y-%m-%d");
let now = Local::now();
let same = format!("{:04}-{:02}-{:02}", now.year(), now.month(), now.day());
let prev = (now - chrono::Duration::days(1)).date_naive();
let prev = format!("{:04}-{:02}-{:02}", prev.year(), prev.month(), prev.day());
assert!(
got == same || got == prev,
"LocalClock date {got} matched neither {same} nor {prev}"
);
}
}