use super::tm::{
empty_tm, get_time_sec, init_tm_unknown, local_time_tzoffset, local_tzoffset, time_to_tm,
time_to_tm_local, tm_to_time_t, TzHhmm,
};
use libc::{time_t, tm};
use std::ffi::CString;
use std::io::IsTerminal;
const MONTH_NAMES: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const WEEKDAY_NAMES: [&str; 7] = [
"Sundays",
"Mondays",
"Tuesdays",
"Wednesdays",
"Thursdays",
"Fridays",
"Saturdays",
];
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum DateModeType {
Normal,
Human,
Relative,
Short,
Iso8601,
Iso8601Strict,
Rfc2822,
Strftime,
Raw,
Unix,
}
pub struct DateMode {
pub ty: DateModeType,
pub local: bool,
pub strftime_fmt: Option<String>,
}
impl DateMode {
pub fn from_type(ty: DateModeType) -> Self {
Self {
ty,
local: false,
strftime_fmt: None,
}
}
}
pub fn parse_date_format(format: &str) -> Result<DateMode, &'static str> {
let mut s = format;
if let Some(rest) = s.strip_prefix("auto:") {
s = if std::io::stdout().is_terminal() {
rest
} else {
"default"
};
}
if s == "local" {
s = "default-local";
}
let (ty, mut p) = parse_date_type(s)?;
let mut local = false;
if let Some(r) = p.strip_prefix("-local") {
local = true;
p = r;
}
let mut mode = DateMode {
ty,
local,
strftime_fmt: None,
};
if ty == DateModeType::Strftime {
let rest = p
.strip_prefix(':')
.ok_or("date format missing colon separator")?;
mode.strftime_fmt = Some(rest.to_string());
} else if !p.is_empty() {
return Err("unknown date format");
}
Ok(mode)
}
fn parse_date_type(s: &str) -> Result<(DateModeType, &str), &'static str> {
if let Some(r) = s.strip_prefix("relative") {
return Ok((DateModeType::Relative, r));
}
if let Some(r) = s
.strip_prefix("iso8601-strict")
.or_else(|| s.strip_prefix("iso-strict"))
{
return Ok((DateModeType::Iso8601Strict, r));
}
if let Some(r) = s.strip_prefix("iso8601").or_else(|| s.strip_prefix("iso")) {
return Ok((DateModeType::Iso8601, r));
}
if let Some(r) = s.strip_prefix("rfc2822").or_else(|| s.strip_prefix("rfc")) {
return Ok((DateModeType::Rfc2822, r));
}
if let Some(r) = s.strip_prefix("short") {
return Ok((DateModeType::Short, r));
}
if let Some(r) = s.strip_prefix("default") {
return Ok((DateModeType::Normal, r));
}
if let Some(r) = s.strip_prefix("human") {
return Ok((DateModeType::Human, r));
}
if let Some(r) = s.strip_prefix("raw") {
return Ok((DateModeType::Raw, r));
}
if let Some(r) = s.strip_prefix("unix") {
return Ok((DateModeType::Unix, r));
}
if let Some(r) = s.strip_prefix("format") {
return Ok((DateModeType::Strftime, r));
}
Err("unknown date format")
}
pub fn date_mode_release(mode: &mut DateMode) {
mode.strftime_fmt = None;
}
pub fn show_date_relative(time: u64, now_sec: i64) -> String {
let now = now_sec as i128;
let t = time as i128;
if now < t {
return "in the future".to_string();
}
let mut diff = (now - t) as u64;
if diff < 90 {
return if diff == 1 {
"1 second ago".to_string()
} else {
format!("{diff} seconds ago")
};
}
diff = (diff + 30) / 60;
if diff < 90 {
return if diff == 1 {
"1 minute ago".to_string()
} else {
format!("{diff} minutes ago")
};
}
diff = (diff + 30) / 60;
if diff < 36 {
return if diff == 1 {
"1 hour ago".to_string()
} else {
format!("{diff} hours ago")
};
}
diff = (diff + 12) / 24;
if diff < 14 {
return if diff == 1 {
"1 day ago".to_string()
} else {
format!("{diff} days ago")
};
}
if diff < 70 {
let w = (diff + 3) / 7;
return if w == 1 {
"1 week ago".to_string()
} else {
format!("{w} weeks ago")
};
}
if diff < 365 {
let m = (diff + 15) / 30;
return if m == 1 {
"1 month ago".to_string()
} else {
format!("{m} months ago")
};
}
if diff < 1825 {
let totalmonths = (diff * 12 * 2 + 365) / (365 * 2);
let years = totalmonths / 12;
let months = totalmonths % 12;
if months > 0 {
let ys = if years == 1 {
"1 year".to_string()
} else {
format!("{years} years")
};
return if months == 1 {
format!("{ys}, 1 month ago")
} else {
format!("{ys}, {months} months ago")
};
}
return if years == 1 {
"1 year ago".to_string()
} else {
format!("{years} years ago")
};
}
let y = (diff + 183) / 365;
if y == 1 {
"1 year ago".to_string()
} else {
format!("{y} years ago")
}
}
fn strbuf_rtrim(s: &mut String) {
while let Some(c) = s.pop() {
if !c.is_whitespace() {
s.push(c);
break;
}
}
}
fn show_date_normal(
time: u64,
tm: &tm,
tz: TzHhmm,
human_tm: &tm,
human_tz: TzHhmm,
local: bool,
) -> String {
#[derive(Clone, Copy)]
struct Hide {
year: bool,
date: bool,
wday: bool,
time: bool,
seconds: bool,
tz: bool,
}
let mut hide = Hide {
year: false,
date: false,
wday: false,
time: false,
seconds: false,
tz: false,
};
hide.tz = local || tz == human_tz;
hide.year = tm.tm_year == human_tm.tm_year;
if hide.year && tm.tm_mon == human_tm.tm_mon {
if tm.tm_mday > human_tm.tm_mday {
} else if tm.tm_mday == human_tm.tm_mday {
hide.date = true;
hide.wday = true;
} else if tm.tm_mday + 5 > human_tm.tm_mday {
hide.date = true;
}
}
if hide.wday {
return show_date_relative(time, get_time_sec());
}
if human_tm.tm_year != 0 {
hide.seconds = true;
hide.tz |= !hide.date;
hide.wday = !hide.year;
hide.time = !hide.year;
}
let mut out = String::new();
if !hide.wday {
let w = WEEKDAY_NAMES[tm.tm_wday as usize].as_bytes();
out.push_str(std::str::from_utf8(&w[..3]).unwrap_or("Sun"));
out.push(' ');
}
if !hide.date {
let m = MONTH_NAMES[tm.tm_mon as usize].as_bytes();
out.push_str(std::str::from_utf8(&m[..3]).unwrap_or("Jan"));
out.push(' ');
out.push_str(&format!("{} ", tm.tm_mday));
}
if !hide.time {
out.push_str(&format!("{:02}:{:02}", tm.tm_hour, tm.tm_min));
if !hide.seconds {
out.push_str(&format!(":{:02}", tm.tm_sec));
}
} else {
strbuf_rtrim(&mut out);
}
if !hide.year {
out.push_str(&format!(" {}", tm.tm_year + 1900));
}
if !hide.tz {
out.push_str(&format!(" {:+05}", tz));
}
out
}
fn strbuf_expand_step(munged: &mut String, fmt: &mut &str) -> bool {
let Some(pct) = fmt.find('%') else {
munged.push_str(fmt);
*fmt = "";
return false;
};
munged.push_str(&fmt[..pct]);
*fmt = &fmt[pct + 1..];
true
}
pub fn strbuf_addftime(tm: &tm, tz_hhmm: TzHhmm, fmt: &str, suppress_tz_name: bool) -> String {
if fmt.is_empty() {
return String::new();
}
let mut munged = String::new();
let mut rest = fmt;
while strbuf_expand_step(&mut munged, &mut rest) {
if rest.starts_with('%') {
munged.push_str("%%");
rest = &rest[1..];
} else if rest.starts_with('s') {
let secs = tm_to_time_t(tm) as i64
- 3600 * (tz_hhmm / 100) as i64
- 60 * (tz_hhmm % 100) as i64;
munged.push_str(&format!("{secs}"));
rest = &rest[1..];
} else if rest.starts_with('z') {
munged.push_str(&format!("{:+05}", tz_hhmm));
rest = &rest[1..];
} else if suppress_tz_name && rest.starts_with('Z') {
rest = &rest[1..];
} else {
munged.push('%');
}
}
strftime_c(&munged, tm)
}
fn strftime_c(fmt: &str, tm: &tm) -> String {
let mut buf = vec![0u8; 4096];
let cfmt = match CString::new(fmt) {
Ok(c) => c,
Err(_) => CString::new("%Y").unwrap(),
};
unsafe {
let n = libc::strftime(
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
cfmt.as_ptr(),
tm,
);
if n == 0 && !fmt.is_empty() {
let mut munged = fmt.to_string();
munged.push(' ');
let c2 = CString::new(munged.as_str()).unwrap();
let n2 = libc::strftime(
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
c2.as_ptr(),
tm,
);
if n2 > 0 {
return String::from_utf8_lossy(&buf[..n2 - 1]).into_owned();
}
}
String::from_utf8_lossy(&buf[..n]).into_owned()
}
}
pub fn show_date(time: u64, mut tz: TzHhmm, mode: &mut DateMode) -> String {
if mode.ty == DateModeType::Unix {
return format!("{time}");
}
let mut tmbuf = init_tm_unknown();
let mut human_tm = empty_tm();
let mut human_tz: TzHhmm = -1;
if mode.ty == DateModeType::Human {
let now = get_time_sec();
unsafe {
human_tz = local_time_tzoffset(now as time_t, &mut human_tm);
}
}
if mode.local {
tz = local_tzoffset(time);
}
if mode.ty == DateModeType::Raw {
return format!("{time} {:+05}", tz);
}
if mode.ty == DateModeType::Relative {
return show_date_relative(time, get_time_sec());
}
let mut tz = tz;
let ok = if mode.local {
unsafe { time_to_tm_local(time, &mut tmbuf).is_some() }
} else {
unsafe { time_to_tm(time, tz, &mut tmbuf).is_some() }
};
if !ok {
unsafe {
time_to_tm(0, 0, &mut tmbuf);
}
tz = 0;
}
match mode.ty {
DateModeType::Short => format!(
"{:04}-{:02}-{:02}",
tmbuf.tm_year + 1900,
tmbuf.tm_mon + 1,
tmbuf.tm_mday
),
DateModeType::Iso8601 => format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} {:+05}",
tmbuf.tm_year + 1900,
tmbuf.tm_mon + 1,
tmbuf.tm_mday,
tmbuf.tm_hour,
tmbuf.tm_min,
tmbuf.tm_sec,
tz
),
DateModeType::Iso8601Strict => {
let mut s = format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
tmbuf.tm_year + 1900,
tmbuf.tm_mon + 1,
tmbuf.tm_mday,
tmbuf.tm_hour,
tmbuf.tm_min,
tmbuf.tm_sec
);
if tz == 0 {
s.push('Z');
} else {
let sign = if tz >= 0 { '+' } else { '-' };
let a = tz.abs();
s.push(sign);
s.push_str(&format!("{:02}:{:02}", a / 100, a % 100));
}
s
}
DateModeType::Rfc2822 => format!(
"{}, {} {} {} {:02}:{:02}:{:02} {:+05}",
&WEEKDAY_NAMES[tmbuf.tm_wday as usize][..3],
tmbuf.tm_mday,
&MONTH_NAMES[tmbuf.tm_mon as usize][..3],
tmbuf.tm_year + 1900,
tmbuf.tm_hour,
tmbuf.tm_min,
tmbuf.tm_sec,
tz
),
DateModeType::Strftime => {
let fmt = mode.strftime_fmt.as_deref().unwrap_or("");
strbuf_addftime(&tmbuf, tz, fmt, !mode.local)
}
_ => show_date_normal(time, &tmbuf, tz, &human_tm, human_tz, mode.local),
}
}