use std::sync::OnceLock;
pub use std::time::Duration;
use eyre::{Result, bail};
use jiff::{Span, Timestamp, Zoned, civil::date};
pub const HOURLY: Duration = Duration::from_secs(60 * 60);
pub const DAILY: Duration = Duration::from_secs(60 * 60 * 24);
pub const WEEKLY: Duration = Duration::from_secs(60 * 60 * 24 * 7);
pub fn elapsed_seconds_ceil(from: Timestamp, to: Timestamp) -> u64 {
if from >= to {
return 0;
}
let nanos = to.as_nanosecond() - from.as_nanosecond();
u64::try_from((nanos + 999_999_999) / 1_000_000_000)
.expect("elapsed timestamp delta must fit into u64")
}
pub fn process_now() -> Timestamp {
static PROCESS_NOW: OnceLock<Timestamp> = OnceLock::new();
*PROCESS_NOW.get_or_init(Timestamp::now)
}
pub fn parse_duration(s: &str) -> Result<Duration> {
match s.parse::<Span>() {
Ok(span) => {
let duration = span.to_duration(date(2025, 1, 1))?;
if duration.is_negative() {
bail!("duration must not be negative: {}", s);
}
Ok(duration.unsigned_abs())
}
Err(_) => Ok(Duration::from_secs(s.parse()?)),
}
}
pub fn parse_into_timestamp(s: &str) -> Result<Timestamp> {
if let Ok(ts) = s.parse::<Timestamp>() {
return Ok(ts);
}
if let Ok(zoned) = s.parse::<Zoned>() {
return Ok(zoned.timestamp());
}
if let Ok(civil_date) = s.parse::<jiff::civil::Date>() {
let datetime = civil_date.at(23, 59, 59, 0);
let ts = datetime.to_zoned(jiff::tz::TimeZone::UTC)?.timestamp();
return Ok(ts);
}
if let Ok(span) = s.parse::<Span>() {
let duration = span.to_duration(date(2025, 1, 1))?;
if duration.is_negative() {
bail!("duration must not be negative: {}", s);
}
let now_zoned = process_now().to_zoned(jiff::tz::TimeZone::UTC);
let past = now_zoned.checked_sub(span)?;
return Ok(past.timestamp());
}
bail!(
"Invalid date or duration: {s}. Expected formats: '2024-06-01', '2024-06-01T12:00:00Z', '90d', '1y'"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_now_is_stable() {
let a = process_now();
std::thread::sleep(std::time::Duration::from_millis(5));
let b = process_now();
assert_eq!(a, b);
}
#[test]
fn test_parse_into_timestamp_relative_is_stable() {
let a = parse_into_timestamp("3d").unwrap();
std::thread::sleep(std::time::Duration::from_millis(5));
let b = parse_into_timestamp("3d").unwrap();
assert_eq!(a, b);
}
#[test]
fn test_parse_into_timestamp_absolute_date() {
let ts = parse_into_timestamp("2024-01-02").unwrap();
assert_eq!(ts.to_string(), "2024-01-02T23:59:59Z");
}
#[test]
fn test_parse_into_timestamp_rfc3339() {
let ts = parse_into_timestamp("2024-01-02T03:04:05Z").unwrap();
assert_eq!(ts.to_string(), "2024-01-02T03:04:05Z");
}
#[test]
fn test_parse_into_timestamp_rejects_garbage() {
assert!(parse_into_timestamp("not a date").is_err());
}
#[test]
fn test_elapsed_seconds_ceil_exact_boundary() {
let a: Timestamp = "2024-01-01T00:00:00Z".parse().unwrap();
let b: Timestamp = "2024-01-01T00:00:01Z".parse().unwrap();
assert_eq!(elapsed_seconds_ceil(a, b), 1);
}
#[test]
fn test_elapsed_seconds_ceil_rounds_up_subsecond() {
let a: Timestamp = "2024-01-01T00:00:00.000000001Z".parse().unwrap();
let b: Timestamp = "2024-01-01T00:00:01Z".parse().unwrap();
assert_eq!(elapsed_seconds_ceil(a, b), 1);
}
#[test]
fn test_elapsed_seconds_ceil_rounds_up_fractional_second() {
let a: Timestamp = "2024-01-01T00:00:00Z".parse().unwrap();
let b: Timestamp = "2024-01-01T00:00:01.100Z".parse().unwrap();
assert_eq!(elapsed_seconds_ceil(a, b), 2);
}
#[test]
fn test_elapsed_seconds_ceil_zero_when_not_elapsed() {
let t: Timestamp = "2024-01-01T00:00:00Z".parse().unwrap();
assert_eq!(elapsed_seconds_ceil(t, t), 0);
let later: Timestamp = "2024-01-02T00:00:00Z".parse().unwrap();
assert_eq!(elapsed_seconds_ceil(later, t), 0);
}
}