use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy)]
struct DateTime {
year: i32,
month: u8, day: u8, hour: u8,
min: u8,
sec: u8,
}
impl DateTime {
fn now_utc() -> Self {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
Self::from_unix(secs)
}
fn from_unix(secs: i64) -> Self {
let (days, rem) = (secs / 86400, secs % 86400);
let hour = (rem / 3600) as u8;
let min = ((rem % 3600) / 60) as u8;
let sec = (rem % 60) as u8;
let (year, month, day) = julian_to_ymd(days + 2440588); DateTime { year, month, day, hour, min, sec }
}
fn to_unix(&self) -> i64 {
let jd = ymd_to_julian(self.year, self.month, self.day);
let days = jd - 2440588;
days * 86400 + self.hour as i64 * 3600 + self.min as i64 * 60 + self.sec as i64
}
fn to_julian_day(&self) -> f64 {
let jd = ymd_to_julian(self.year, self.month, self.day) as f64;
let frac = (self.hour as f64 - 12.0) / 24.0
+ self.min as f64 / 1440.0
+ self.sec as f64 / 86400.0;
jd + frac
}
fn from_julian_day(jd: f64) -> Self {
let day_int = jd as i64;
let frac = jd - day_int as f64;
let secs_in_day = (frac * 86400.0).round() as i64;
let unix_secs = (day_int - 2440588) * 86400 + secs_in_day;
Self::from_unix(unix_secs)
}
fn date_str(&self) -> String {
format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
fn time_str(&self) -> String {
format!("{:02}:{:02}:{:02}", self.hour, self.min, self.sec)
}
fn datetime_str(&self) -> String {
format!("{} {}", self.date_str(), self.time_str())
}
}
enum Modifier {
AddDays(i64),
AddMonths(i32),
AddYears(i32),
StartOfMonth,
StartOfYear,
StartOfDay,
Weekday(u8), }
fn parse_modifier(s: &str) -> Option<Modifier> {
let s = s.trim().to_lowercase();
if s == "start of month" { return Some(Modifier::StartOfMonth); }
if s == "start of year" { return Some(Modifier::StartOfYear); }
if s == "start of day" { return Some(Modifier::StartOfDay); }
if let Some(rest) = s.strip_prefix("weekday ") {
if let Ok(n) = rest.trim().parse::<u8>() { return Some(Modifier::Weekday(n)); }
}
let (sign, rest) = if s.starts_with('-') { (-1i64, &s[1..]) }
else if s.starts_with('+') { (1i64, &s[1..]) }
else { (1i64, s.as_str()) };
let parts: Vec<&str> = rest.trim().splitn(2, ' ').collect();
if parts.len() == 2 {
if let Ok(n) = parts[0].parse::<i64>() {
let unit = parts[1].trim_end_matches('s'); return match unit {
"day" => Some(Modifier::AddDays(sign * n)),
"month" => Some(Modifier::AddMonths((sign * n) as i32)),
"year" => Some(Modifier::AddYears((sign * n) as i32)),
"hour" => Some(Modifier::AddDays(sign * n / 24)),
_ => None,
};
}
}
None
}
fn apply_modifier(mut dt: DateTime, m: &Modifier) -> DateTime {
match m {
Modifier::AddDays(d) => {
let unix = dt.to_unix() + d * 86400;
dt = DateTime::from_unix(unix);
}
Modifier::AddMonths(m) => {
let total = dt.month as i32 - 1 + m;
let years = total.div_euclid(12);
dt.month = (total.rem_euclid(12) + 1) as u8;
dt.year += years;
}
Modifier::AddYears(y) => { dt.year += y; }
Modifier::StartOfMonth => { dt.day = 1; dt.hour = 0; dt.min = 0; dt.sec = 0; }
Modifier::StartOfYear => { dt.month = 1; dt.day = 1; dt.hour = 0; dt.min = 0; dt.sec = 0; }
Modifier::StartOfDay => { dt.hour = 0; dt.min = 0; dt.sec = 0; }
Modifier::Weekday(w) => {
let jd = ymd_to_julian(dt.year, dt.month, dt.day);
let current_dow = (jd + 1) % 7; let target = *w as i64;
let diff = (target - current_dow).rem_euclid(7);
let unix = dt.to_unix() + diff * 86400;
dt = DateTime::from_unix(unix);
}
}
dt
}
fn ymd_to_julian(y: i32, m: u8, d: u8) -> i64 {
let (y, m) = (y as i64, m as i64);
let a = (14 - m) / 12;
let yr = y + 4800 - a;
let mo = m + 12 * a - 3;
d as i64 + (153 * mo + 2) / 5 + 365 * yr + yr / 4 - yr / 100 + yr / 400 - 32045
}
fn julian_to_ymd(jd: i64) -> (i32, u8, u8) {
let a = jd + 32044;
let b = (4 * a + 3) / 146097;
let c = a - (146097 * b) / 4;
let d = (4 * c + 3) / 1461;
let e = c - (1461 * d) / 4;
let m = (5 * e + 2) / 153;
let day = (e - (153 * m + 2) / 5 + 1) as u8;
let month = (m + 3 - 12 * (m / 10)) as u8;
let year = (100 * b + d - 4800 + m / 10) as i32;
(year, month, day)
}
fn parse_timestr(s: &str) -> Option<DateTime> {
let s = s.trim();
if s.eq_ignore_ascii_case("now") {
return Some(DateTime::now_utc());
}
if let Ok(jd) = s.parse::<f64>() {
return Some(DateTime::from_julian_day(jd));
}
if s.len() >= 19 && s.as_bytes()[4] == b'-' && s.as_bytes()[10] == b' ' {
let y: i32 = s[0..4].parse().ok()?;
let mo: u8 = s[5..7].parse().ok()?;
let d: u8 = s[8..10].parse().ok()?;
let h: u8 = s[11..13].parse().ok()?;
let mi: u8 = s[14..16].parse().ok()?;
let sc: u8 = s[17..19].parse().ok()?;
return Some(DateTime { year: y, month: mo, day: d, hour: h, min: mi, sec: sc });
}
if s.len() == 10 && s.as_bytes()[4] == b'-' {
let y: i32 = s[0..4].parse().ok()?;
let mo: u8 = s[5..7].parse().ok()?;
let d: u8 = s[8..10].parse().ok()?;
return Some(DateTime { year: y, month: mo, day: d, hour: 0, min: 0, sec: 0 });
}
None
}
fn resolve_datetime(timestr: &str, modifiers: &[String]) -> Option<DateTime> {
let mut dt = parse_timestr(timestr)?;
for m in modifiers {
if let Some(modifier) = parse_modifier(m) {
dt = apply_modifier(dt, &modifier);
}
}
Some(dt)
}
pub fn fn_date(args: &[String]) -> Option<String> {
let timestr = args.first()?;
let mods: Vec<String> = args[1..].to_vec();
Some(resolve_datetime(timestr, &mods)?.date_str())
}
pub fn fn_time(args: &[String]) -> Option<String> {
let timestr = args.first()?;
let mods: Vec<String> = args[1..].to_vec();
Some(resolve_datetime(timestr, &mods)?.time_str())
}
pub fn fn_datetime(args: &[String]) -> Option<String> {
let timestr = args.first()?;
let mods: Vec<String> = args[1..].to_vec();
Some(resolve_datetime(timestr, &mods)?.datetime_str())
}
pub fn fn_julianday(args: &[String]) -> Option<f64> {
let timestr = args.first()?;
let mods: Vec<String> = args[1..].to_vec();
Some(resolve_datetime(timestr, &mods)?.to_julian_day())
}
pub fn fn_strftime(args: &[String]) -> Option<String> {
let fmt = args.first()?;
let timestr = args.get(1)?;
let mods: Vec<String> = args[2..].to_vec();
let dt = resolve_datetime(timestr, &mods)?;
let mut out = String::new();
let chars: Vec<char> = fmt.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' && i + 1 < chars.len() {
i += 1;
match chars[i] {
'Y' => out.push_str(&format!("{:04}", dt.year)),
'm' => out.push_str(&format!("{:02}", dt.month)),
'd' => out.push_str(&format!("{:02}", dt.day)),
'H' => out.push_str(&format!("{:02}", dt.hour)),
'M' => out.push_str(&format!("{:02}", dt.min)),
'S' => out.push_str(&format!("{:02}", dt.sec)),
'j' => { let jan1 = ymd_to_julian(dt.year, 1, 1);
let today = ymd_to_julian(dt.year, dt.month, dt.day);
out.push_str(&format!("{:03}", today - jan1 + 1));
}
'w' => { let jd = ymd_to_julian(dt.year, dt.month, dt.day);
out.push_str(&format!("{}", (jd + 1) % 7));
}
'W' => { let jan1 = ymd_to_julian(dt.year, 1, 1);
let today = ymd_to_julian(dt.year, dt.month, dt.day);
out.push_str(&format!("{:02}", (today - jan1) / 7));
}
'J' => out.push_str(&format!("{}", dt.to_julian_day())),
's' => out.push_str(&format!("{}", dt.to_unix())),
'%' => out.push('%'),
c => { out.push('%'); out.push(c); }
}
} else {
out.push(chars[i]);
}
i += 1;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &str) -> Vec<String> { v.split(',').map(|s| s.trim().to_string()).collect() }
#[test]
fn date_basic() {
let r = fn_date(&s("2024-03-15")).unwrap();
assert_eq!(r, "2024-03-15");
}
#[test]
fn date_add_days() {
let r = fn_date(&s("2024-03-15, +5 days")).unwrap();
assert_eq!(r, "2024-03-20");
}
#[test]
fn date_subtract_days() {
let r = fn_date(&s("2024-03-15, -5 days")).unwrap();
assert_eq!(r, "2024-03-10");
}
#[test]
fn date_start_of_month() {
let r = fn_date(&s("2024-03-15, start of month")).unwrap();
assert_eq!(r, "2024-03-01");
}
#[test]
fn date_start_of_year() {
let r = fn_date(&s("2024-08-20, start of year")).unwrap();
assert_eq!(r, "2024-01-01");
}
#[test]
fn time_basic() {
let r = fn_time(&s("2024-03-15 14:30:00")).unwrap();
assert_eq!(r, "14:30:00");
}
#[test]
fn datetime_basic() {
let r = fn_datetime(&s("2024-03-15 14:30:00")).unwrap();
assert_eq!(r, "2024-03-15 14:30:00");
}
#[test]
fn julianday_epoch() {
let jd = fn_julianday(&s("1970-01-01")).unwrap();
assert!((jd - 2440587.5).abs() < 0.01);
}
#[test]
fn strftime_format() {
let r = fn_strftime(&vec!["%Y/%m/%d".into(), "2024-03-15".into()]).unwrap();
assert_eq!(r, "2024/03/15");
}
#[test]
fn strftime_time() {
let r = fn_strftime(&vec!["%H:%M:%S".into(), "2024-03-15 09:05:03".into()]).unwrap();
assert_eq!(r, "09:05:03");
}
#[test]
fn add_months() {
let r = fn_date(&s("2024-01-31, +1 month")).unwrap();
assert!(r.starts_with("2024-02"));
}
#[test]
fn add_years() {
let r = fn_date(&s("2024-03-15, +1 year")).unwrap();
assert_eq!(r, "2025-03-15");
}
#[test]
fn now_returns_string() {
let r = fn_date(&vec!["now".into()]).unwrap();
assert_eq!(r.len(), 10);
assert_eq!(&r[4..5], "-");
}
}