use super::error::{EvalResult, Flow, signal};
use super::intern::resolve_sym;
use super::value::*;
use crate::emacs_core::value::ValueKind;
use std::cell::RefCell;
use std::ffi::{CStr, OsString};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
fn expect_args(name: &str, args: &[Value], n: usize) -> Result<(), Flow> {
if args.len() != n {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_min_max_args(name: &str, args: &[Value], min: usize, max: usize) -> Result<(), Flow> {
if args.len() < min || args.len() > max {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
struct TimeMicros {
secs: i64,
usecs: i64,
psecs: i64,
}
impl TimeMicros {
fn now() -> Self {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(dur) => TimeMicros {
secs: dur.as_secs() as i64,
usecs: dur.subsec_micros() as i64,
psecs: 0,
},
Err(e) => {
let dur = e.duration();
TimeMicros {
secs: -(dur.as_secs() as i64),
usecs: -(dur.subsec_micros() as i64),
psecs: 0,
}
}
}
}
fn to_list(&self) -> Value {
let high = self.secs >> 16;
let low = self.secs & 0xFFFF;
Value::list(vec![
Value::fixnum(high),
Value::fixnum(low),
Value::fixnum(self.usecs),
Value::fixnum(self.psecs),
])
}
fn to_float(&self) -> f64 {
self.secs as f64 + self.usecs as f64 / 1_000_000.0
}
fn add(self, other: TimeMicros) -> TimeMicros {
let mut psecs = self.psecs + other.psecs;
let mut usecs = self.usecs + other.usecs;
let mut secs = self.secs + other.secs;
if psecs >= 1_000_000 {
psecs -= 1_000_000;
usecs += 1;
} else if psecs < 0 {
psecs += 1_000_000;
usecs -= 1;
}
if usecs >= 1_000_000 {
usecs -= 1_000_000;
secs += 1;
} else if usecs < 0 {
usecs += 1_000_000;
secs -= 1;
}
TimeMicros { secs, usecs, psecs }
}
fn sub(self, other: TimeMicros) -> TimeMicros {
let mut psecs = self.psecs - other.psecs;
let mut usecs = self.usecs - other.usecs;
let mut secs = self.secs - other.secs;
if psecs < 0 {
psecs += 1_000_000;
usecs -= 1;
} else if psecs >= 1_000_000 {
psecs -= 1_000_000;
usecs += 1;
}
if usecs < 0 {
usecs += 1_000_000;
secs -= 1;
} else if usecs >= 1_000_000 {
usecs -= 1_000_000;
secs += 1;
}
TimeMicros { secs, usecs, psecs }
}
fn less_than(self, other: TimeMicros) -> bool {
if self.secs != other.secs {
self.secs < other.secs
} else if self.usecs != other.usecs {
self.usecs < other.usecs
} else {
self.psecs < other.psecs
}
}
fn equal(self, other: TimeMicros) -> bool {
self.secs == other.secs && self.usecs == other.usecs && self.psecs == other.psecs
}
}
fn parse_time(val: &Value) -> Result<TimeMicros, Flow> {
use crate::emacs_core::value::VecLikeType;
if let ValueKind::Veclike(VecLikeType::Bignum) = val.kind() {
let f = val.as_bignum().unwrap().to_f64();
return Ok(TimeMicros {
secs: f as i64,
usecs: 0,
psecs: 0,
});
}
match val.kind() {
ValueKind::Nil => Ok(TimeMicros::now()),
ValueKind::Fixnum(n) => Ok(TimeMicros {
secs: n,
usecs: 0,
psecs: 0,
}),
ValueKind::Float => {
let f = val.xfloat();
let secs = f.floor() as i64;
let usecs = ((f - f.floor()) * 1_000_000.0).round() as i64;
Ok(TimeMicros {
secs,
usecs,
psecs: 0,
})
}
ValueKind::Cons => {
let items = list_to_vec(val)
.ok_or_else(|| signal("wrong-type-argument", vec![Value::symbol("listp"), *val]))?;
if items.len() < 2 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), *val],
));
}
let high = items[0].as_int().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), items[0]],
)
})?;
let low = items[1].as_int().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), items[1]],
)
})?;
let usec = if items.len() > 2 {
items[2].as_int().unwrap_or(0)
} else {
0
};
let psec = if items.len() > 3 {
items[3].as_int().unwrap_or(0)
} else {
0
};
let secs = high * 65536 + low;
Ok(TimeMicros {
secs,
usecs: usec,
psecs: psec,
})
}
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("numberp"), *val],
)),
}
}
fn is_leap_year(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn days_in_month(month: i64, year: i64) -> i64 {
match month {
1 => 31,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
_ => 30,
}
}
fn days_in_year(year: i64) -> i64 {
if is_leap_year(year) { 366 } else { 365 }
}
struct DecodedTime {
sec: i64,
min: i64,
hour: i64,
day: i64, month: i64, year: i64,
dow: i64, }
fn decode_epoch_secs(total_secs: i64) -> DecodedTime {
let mut days = total_secs.div_euclid(86400);
let day_secs = total_secs.rem_euclid(86400);
let sec = day_secs % 60;
let min = (day_secs / 60) % 60;
let hour = day_secs / 3600;
let dow = ((days % 7) + 4).rem_euclid(7);
let mut year: i64 = 1970;
if days >= 0 {
loop {
let dy = days_in_year(year);
if days < dy {
break;
}
days -= dy;
year += 1;
}
} else {
loop {
year -= 1;
let dy = days_in_year(year);
days += dy;
if days >= 0 {
break;
}
}
}
let mut month: i64 = 1;
loop {
let dm = days_in_month(month, year);
if days < dm {
break;
}
days -= dm;
month += 1;
if month > 12 {
break;
}
}
let day = days + 1;
DecodedTime {
sec,
min,
hour,
day,
month,
year,
dow,
}
}
fn encode_to_epoch_secs(sec: i64, min: i64, hour: i64, day: i64, month: i64, year: i64) -> i64 {
let mut total_days: i64 = 0;
if year >= 1970 {
for y in 1970..year {
total_days += days_in_year(y);
}
} else {
for y in year..1970 {
total_days -= days_in_year(y);
}
}
for m in 1..month {
total_days += days_in_month(m, year);
}
total_days += day - 1;
total_days * 86400 + hour * 3600 + min * 60 + sec
}
const DAY_NAMES: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTH_NAMES: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
#[derive(Clone, Debug)]
enum ZoneRule {
Local,
Utc,
FixedOffset(i64),
FixedNamedOffset(i64, String),
TzString(String),
}
thread_local! {
static TIME_ZONE_RULE: RefCell<ZoneRule> = RefCell::new(ZoneRule::Local);
}
pub(crate) fn reset_timefns_thread_locals() {
TIME_ZONE_RULE.with(|slot| *slot.borrow_mut() = ZoneRule::Local);
}
fn tz_env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
fn invalid_time_zone_spec(spec: &Value) -> Flow {
signal(
"error",
vec![Value::string("Invalid time zone specification"), *spec],
)
}
fn format_fixed_offset_name(offset_secs: i64) -> String {
if offset_secs == 0 {
return "GMT".to_string();
}
let sign = if offset_secs < 0 { '-' } else { '+' };
let abs_secs = offset_secs.abs();
if abs_secs % 3600 == 0 {
format!("{}{abs_hours:02}", sign, abs_hours = abs_secs / 3600)
} else if abs_secs % 60 == 0 {
let total_minutes = abs_secs / 60;
format!(
"{}{hours:02}{mins:02}",
sign,
hours = total_minutes / 60,
mins = total_minutes % 60
)
} else {
format!(
"{}{hours:02}{mins:02}{secs:02}",
sign,
hours = abs_secs / 3600,
mins = (abs_secs % 3600) / 60,
secs = abs_secs % 60
)
}
}
#[cfg(unix)]
fn local_offset_name_at_epoch(epoch_secs: i64) -> (i64, String) {
let mut time_val: libc::time_t = epoch_secs as libc::time_t;
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
let tm_ptr = unsafe { libc::localtime_r(&mut time_val as *mut _, &mut tm as *mut _) };
if tm_ptr.is_null() {
return (0, "UTC".to_string());
}
let offset = tm.tm_gmtoff as i64;
let name = if tm.tm_zone.is_null() {
format_fixed_offset_name(offset)
} else {
unsafe { CStr::from_ptr(tm.tm_zone) }
.to_string_lossy()
.into_owned()
};
(offset, name)
}
#[cfg(not(unix))]
fn local_offset_name_at_epoch(_epoch_secs: i64) -> (i64, String) {
(0, "UTC".to_string())
}
#[cfg(unix)]
fn refresh_tz_env() {
unsafe extern "C" {
fn tzset();
}
unsafe {
tzset();
}
}
#[cfg(not(unix))]
fn refresh_tz_env() {}
struct ScopedTzEnv {
previous: Option<OsString>,
}
impl ScopedTzEnv {
fn new(spec: Option<&str>) -> Self {
let previous = std::env::var_os("TZ");
match spec {
Some(v) => unsafe { std::env::set_var("TZ", v) },
None => unsafe { std::env::remove_var("TZ") },
}
refresh_tz_env();
Self { previous }
}
}
impl Drop for ScopedTzEnv {
fn drop(&mut self) {
match &self.previous {
Some(v) => unsafe { std::env::set_var("TZ", v) },
None => unsafe { std::env::remove_var("TZ") },
}
refresh_tz_env();
}
}
fn with_tz_env<T>(spec: Option<&str>, f: impl FnOnce() -> T) -> T {
let _lock = tz_env_lock().lock().expect("time zone env lock poisoned");
let _guard = ScopedTzEnv::new(spec);
f()
}
fn parse_zone_rule(zone: &Value) -> Result<ZoneRule, Flow> {
match zone.kind() {
ValueKind::Nil => Ok(ZoneRule::Local),
ValueKind::T => Ok(ZoneRule::Utc),
ValueKind::Symbol(id) if resolve_sym(id) == "wall" => Ok(ZoneRule::Local),
ValueKind::Fixnum(n) => Ok(ZoneRule::FixedOffset(n)),
ValueKind::String => Ok(ZoneRule::TzString(
zone.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload"),
)),
ValueKind::Cons => {
let items = list_to_vec(zone).ok_or_else(|| invalid_time_zone_spec(zone))?;
if items.len() != 2 {
return Err(invalid_time_zone_spec(zone));
}
let Some(offset) = items[0].as_int() else {
return Err(invalid_time_zone_spec(zone));
};
let name = match items[1].kind() {
ValueKind::String => items[1]
.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload"),
ValueKind::Symbol(id) => resolve_sym(id).to_owned(),
_ => return Err(invalid_time_zone_spec(zone)),
};
Ok(ZoneRule::FixedNamedOffset(offset, name))
}
_ => Err(invalid_time_zone_spec(zone)),
}
}
fn zone_rule_to_offset_name(rule: &ZoneRule, epoch_secs: i64) -> (i64, String) {
match rule {
ZoneRule::Local => local_offset_name_at_epoch(epoch_secs),
ZoneRule::Utc => (0, "GMT".to_string()),
ZoneRule::FixedOffset(offset) => (*offset, format_fixed_offset_name(*offset)),
ZoneRule::FixedNamedOffset(offset, name) => (*offset, name.clone()),
ZoneRule::TzString(spec) => {
with_tz_env(Some(spec), || local_offset_name_at_epoch(epoch_secs))
}
}
}
pub(crate) fn zone_offset_name_for_time(
zone: Option<&Value>,
epoch_secs: i64,
) -> Result<(i64, String), Flow> {
let rule = if let Some(zone) = zone {
parse_zone_rule(zone)?
} else {
TIME_ZONE_RULE.with(|slot| slot.borrow().clone())
};
Ok(zone_rule_to_offset_name(&rule, epoch_secs))
}
fn require_integer_component(value: &Value) -> Result<i64, Flow> {
value.as_int().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), *value],
)
})
}
fn encode_time_zone_offset(zone: &Value, approx_epoch_secs: i64) -> Result<i64, Flow> {
let rule = parse_zone_rule(zone)?;
let initial = zone_rule_to_offset_name(&rule, approx_epoch_secs).0;
Ok(match rule {
ZoneRule::Local | ZoneRule::TzString(_) => {
let adjusted_epoch = approx_epoch_secs - initial;
zone_rule_to_offset_name(&rule, adjusted_epoch).0
}
_ => initial,
})
}
pub(crate) fn builtin_current_time(args: Vec<Value>) -> EvalResult {
expect_args("current-time", &args, 0)?;
Ok(TimeMicros::now().to_list())
}
pub(crate) fn builtin_float_time(args: Vec<Value>) -> EvalResult {
expect_min_max_args("float-time", &args, 0, 1)?;
let tm = if args.is_empty() || args[0].is_nil() {
TimeMicros::now()
} else {
parse_time(&args[0])?
};
Ok(Value::make_float(tm.to_float()))
}
pub(crate) fn builtin_time_add(args: Vec<Value>) -> EvalResult {
expect_args("time-add", &args, 2)?;
let a = parse_time(&args[0])?;
let b = parse_time(&args[1])?;
Ok(a.add(b).to_list())
}
pub(crate) fn builtin_time_subtract(args: Vec<Value>) -> EvalResult {
expect_args("time-subtract", &args, 2)?;
let a = parse_time(&args[0])?;
let b = parse_time(&args[1])?;
Ok(a.sub(b).to_list())
}
pub(crate) fn builtin_time_less_p(args: Vec<Value>) -> EvalResult {
expect_args("time-less-p", &args, 2)?;
let a = parse_time(&args[0])?;
let b = parse_time(&args[1])?;
Ok(Value::bool_val(a.less_than(b)))
}
pub(crate) fn builtin_time_equal_p(args: Vec<Value>) -> EvalResult {
expect_args("time-equal-p", &args, 2)?;
if args[0] == args[1] {
return Ok(Value::T);
}
if args[0].is_nil() || args[1].is_nil() {
return Ok(Value::NIL);
}
let a = parse_time(&args[0])?;
let b = parse_time(&args[1])?;
Ok(Value::bool_val(a.equal(b)))
}
pub(crate) fn builtin_current_time_string(args: Vec<Value>) -> EvalResult {
expect_min_max_args("current-time-string", &args, 0, 2)?;
let tm = if args.is_empty() || args[0].is_nil() {
TimeMicros::now()
} else {
parse_time(&args[0])?
};
let (offset_secs, _) = zone_offset_name_for_time(args.get(1), tm.secs)?;
let dt = decode_epoch_secs(tm.secs.saturating_add(offset_secs));
let s = format!(
"{} {} {:2} {:02}:{:02}:{:02} {}",
DAY_NAMES[dt.dow as usize],
MONTH_NAMES[(dt.month - 1) as usize],
dt.day,
dt.hour,
dt.min,
dt.sec,
dt.year,
);
Ok(Value::string(s))
}
pub(crate) fn builtin_current_time_zone(args: Vec<Value>) -> EvalResult {
expect_min_max_args("current-time-zone", &args, 0, 2)?;
let tm = if args.is_empty() || args[0].is_nil() {
TimeMicros::now()
} else {
parse_time(&args[0])?
};
let rule = if args.len() > 1 {
parse_zone_rule(&args[1])?
} else {
TIME_ZONE_RULE.with(|slot| slot.borrow().clone())
};
let (offset, name) = zone_rule_to_offset_name(&rule, tm.secs);
Ok(Value::list(vec![
Value::fixnum(offset),
Value::string(name),
]))
}
pub(crate) fn builtin_encode_time(args: Vec<Value>) -> EvalResult {
let (sec, min, hour, day, month, year, zone) = if args.len() == 1 {
let items = list_to_vec(&args[0])
.ok_or_else(|| signal("wrong-type-argument", vec![Value::symbol("listp"), args[0]]))?;
if items.len() < 6 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), args[0]],
));
}
(
require_integer_component(&items[0])?,
require_integer_component(&items[1])?,
require_integer_component(&items[2])?,
require_integer_component(&items[3])?,
require_integer_component(&items[4])?,
require_integer_component(&items[5])?,
items.get(8).copied().unwrap_or(Value::NIL),
)
} else if args.len() < 6 {
return Err(signal(
"wrong-number-of-arguments",
vec![
Value::symbol("encode-time"),
Value::fixnum(args.len() as i64),
],
));
} else {
(
require_integer_component(&args[0])?,
require_integer_component(&args[1])?,
require_integer_component(&args[2])?,
require_integer_component(&args[3])?,
require_integer_component(&args[4])?,
require_integer_component(&args[5])?,
if args.len() > 6 {
args.last().copied().unwrap_or(Value::NIL)
} else {
Value::NIL
},
)
};
let local_secs = encode_to_epoch_secs(sec, min, hour, day, month, year);
let zone_offset = encode_time_zone_offset(&zone, local_secs)?;
let total_secs = local_secs - zone_offset;
let high = total_secs >> 16;
let low = total_secs & 0xFFFF;
Ok(Value::list(vec![Value::fixnum(high), Value::fixnum(low)]))
}
pub(crate) fn builtin_decode_time(args: Vec<Value>) -> EvalResult {
expect_min_max_args("decode-time", &args, 0, 2)?;
let tm = if args.is_empty() || args[0].is_nil() {
TimeMicros::now()
} else {
parse_time(&args[0])?
};
let dt = decode_epoch_secs(tm.secs);
Ok(Value::list(vec![
Value::fixnum(dt.sec),
Value::fixnum(dt.min),
Value::fixnum(dt.hour),
Value::fixnum(dt.day),
Value::fixnum(dt.month),
Value::fixnum(dt.year),
Value::fixnum(dt.dow),
Value::NIL, Value::fixnum(0), ]))
}
pub(crate) fn builtin_time_convert(args: Vec<Value>) -> EvalResult {
expect_min_max_args("time-convert", &args, 1, 2)?;
let tm = parse_time(&args[0])?;
let form = if args.len() > 1 {
&args[1]
} else {
&Value::NIL
};
match form.kind() {
ValueKind::Nil => Ok(tm.to_list()),
ValueKind::T => {
let hz: i64 = 1_000_000;
let ticks = tm.secs * hz + tm.usecs;
Ok(Value::cons(Value::fixnum(ticks), Value::fixnum(hz)))
}
ValueKind::Symbol(id) => match resolve_sym(id) {
"list" => Ok(tm.to_list()),
"integer" => Ok(Value::fixnum(tm.secs)),
"float" => Ok(Value::make_float(tm.to_float())),
_ => Ok(tm.to_list()),
},
ValueKind::Fixnum(_) => {
Ok(Value::cons(Value::fixnum(tm.secs), Value::fixnum(1)))
}
_ => Ok(tm.to_list()),
}
}
pub(crate) fn builtin_set_time_zone_rule(args: Vec<Value>) -> EvalResult {
expect_args("set-time-zone-rule", &args, 1)?;
let rule = parse_zone_rule(&args[0])?;
TIME_ZONE_RULE.with(|slot| *slot.borrow_mut() = rule);
Ok(Value::NIL)
}
#[cfg(test)]
#[path = "timefns_test.rs"]
mod tests;