use chrono::{Datelike, Timelike}; use std::sync::OnceLock; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; const MILLIS_PER_DAY: i64 = 86_400_000; /// A worker and its application test share this clock; advancing it never sleeps. #[derive(Clone, Default)] pub struct Clock(Option>); impl Clock { pub fn for_test() -> Self { Self(Some(std::sync::Arc::new( std::sync::atomic::AtomicI64::new(0), ))) } pub fn now(&self) -> Result { match &self.0 { Some(clock) => Ok(clock.load(std::sync::atomic::Ordering::SeqCst)), None => now().map_err(crate::orm::Error::invalid_data), } } pub fn advance(&self, millis: i64) -> Result<(), String> { if millis < 0 { return Err("test clock cannot move backwards".into()); } let clock = self .0 .as_ref() .ok_or("production clock cannot be advanced")?; clock .fetch_update( std::sync::atomic::Ordering::SeqCst, std::sync::atomic::Ordering::SeqCst, |current| current.checked_add(millis), ) .map(|_| ()) .map_err(|_| "test clock overflow".into()) } } pub fn now() -> Result { let value = unix_millis()?; datetime(value)?; Ok(value) } fn datetime(value: i64) -> Result, String> { chrono::DateTime::from_timestamp_millis(value) .filter(|value| (0..=9999).contains(&value.year())) .ok_or_else(|| "DateTime is outside the four-digit UTC calendar range".into()) } pub fn parse_datetime(text: &str) -> Result { if let Some(fraction) = text.get(19..).and_then(|tail| tail.strip_prefix('.')) && fraction.bytes().take_while(u8::is_ascii_digit).count() > 3 { return Err("DateTime requires at most three fractional digits".into()); } let parsed = chrono::DateTime::parse_from_rfc3339(text) .map_err(|_| "invalid RFC3339 DateTime".to_owned())?; if parsed.nanosecond() >= 1_000_000_000 || parsed.nanosecond() % 1_000_000 != 0 { return Err("DateTime requires millisecond precision and no leap seconds".into()); } let value = parsed.timestamp_millis(); datetime(value)?; Ok(value) } pub fn format_datetime(value: i64) -> Result { let value = datetime(value)?; let precision = if value.timestamp_subsec_millis() == 0 { chrono::SecondsFormat::Secs } else { chrono::SecondsFormat::Millis }; Ok(value.to_rfc3339_opts(precision, true)) } pub fn parse_date(text: &str) -> Result { if text.len() != 10 || text.as_bytes()[4] != b'-' || text.as_bytes()[7] != b'-' || !text .bytes() .enumerate() .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit()) { return Err("Date must use YYYY-MM-DD".into()); } let date = chrono::NaiveDate::parse_from_str(text, "%Y-%m-%d") .map_err(|_| "invalid calendar Date".to_owned())?; Ok(date .and_hms_opt(0, 0, 0) .expect("midnight") .and_utc() .timestamp_millis()) } pub fn format_date(value: i64) -> Result { if value.rem_euclid(MILLIS_PER_DAY) != 0 { return Err("Date must be UTC midnight milliseconds".into()); } Ok(datetime(value)?.format("%Y-%m-%d").to_string()) } pub fn parse_time(text: &str) -> Result { let bytes = text.as_bytes(); if !matches!(bytes.len(), 8 | 12) || bytes[2] != b':' || bytes[5] != b':' || (bytes.len() == 12 && bytes[8] != b'.') || !bytes .iter() .enumerate() .all(|(index, byte)| matches!(index, 2 | 5 | 8) || byte.is_ascii_digit()) { return Err("Time must use HH:MM:SS[.sss]".into()); } let time = chrono::NaiveTime::parse_from_str(text, "%H:%M:%S%.f") .map_err(|_| "invalid clock Time".to_owned())?; if time.nanosecond() >= 1_000_000_000 { return Err("Time does not accept leap seconds".into()); } Ok(i64::from(time.num_seconds_from_midnight()) * 1000 + i64::from(time.nanosecond() / 1_000_000)) } pub fn format_time(value: i64) -> Result { if !(0..MILLIS_PER_DAY).contains(&value) { return Err("Time must be milliseconds within one day".into()); } let time = chrono::NaiveTime::from_num_seconds_from_midnight_opt( (value / 1000) as u32, (value % 1000) as u32 * 1_000_000, ) .expect("validated time of day"); Ok(time .format(if value % 1000 == 0 { "%H:%M:%S" } else { "%H:%M:%S%.3f" }) .to_string()) } pub fn duration(millis: i64) -> i64 { millis } pub fn add(value: i64, duration: i64) -> Result { value .checked_add(duration) .ok_or_else(|| "DateTime addition overflow".into()) } pub fn subtract(value: i64, duration: i64) -> Result { value .checked_sub(duration) .ok_or_else(|| "DateTime subtraction overflow".into()) } pub fn difference(later: i64, earlier: i64) -> Result { later .checked_sub(earlier) .ok_or_else(|| "Duration difference overflow".into()) } pub fn unix_millis() -> Result { let elapsed = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|error| error.to_string())?; i64::try_from(elapsed.as_millis()).map_err(|_| "clock value is out of Int range".into()) } pub fn monotonic_nanos() -> Result { static ORIGIN: OnceLock = OnceLock::new(); i64::try_from(ORIGIN.get_or_init(Instant::now).elapsed().as_nanos()) .map_err(|_| "clock value is out of Int range".into()) } pub fn sleep(millis: i64) -> Result<(), String> { let millis = u64::try_from(millis).map_err(|_| "sleep duration must not be negative")?; std::thread::sleep(Duration::from_millis(millis)); Ok(()) }