use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Default)]
pub struct DateConfig {
pub date_string: Option<String>,
pub date_file: Option<String>,
pub iso_format: Option<IsoFormat>,
pub rfc_email: bool,
pub rfc_3339: Option<Rfc3339Format>,
pub reference_file: Option<String>,
pub set_string: Option<String>,
pub utc: bool,
pub format: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum IsoFormat {
Date,
Hours,
Minutes,
Seconds,
Ns,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Rfc3339Format {
Date,
Seconds,
Ns,
}
pub fn parse_iso_format(s: &str) -> Result<IsoFormat, String> {
match s {
"" | "date" => Ok(IsoFormat::Date),
"hours" => Ok(IsoFormat::Hours),
"minutes" => Ok(IsoFormat::Minutes),
"seconds" => Ok(IsoFormat::Seconds),
"ns" => Ok(IsoFormat::Ns),
_ => Err(format!("invalid ISO 8601 format: '{}'", s)),
}
}
pub fn parse_rfc3339_format(s: &str) -> Result<Rfc3339Format, String> {
match s {
"date" => Ok(Rfc3339Format::Date),
"seconds" => Ok(Rfc3339Format::Seconds),
"ns" => Ok(Rfc3339Format::Ns),
_ => Err(format!("invalid RFC 3339 format: '{}'", s)),
}
}
pub fn format_date(time: &SystemTime, format: &str, utc: bool) -> String {
let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
let secs = dur.as_secs() as i64;
let nanos = dur.subsec_nanos();
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
if utc {
unsafe {
libc::gmtime_r(&secs, &mut tm);
}
} else {
unsafe {
libc::localtime_r(&secs, &mut tm);
}
}
let mut result = String::with_capacity(format.len() * 2);
let chars: Vec<char> = format.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' && i + 1 < chars.len() {
let modifier = if i + 2 < chars.len()
&& (chars[i + 1] == '-' || chars[i + 1] == '_' || chars[i + 1] == '0')
&& chars[i + 2].is_ascii_alphabetic()
{
let m = chars[i + 1];
i += 1; Some(m)
} else {
None
};
match chars[i + 1] {
's' => {
result.push_str(&secs.to_string());
i += 2;
}
'N' => {
result.push_str(&format!("{:09}", nanos));
i += 2;
}
'q' => {
let month = tm.tm_mon; let quarter = (month / 3) + 1;
result.push_str(&quarter.to_string());
i += 2;
}
'P' => {
let ampm = if tm.tm_hour < 12 { "am" } else { "pm" };
result.push_str(ampm);
i += 2;
}
'Z' if utc => {
result.push_str("UTC");
i += 2;
}
'n' => {
result.push('\n');
i += 2;
}
't' => {
result.push('\t');
i += 2;
}
_ => {
let spec = format!("%{}", chars[i + 1]);
let formatted = strftime_single(&tm, &spec);
let formatted = if let Some(mod_char) = modifier {
apply_format_modifier(&formatted, mod_char)
} else {
formatted
};
result.push_str(&formatted);
i += 2;
}
}
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn strftime_single(tm: &libc::tm, fmt: &str) -> String {
let c_fmt = match std::ffi::CString::new(fmt) {
Ok(c) => c,
Err(_) => return String::new(),
};
let mut buf = vec![0u8; 128];
let len = unsafe {
libc::strftime(
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
c_fmt.as_ptr(),
tm,
)
};
if len == 0 && !fmt.is_empty() && fmt != "%%" {
return String::new();
}
buf.truncate(len);
String::from_utf8_lossy(&buf).into_owned()
}
fn apply_format_modifier(formatted: &str, modifier: char) -> String {
match modifier {
'-' => {
let trimmed = formatted.trim_start_matches(['0', ' ']);
if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
}
}
'_' => {
let mut result = String::with_capacity(formatted.len());
let mut leading = true;
for ch in formatted.chars() {
if leading && ch == '0' {
result.push(' ');
} else {
leading = false;
result.push(ch);
}
}
result
}
'0' => {
let mut result = String::with_capacity(formatted.len());
let mut leading = true;
for ch in formatted.chars() {
if leading && ch == ' ' {
result.push('0');
} else {
leading = false;
result.push(ch);
}
}
result
}
_ => formatted.to_string(),
}
}
pub fn format_iso(time: &SystemTime, precision: &IsoFormat, utc: bool) -> String {
match precision {
IsoFormat::Date => format_date(time, "%Y-%m-%d", utc),
IsoFormat::Hours => {
let date_part = format_date(time, "%Y-%m-%dT%H", utc);
let tz = format_timezone_colon(time, utc);
format!("{}{}", date_part, tz)
}
IsoFormat::Minutes => {
let date_part = format_date(time, "%Y-%m-%dT%H:%M", utc);
let tz = format_timezone_colon(time, utc);
format!("{}{}", date_part, tz)
}
IsoFormat::Seconds => {
let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
let tz = format_timezone_colon(time, utc);
format!("{}{}", date_part, tz)
}
IsoFormat::Ns => {
let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
let nanos = dur.subsec_nanos();
let date_part = format_date(time, "%Y-%m-%dT%H:%M:%S", utc);
let tz = format_timezone_colon(time, utc);
format!("{},{:09}{}", date_part, nanos, tz)
}
}
}
pub fn format_rfc_email(time: &SystemTime, utc: bool) -> String {
format_date(time, "%a, %d %b %Y %H:%M:%S %z", utc)
}
pub fn format_rfc3339(time: &SystemTime, precision: &Rfc3339Format, utc: bool) -> String {
match precision {
Rfc3339Format::Date => format_date(time, "%Y-%m-%d", utc),
Rfc3339Format::Seconds => {
let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
let tz = format_timezone_colon(time, utc);
format!("{}{}", date_part, tz)
}
Rfc3339Format::Ns => {
let dur = time.duration_since(UNIX_EPOCH).unwrap_or_default();
let nanos = dur.subsec_nanos();
let date_part = format_date(time, "%Y-%m-%d %H:%M:%S", utc);
let tz = format_timezone_colon(time, utc);
format!("{}.{:09}{}", date_part, nanos, tz)
}
}
}
fn format_timezone_colon(time: &SystemTime, utc: bool) -> String {
if utc {
return "+00:00".to_string();
}
let raw = format_date(time, "%z", false);
if raw.len() >= 5 {
format!("{}:{}", &raw[..3], &raw[3..5])
} else {
raw
}
}
pub fn parse_date_string(s: &str, utc: bool) -> Result<SystemTime, String> {
let s = s.trim();
if let Some(epoch_str) = s.strip_prefix('@') {
let secs: i64 = epoch_str
.trim()
.parse()
.map_err(|_| format!("invalid date '@{}'", epoch_str))?;
if secs >= 0 {
return Ok(UNIX_EPOCH + Duration::from_secs(secs as u64));
} else {
return Ok(UNIX_EPOCH - Duration::from_secs((-secs) as u64));
}
}
let now = SystemTime::now();
match s.to_lowercase().as_str() {
"now" | "today" => return Ok(now),
"yesterday" => {
return Ok(now - Duration::from_secs(86400));
}
"tomorrow" => {
return Ok(now + Duration::from_secs(86400));
}
_ => {}
}
if let Some(result) = try_parse_relative(s, &now) {
return Ok(result);
}
if let Some(result) = try_parse_time_only(s, utc) {
return Ok(result);
}
if let Some(result) = try_parse_iso(s, utc) {
return Ok(result);
}
Err(format!("invalid date '{}'", s))
}
fn try_parse_time_only(s: &str, utc: bool) -> Option<SystemTime> {
let s = s.trim();
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.is_empty() || parts.len() > 2 {
return None;
}
let time_str = parts[0];
let tz_str = if parts.len() == 2 {
Some(parts[1])
} else {
None
};
let time_fields: Vec<&str> = time_str.split(':').collect();
if time_fields.len() < 2 || time_fields.len() > 3 {
return None;
}
let hour: u32 = time_fields[0].parse().ok()?;
let minute: u32 = time_fields[1].parse().ok()?;
let second: u32 = if time_fields.len() == 3 {
time_fields[2].parse().ok()?
} else {
0
};
if hour > 23 || minute > 59 || second > 60 {
return None;
}
let mut use_utc = utc;
let mut tz_offset_secs: i64 = 0;
if let Some(tz) = tz_str {
if tz.eq_ignore_ascii_case("UTC") || tz == "Z" {
use_utc = true;
} else if (tz.starts_with('+') || tz.starts_with('-')) && (tz.len() == 5 || tz.len() == 3) {
let sign: i64 = if tz.starts_with('-') { -1 } else { 1 };
let digits = &tz[1..];
if !digits.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let (oh, om) = if digits.len() == 4 {
let h: i64 = digits[..2].parse().ok()?;
let m: i64 = digits[2..].parse().ok()?;
(h, m)
} else {
let h: i64 = digits.parse().ok()?;
(h, 0)
};
tz_offset_secs = sign * (oh * 3600 + om * 60);
use_utc = true;
} else {
return None;
}
}
let now = SystemTime::now();
let now_secs = now.duration_since(UNIX_EPOCH).ok()?.as_secs() as i64;
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
if use_utc {
let now_t = now_secs as libc::time_t;
unsafe {
libc::gmtime_r(&now_t, &mut tm);
}
} else {
let now_t = now_secs as libc::time_t;
unsafe {
libc::localtime_r(&now_t, &mut tm);
}
}
tm.tm_hour = hour as i32;
tm.tm_min = minute as i32;
tm.tm_sec = second as i32;
tm.tm_isdst = -1;
let epoch_secs = if use_utc {
unsafe { libc::timegm(&mut tm) }
} else {
unsafe { libc::mktime(&mut tm) }
};
if epoch_secs == -1 {
return None;
}
let final_secs = epoch_secs as i64 - tz_offset_secs;
if final_secs >= 0 {
Some(UNIX_EPOCH + Duration::from_secs(final_secs as u64))
} else {
Some(UNIX_EPOCH - Duration::from_secs((-final_secs) as u64))
}
}
fn try_parse_relative(s: &str, now: &SystemTime) -> Option<SystemTime> {
let lower = s.to_lowercase();
let parts: Vec<&str> = lower.split_whitespace().collect();
if parts.len() < 2 {
return None;
}
let is_ago = parts.last().map_or(false, |&p| p == "ago");
let num_str = parts[0].trim_start_matches('+');
let amount: i64 = num_str.parse().ok()?;
let unit_idx = 1;
if unit_idx >= parts.len() {
return None;
}
let unit = parts[unit_idx];
let seconds = match unit.trim_end_matches('s') {
"second" => amount,
"minute" => amount * 60,
"hour" => amount * 3600,
"day" => amount * 86400,
"week" => amount * 86400 * 7,
"month" => amount * 86400 * 30,
"year" => amount * 86400 * 365,
_ => return None,
};
let duration = Duration::from_secs(seconds.unsigned_abs());
if is_ago || seconds < 0 {
Some(*now - duration)
} else {
Some(*now + duration)
}
}
fn try_parse_iso(s: &str, utc: bool) -> Option<SystemTime> {
let s = s.replace('T', " ");
let parts: Vec<&str> = s.splitn(2, ' ').collect();
let date_part = parts[0];
let time_part = if parts.len() > 1 {
parts[1]
} else {
"00:00:00"
};
let date_fields: Vec<&str> = date_part.split('-').collect();
if date_fields.len() != 3 {
return None;
}
let year: i32 = date_fields[0].parse().ok()?;
let month: u32 = date_fields[1].parse().ok()?;
let day: u32 = date_fields[2].parse().ok()?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let time_clean = time_part
.split('+')
.next()
.unwrap_or(time_part)
.split('Z')
.next()
.unwrap_or(time_part);
let time_fields: Vec<&str> = time_clean.split(':').collect();
let hour: u32 = time_fields
.first()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let minute: u32 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let second: u32 = time_fields
.get(2)
.and_then(|s| s.split('.').next())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if hour > 23 || minute > 59 || second > 60 {
return None;
}
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
tm.tm_year = year - 1900;
tm.tm_mon = month as i32 - 1;
tm.tm_mday = day as i32;
tm.tm_hour = hour as i32;
tm.tm_min = minute as i32;
tm.tm_sec = second as i32;
tm.tm_isdst = -1;
let epoch_secs = if utc {
unsafe { libc::timegm(&mut tm) }
} else {
unsafe { libc::mktime(&mut tm) }
};
if epoch_secs == -1 {
return None;
}
if epoch_secs >= 0 {
Some(UNIX_EPOCH + Duration::from_secs(epoch_secs as u64))
} else {
Some(UNIX_EPOCH - Duration::from_secs((-epoch_secs) as u64))
}
}
pub fn file_mod_time(path: &str) -> Result<SystemTime, String> {
std::fs::metadata(path)
.map_err(|e| format!("{}: {}", path, e))?
.modified()
.map_err(|e| format!("{}: {}", path, e))
}
pub fn default_format() -> &'static str {
"%a %b %e %H:%M:%S %Z %Y"
}