use std::sync::OnceLock;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use bigdecimal::ToPrimitive;
use crate::error::{DogeError, DogeResult};
use crate::stdlib::str_arg;
use crate::value::Value;
const SECS_PER_DAY: i64 = 86_400;
static MONO_ORIGIN: OnceLock<Instant> = OnceLock::new();
fn numeric(fname: &str, v: &Value) -> DogeResult<f64> {
match v {
Value::Int(n) => n
.to_f64()
.ok_or_else(|| DogeError::overflow(format!("{n} is too large for nap.{fname}"))),
Value::Float(f) => Ok(*f),
_ => Err(DogeError::type_error(format!(
"nap.{fname} needs a number, got {}",
v.describe()
))),
}
}
pub fn nap_now() -> DogeResult {
let secs = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(elapsed) => elapsed.as_secs_f64(),
Err(before) => -before.duration().as_secs_f64(),
};
Ok(Value::Float(secs))
}
pub fn nap_mono() -> DogeResult {
let origin = MONO_ORIGIN.get_or_init(Instant::now);
Ok(Value::Float(origin.elapsed().as_secs_f64()))
}
pub fn nap_rest(seconds: &Value) -> DogeResult {
let secs = numeric("rest", seconds)?;
if !secs.is_finite() || secs < 0.0 {
return Err(DogeError::value_error(format!(
"cannot rest for {secs} seconds — the duration must be finite and not negative"
)));
}
if secs > Duration::MAX.as_secs_f64() {
return Err(DogeError::value_error(
"cannot rest that long — the duration is out of range",
));
}
std::thread::sleep(Duration::from_secs_f64(secs));
Ok(Value::None)
}
pub fn nap_stamp(secs: &Value) -> DogeResult {
let secs = numeric("stamp", secs)?;
if !secs.is_finite() || secs < i64::MIN as f64 || secs >= i64::MAX as f64 {
return Err(DogeError::value_error(
"that timestamp is outside the representable range",
));
}
let secs = secs.floor() as i64;
let days = secs.div_euclid(SECS_PER_DAY);
let time_of_day = secs.rem_euclid(SECS_PER_DAY);
let (year, month, day) = civil_from_days(days);
let hour = time_of_day / 3600;
let minute = (time_of_day % 3600) / 60;
let second = time_of_day % 60;
Ok(Value::str(format!(
"{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
)))
}
pub fn nap_parse(text: &Value) -> DogeResult {
let text = str_arg("nap", "parse", text)?;
let secs = parse_iso8601(text).ok_or_else(|| {
DogeError::value_error(format!(
"cannot read \"{text}\" as a timestamp — expected \"YYYY-MM-DDTHH:MM:SSZ\""
))
})?;
Ok(Value::Float(secs as f64))
}
fn parse_iso8601(text: &str) -> Option<i64> {
let text = text.strip_suffix('Z').unwrap_or(text);
let (date, time) = text.split_once('T')?;
let mut date_parts = date.splitn(3, '-');
let year: i64 = signed_field(date_parts.next()?)?;
let month: i64 = field(date_parts.next()?)?;
let day: i64 = field(date_parts.next()?)?;
if date_parts.next().is_some() {
return None;
}
let mut time_parts = time.splitn(3, ':');
let hour: i64 = field(time_parts.next()?)?;
let minute: i64 = field(time_parts.next()?)?;
let second: i64 = field(time_parts.next()?)?;
if time_parts.next().is_some() {
return None;
}
if !(1..=12).contains(&month)
|| !(1..=31).contains(&day)
|| !(0..=23).contains(&hour)
|| !(0..=59).contains(&minute)
|| !(0..=59).contains(&second)
{
return None;
}
let days = days_from_civil(year, month, day);
Some(days * SECS_PER_DAY + hour * 3600 + minute * 60 + second)
}
fn field(s: &str) -> Option<i64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
fn signed_field(s: &str) -> Option<i64> {
match s.strip_prefix('-') {
Some(rest) => field(rest).map(|n| -n),
None => field(s),
}
}
fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let year = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
(if month <= 2 { year + 1 } else { year }, month, day)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
fn stamp(secs: i64) -> String {
match nap_stamp(&Value::int(secs)).unwrap() {
Value::Str(s) => s.to_string(),
other => panic!("expected a Str, got {other:?}"),
}
}
fn parse(text: &str) -> f64 {
match nap_parse(&Value::str(text)).unwrap() {
Value::Float(f) => f,
other => panic!("expected a Float, got {other:?}"),
}
}
#[test]
fn stamp_formats_known_timestamps() {
assert_eq!(stamp(0), "1970-01-01T00:00:00Z");
assert_eq!(stamp(946_684_800), "2000-01-01T00:00:00Z");
assert_eq!(stamp(1_609_459_199), "2020-12-31T23:59:59Z");
}
#[test]
fn stamp_handles_pre_epoch_dates() {
assert_eq!(stamp(-1), "1969-12-31T23:59:59Z");
assert_eq!(stamp(-SECS_PER_DAY), "1969-12-31T00:00:00Z");
}
#[test]
fn parse_is_the_inverse_of_stamp() {
for secs in [0, 946_684_800, 1_609_459_199, -1, -SECS_PER_DAY] {
assert_eq!(parse(&stamp(secs)), secs as f64);
}
}
#[test]
fn parse_tolerates_a_missing_z() {
assert_eq!(parse("2000-01-01T00:00:00"), 946_684_800.0);
}
#[test]
fn parse_rejects_malformed_input() {
for bad in [
"not a date",
"2000-13-01T00:00:00Z",
"2000-01-32T00:00:00Z",
"2000-01-01T24:00:00Z",
"2000-01-01 00:00:00Z",
"2000/01/01T00:00:00Z",
"2000-01-01T00:00:00+02:00",
"",
] {
assert_eq!(
nap_parse(&Value::str(bad)).unwrap_err().kind,
ErrorKind::ValueError,
"{bad:?} should be a ValueError"
);
}
}
#[test]
fn rest_rejects_bad_durations() {
assert_eq!(
nap_rest(&Value::int(-1)).unwrap_err().kind,
ErrorKind::ValueError
);
assert_eq!(
nap_rest(&Value::Float(f64::NAN)).unwrap_err().kind,
ErrorKind::ValueError
);
assert_eq!(
nap_rest(&Value::Float(f64::INFINITY)).unwrap_err().kind,
ErrorKind::ValueError
);
}
#[test]
fn rest_zero_returns_none() {
assert!(matches!(nap_rest(&Value::int(0)).unwrap(), Value::None));
}
#[test]
fn mono_never_goes_backwards() {
let a = match nap_mono().unwrap() {
Value::Float(f) => f,
other => panic!("expected a Float, got {other:?}"),
};
let b = match nap_mono().unwrap() {
Value::Float(f) => f,
other => panic!("expected a Float, got {other:?}"),
};
assert!(b >= a);
}
#[test]
fn now_is_after_the_epoch() {
match nap_now().unwrap() {
Value::Float(f) => assert!(f > 0.0),
other => panic!("expected a Float, got {other:?}"),
}
}
#[test]
fn stamp_out_of_range_is_value_error() {
assert_eq!(
nap_stamp(&Value::Float(f64::INFINITY)).unwrap_err().kind,
ErrorKind::ValueError
);
}
#[test]
fn bad_arg_types_are_type_errors() {
assert_eq!(
nap_rest(&Value::str("soon")).unwrap_err().kind,
ErrorKind::TypeError
);
assert_eq!(
nap_stamp(&Value::str("soon")).unwrap_err().kind,
ErrorKind::TypeError
);
assert_eq!(
nap_parse(&Value::int(0)).unwrap_err().kind,
ErrorKind::TypeError
);
}
}