use serde_json::{json, Value};
fn format_strftime(fmt: &str, ts: libc::time_t) -> String {
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
let p = unsafe { libc::localtime_r(&ts, &mut tm) };
if p.is_null() {
return String::new();
}
let mut buf = vec![0i8; 256];
let cfmt = std::ffi::CString::new(fmt).unwrap_or_default();
let n = unsafe { libc::strftime(buf.as_mut_ptr(), buf.len(), cfmt.as_ptr(), &tm) };
if n == 0 {
return String::new();
}
let bytes: Vec<u8> = buf[..n].iter().map(|&b| b as u8).collect();
String::from_utf8_lossy(&bytes).into_owned()
}
pub fn date(_pl: &(), format: &str, istime: bool, _timezone: Option<&str>) -> Vec<Value> {
let ts = unsafe { libc::time(std::ptr::null_mut()) };
let contents = format_strftime(format, ts);
let highlight_groups: Vec<Value> = if istime {
vec![Value::String("time".into()), Value::String("date".into())]
} else {
vec![Value::String("date".into())]
};
let divider_highlight_group = if istime {
json!("time:divider")
} else {
Value::Null
};
vec![json!({
"contents": contents,
"highlight_groups": highlight_groups,
"divider_highlight_group": divider_highlight_group,
})]
}
pub fn unicode_text_translate(s: &str) -> String {
s.replace('\'', "\u{2019}").replace('-', "\u{2010}")
}
pub fn fuzzy_time_default_hour_str() -> Vec<&'static str> {
vec![
"twelve", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
"eleven",
]
}
pub fn fuzzy_time_default_minute_str() -> std::collections::HashMap<u32, &'static str> {
let mut m = std::collections::HashMap::new();
m.insert(0, "{hour_str} o'clock");
m.insert(5, "five past {hour_str}");
m.insert(10, "ten past {hour_str}");
m.insert(15, "quarter past {hour_str}");
m.insert(20, "twenty past {hour_str}");
m.insert(25, "twenty-five past {hour_str}");
m.insert(30, "half past {hour_str}");
m.insert(35, "twenty-five to {hour_str}");
m.insert(40, "twenty to {hour_str}");
m.insert(45, "quarter to {hour_str}");
m.insert(50, "ten to {hour_str}");
m.insert(55, "five to {hour_str}");
m
}
pub fn fuzzy_time_default_special_cases() -> std::collections::HashMap<(u32, u32), &'static str> {
let mut m = std::collections::HashMap::new();
m.insert((23, 58), "round about midnight");
m.insert((23, 59), "round about midnight");
m.insert((0, 0), "midnight");
m.insert((0, 1), "round about midnight");
m.insert((0, 2), "round about midnight");
m.insert((12, 0), "noon");
m
}
pub fn fuzzy_time_compute(
hour: u32,
minute: u32,
hour_str: &[&str],
minute_str: &std::collections::HashMap<u32, &str>,
special_cases: &std::collections::HashMap<(u32, u32), &str>,
unicode_text: bool,
) -> String {
if let Some(s) = special_cases.get(&(hour, minute)) {
if unicode_text {
return unicode_text_translate(s);
}
return s.to_string();
}
let mut h = hour;
if minute >= 32 {
h += 1;
}
let h = (h as usize) % hour_str.len();
let mut min_dis: u32 = 100;
let mut min_pos: u32 = 0;
let mut keys: Vec<u32> = minute_str.keys().copied().collect();
keys.sort();
for &mn in &keys {
let dis = minute.abs_diff(mn);
if dis < min_dis {
min_dis = dis;
min_pos = mn;
}
}
let template = minute_str.get(&min_pos).copied().unwrap_or("");
let result = template.replace("{hour_str}", hour_str[h]);
if unicode_text {
unicode_text_translate(&result)
} else {
result
}
}
pub fn fuzzy_time(
_format: Option<&str>,
unicode_text: bool,
_timezone: Option<&str>,
hour_str: Option<&[&str]>,
minute_str: Option<&std::collections::HashMap<u32, &str>>,
special_cases: Option<&std::collections::HashMap<(u32, u32), &str>>,
) -> String {
let ts = unsafe { libc::time(std::ptr::null_mut()) };
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
let p = unsafe { libc::localtime_r(&ts, &mut tm) };
if p.is_null() {
return String::new();
}
let hour = tm.tm_hour as u32;
let minute = tm.tm_min as u32;
let default_hour = fuzzy_time_default_hour_str();
let h: &[&str] = hour_str.unwrap_or(&default_hour);
let default_minute = fuzzy_time_default_minute_str();
let m_ref: &std::collections::HashMap<u32, &str> = minute_str.unwrap_or(&default_minute);
let default_special = fuzzy_time_default_special_cases();
let s_ref: &std::collections::HashMap<(u32, u32), &str> =
special_cases.unwrap_or(&default_special);
fuzzy_time_compute(hour, minute, h, m_ref, s_ref, unicode_text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn date_returns_non_empty_contents() {
let segments = date(&(), "%Y-%m-%d", false, None);
assert_eq!(segments.len(), 1);
let contents = segments[0]["contents"].as_str().unwrap();
assert_eq!(contents.len(), 10);
assert_eq!(contents.matches('-').count(), 2);
}
#[test]
fn date_istime_uses_time_highlight() {
let segments = date(&(), "%H:%M:%S", true, None);
let hl = segments[0]["highlight_groups"].as_array().unwrap();
assert_eq!(hl[0], "time");
assert_eq!(hl[1], "date");
}
#[test]
fn date_istime_emits_time_divider() {
let segments = date(&(), "%H:%M", true, None);
assert_eq!(segments[0]["divider_highlight_group"], "time:divider");
}
#[test]
fn date_not_istime_omits_time_divider() {
let segments = date(&(), "%Y", false, None);
assert_eq!(segments[0]["divider_highlight_group"], Value::Null);
assert_eq!(segments[0]["highlight_groups"].as_array().unwrap().len(), 1);
assert_eq!(segments[0]["highlight_groups"][0], "date");
}
#[test]
fn date_yyyymmdd_format() {
let segments = date(&(), "%Y", false, None);
let contents = segments[0]["contents"].as_str().unwrap();
assert_eq!(contents.len(), 4);
assert!(contents.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn date_h_format_emits_two_digit_hour() {
let segments = date(&(), "%H", false, None);
let contents = segments[0]["contents"].as_str().unwrap();
assert_eq!(contents.len(), 2);
assert!(contents.chars().all(|c| c.is_ascii_digit()));
let h: i32 = contents.parse().unwrap();
assert!((0..24).contains(&h));
}
#[test]
fn format_strftime_basic_unix_epoch() {
let s = format_strftime("%Y-%m-%d", 0);
assert_eq!(s.len(), 10);
}
#[test]
fn unicode_text_translate_replaces_apostrophe_and_hyphen() {
assert_eq!(unicode_text_translate("o'clock"), "o\u{2019}clock");
assert_eq!(unicode_text_translate("twenty-five"), "twenty\u{2010}five");
}
#[test]
fn unicode_text_translate_no_op_on_plain_text() {
assert_eq!(unicode_text_translate("noon"), "noon");
}
#[test]
fn fuzzy_time_default_hour_str_has_12_entries() {
let h = fuzzy_time_default_hour_str();
assert_eq!(h.len(), 12);
assert_eq!(h[0], "twelve");
assert_eq!(h[11], "eleven");
}
#[test]
fn fuzzy_time_default_minute_str_has_12_entries() {
let m = fuzzy_time_default_minute_str();
assert_eq!(m.len(), 12);
for key in [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] {
assert!(m.contains_key(&key));
}
}
#[test]
fn fuzzy_time_default_minute_str_quarter_past() {
let m = fuzzy_time_default_minute_str();
assert_eq!(*m.get(&15).unwrap(), "quarter past {hour_str}");
}
#[test]
fn fuzzy_time_default_special_cases_includes_noon_and_midnight() {
let s = fuzzy_time_default_special_cases();
assert_eq!(*s.get(&(0, 0)).unwrap(), "midnight");
assert_eq!(*s.get(&(12, 0)).unwrap(), "noon");
assert_eq!(*s.get(&(23, 58)).unwrap(), "round about midnight");
}
#[test]
fn fuzzy_time_compute_quarter_past_six() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(
fuzzy_time_compute(6, 15, &h, &m, &s, false),
"quarter past six"
);
}
#[test]
fn fuzzy_time_compute_o_clock() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(fuzzy_time_compute(3, 0, &h, &m, &s, false), "three o'clock");
}
#[test]
fn fuzzy_time_compute_special_case_noon() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(fuzzy_time_compute(12, 0, &h, &m, &s, false), "noon");
}
#[test]
fn fuzzy_time_compute_special_case_midnight() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(fuzzy_time_compute(0, 0, &h, &m, &s, false), "midnight");
}
#[test]
fn fuzzy_time_compute_special_case_round_about_midnight() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(
fuzzy_time_compute(23, 59, &h, &m, &s, false),
"round about midnight"
);
}
#[test]
fn fuzzy_time_compute_rounds_up_after_32() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(
fuzzy_time_compute(5, 40, &h, &m, &s, false),
"twenty to six"
);
}
#[test]
fn fuzzy_time_compute_unicode_text_translates_apostrophe() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
let r = fuzzy_time_compute(3, 0, &h, &m, &s, true);
assert_eq!(r, "three o\u{2019}clock");
}
#[test]
fn fuzzy_time_compute_unicode_text_translates_hyphen_in_special() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let mut s = std::collections::HashMap::new();
s.insert((10, 10), "ten-thirty");
let r = fuzzy_time_compute(10, 10, &h, &m, &s, true);
assert!(r.contains('\u{2010}'));
}
#[test]
fn fuzzy_time_compute_modulo_24_hour_wraps_to_12_label() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(fuzzy_time_compute(18, 0, &h, &m, &s, false), "six o'clock");
}
#[test]
fn fuzzy_time_compute_closest_minute_for_unaligned_input() {
let h = fuzzy_time_default_hour_str();
let m = fuzzy_time_default_minute_str();
let s = fuzzy_time_default_special_cases();
assert_eq!(
fuzzy_time_compute(6, 13, &h, &m, &s, false),
"quarter past six"
);
}
#[test]
fn fuzzy_time_smoke_test_uses_localtime() {
let r = fuzzy_time(None, false, None, None, None, None);
assert!(!r.is_empty());
}
}