use std::num::NonZero;
use crate::coding::VarInt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("time overflow")]
pub struct TimeOverflow;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Timescale(NonZero<u64>);
impl Timescale {
pub const SECOND: Self = Self(NonZero::<u64>::MIN);
pub const MILLI: Self = match NonZero::new(1_000) {
Some(n) => Self(n),
None => unreachable!(),
};
pub const MICRO: Self = match NonZero::new(1_000_000) {
Some(n) => Self(n),
None => unreachable!(),
};
pub const NANO: Self = match NonZero::new(1_000_000_000) {
Some(n) => Self(n),
None => unreachable!(),
};
pub const fn new(units_per_second: u64) -> Result<Self, TimeOverflow> {
if VarInt::from_u64(units_per_second).is_none() {
return Err(TimeOverflow);
}
match NonZero::new(units_per_second) {
Some(n) => Ok(Self(n)),
None => Err(TimeOverflow),
}
}
pub const fn as_u64(self) -> u64 {
self.0.get()
}
}
impl TryFrom<u64> for Timescale {
type Error = TimeOverflow;
fn try_from(units_per_second: u64) -> Result<Self, Self::Error> {
Self::new(units_per_second)
}
}
impl From<NonZero<u64>> for Timescale {
fn from(units_per_second: NonZero<u64>) -> Self {
Self(units_per_second)
}
}
impl From<Timescale> for u64 {
fn from(scale: Timescale) -> Self {
scale.0.get()
}
}
impl From<Timescale> for NonZero<u64> {
fn from(scale: Timescale) -> Self {
scale.0
}
}
impl Default for Timescale {
fn default() -> Self {
Self::MILLI
}
}
impl std::fmt::Debug for Timescale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::SECOND => write!(f, "Timescale::SECOND"),
Self::MILLI => write!(f, "Timescale::MILLI"),
Self::MICRO => write!(f, "Timescale::MICRO"),
Self::NANO => write!(f, "Timescale::NANO"),
Self(n) => write!(f, "Timescale({n})"),
}
}
}
impl std::fmt::Display for Timescale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Timestamp {
value: VarInt,
scale: Timescale,
}
impl Timestamp {
pub const ZERO: Self = Self::new_const(0, Timescale::SECOND);
pub const fn new(value: u64, scale: Timescale) -> Result<Self, TimeOverflow> {
match VarInt::from_u64(value) {
Some(value) => Ok(Self { value, scale }),
None => Err(TimeOverflow),
}
}
pub const fn new_const(value: u64, scale: Timescale) -> Self {
match Self::new(value, scale) {
Ok(time) => time,
Err(_) => panic!("timestamp value exceeds 2^62 - 1"),
}
}
pub fn from_scale(value: u64, units_per_second: u64) -> Result<Self, TimeOverflow> {
Self::new(value, Timescale::new(units_per_second)?)
}
pub const fn from_secs(seconds: u64) -> Result<Self, TimeOverflow> {
Self::new(seconds, Timescale::SECOND)
}
pub const fn from_millis(millis: u64) -> Result<Self, TimeOverflow> {
Self::new(millis, Timescale::MILLI)
}
pub const fn from_micros(micros: u64) -> Result<Self, TimeOverflow> {
Self::new(micros, Timescale::MICRO)
}
pub const fn from_nanos(nanos: u64) -> Result<Self, TimeOverflow> {
Self::new(nanos, Timescale::NANO)
}
pub const fn value(self) -> u64 {
self.value.into_inner()
}
pub const fn scale(self) -> Timescale {
self.scale
}
pub const fn is_zero(self) -> bool {
self.value.into_inner() == 0
}
pub const fn convert(self, new_scale: Timescale) -> Result<Self, TimeOverflow> {
if self.scale.0.get() == new_scale.0.get() {
return Ok(self);
}
match (self.value.into_inner() as u128).checked_mul(new_scale.0.get() as u128) {
Some(scaled) => match VarInt::from_u128(scaled / self.scale.0.get() as u128) {
Some(value) => Ok(Self {
value,
scale: new_scale,
}),
None => Err(TimeOverflow),
},
None => Err(TimeOverflow),
}
}
pub const fn as_scale(self, target: Timescale) -> u128 {
self.value.into_inner() as u128 * target.0.get() as u128 / self.scale.0.get() as u128
}
pub const fn as_secs(self) -> u64 {
self.value.into_inner() / self.scale.0.get()
}
pub const fn as_millis(self) -> u128 {
self.as_scale(Timescale::MILLI)
}
pub const fn as_micros(self) -> u128 {
self.as_scale(Timescale::MICRO)
}
pub const fn as_nanos(self) -> u128 {
self.as_scale(Timescale::NANO)
}
pub const fn checked_add(self, rhs: Self) -> Result<Self, TimeOverflow> {
if self.scale.0.get() != rhs.scale.0.get() {
return Err(TimeOverflow);
}
match self.value.into_inner().checked_add(rhs.value.into_inner()) {
Some(result) => Self::new(result, self.scale),
None => Err(TimeOverflow),
}
}
pub const fn checked_sub(self, rhs: Self) -> Result<Self, TimeOverflow> {
if self.scale.0.get() != rhs.scale.0.get() {
return Err(TimeOverflow);
}
match self.value.into_inner().checked_sub(rhs.value.into_inner()) {
Some(result) => Self::new(result, self.scale),
None => Err(TimeOverflow),
}
}
pub fn now() -> Self {
clock::now()
}
}
impl TryFrom<std::time::Duration> for Timestamp {
type Error = TimeOverflow;
fn try_from(duration: std::time::Duration) -> Result<Self, Self::Error> {
match VarInt::from_u128(duration.as_nanos()) {
Some(value) => Ok(Self {
value,
scale: Timescale::NANO,
}),
None => Err(TimeOverflow),
}
}
}
impl From<Timestamp> for std::time::Duration {
fn from(time: Timestamp) -> Self {
let nanos = time.as_nanos();
std::time::Duration::new(time.as_secs(), (nanos % 1_000_000_000) as u32)
}
}
impl std::fmt::Debug for Timestamp {
#[allow(clippy::manual_is_multiple_of)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let nanos = self.as_nanos();
if nanos % 1_000_000_000 == 0 {
write!(f, "{}s", nanos / 1_000_000_000)
} else if nanos % 1_000_000 == 0 {
write!(f, "{}ms", nanos / 1_000_000)
} else if nanos % 1_000 == 0 {
write!(f, "{}µs", nanos / 1_000)
} else {
write!(f, "{}ns", nanos)
}
}
}
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Timestamp {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
if self.scale.0.get() == other.scale.0.get() {
return self.value.cmp(&other.value);
}
let lhs = self.value.into_inner() as u128 * other.scale.0.get() as u128;
let rhs = other.value.into_inner() as u128 * self.scale.0.get() as u128;
lhs.cmp(&rhs)
.then_with(|| self.scale.0.get().cmp(&other.scale.0.get()))
.then_with(|| self.value.cmp(&other.value))
}
}
#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
mod clock {
use std::sync::LazyLock;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::RngExt;
use super::Timestamp;
const ANCHOR_EPOCH_SECS: u64 = 1_577_836_800;
static TIME_ANCHOR: LazyLock<(std::time::Instant, SystemTime)> = LazyLock::new(|| {
let jitter = std::time::Duration::from_millis(rand::rng().random_range(0..69_420));
(std::time::Instant::now(), SystemTime::now() - jitter)
});
pub(super) fn now() -> Timestamp {
let instant: std::time::Instant = web_async::time::Instant::now().into();
from_std_instant(instant)
}
fn from_std_instant(instant: std::time::Instant) -> Timestamp {
let (anchor_instant, anchor_system) = *TIME_ANCHOR;
let system = match instant.checked_duration_since(anchor_instant) {
Some(forward) => anchor_system + forward,
None => anchor_system - anchor_instant.duration_since(instant),
};
let epoch = UNIX_EPOCH + std::time::Duration::from_secs(ANCHOR_EPOCH_SECS);
let duration = system.duration_since(epoch).unwrap_or(std::time::Duration::ZERO);
Timestamp::from_millis(duration.as_millis() as u64).expect("clock is somehow past the year 2300")
}
impl From<std::time::Instant> for Timestamp {
fn from(instant: std::time::Instant) -> Self {
from_std_instant(instant)
}
}
}
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
mod clock {
use std::sync::LazyLock;
use rand::RngExt;
use super::Timestamp;
static TIME_ANCHOR: LazyLock<(web_async::time::Instant, std::time::Duration)> = LazyLock::new(|| {
let jitter = std::time::Duration::from_millis(rand::rng().random_range(1..69_420));
(web_async::time::Instant::now(), jitter)
});
pub(super) fn now() -> Timestamp {
let (anchor_instant, anchor_duration) = *TIME_ANCHOR;
let instant = web_async::time::Instant::now();
let duration = match instant.checked_duration_since(anchor_instant) {
Some(forward) => anchor_duration + forward,
None => anchor_duration
.checked_sub(anchor_instant.duration_since(instant))
.unwrap_or(std::time::Duration::ZERO),
};
Timestamp::from_millis(duration.as_millis() as u64).expect("clock is somehow past the year 2300")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_secs() {
let time = Timestamp::from_secs(5).unwrap();
assert_eq!(time.scale(), Timescale::SECOND);
assert_eq!(time.as_secs(), 5);
assert_eq!(time.as_millis(), 5000);
assert_eq!(time.as_micros(), 5_000_000);
assert_eq!(time.as_nanos(), 5_000_000_000);
}
#[test]
fn test_from_millis() {
let time = Timestamp::from_millis(5000).unwrap();
assert_eq!(time.scale(), Timescale::MILLI);
assert_eq!(time.as_secs(), 5);
assert_eq!(time.as_millis(), 5000);
}
#[test]
fn test_from_micros() {
let time = Timestamp::from_micros(5_000_000).unwrap();
assert_eq!(time.scale(), Timescale::MICRO);
assert_eq!(time.as_secs(), 5);
assert_eq!(time.as_micros(), 5_000_000);
}
#[test]
fn test_from_nanos() {
let time = Timestamp::from_nanos(5_000_000_000).unwrap();
assert_eq!(time.scale(), Timescale::NANO);
assert_eq!(time.as_secs(), 5);
assert_eq!(time.as_nanos(), 5_000_000_000);
}
#[test]
fn test_timescale_new_rejects_zero_and_overflow() {
assert!(Timescale::new(0).is_err());
assert!(Timescale::new(1).is_ok());
assert_eq!(Timescale::new(1).unwrap(), Timescale::SECOND);
assert_eq!(Timescale::new(1_000).unwrap(), Timescale::MILLI);
assert!(Timescale::new(1u64 << 62).is_err());
assert!(Timescale::new((1u64 << 62) - 1).is_ok());
}
#[test]
fn test_convert_to_finer() {
let time_ms = Timestamp::from_millis(5000).unwrap();
let time_us = time_ms.convert(Timescale::MICRO).unwrap();
assert_eq!(time_us.scale(), Timescale::MICRO);
assert_eq!(time_us.as_micros(), 5_000_000);
}
#[test]
fn test_convert_to_coarser() {
let time_ms = Timestamp::from_millis(5000).unwrap();
let time_s = time_ms.convert(Timescale::SECOND).unwrap();
assert_eq!(time_s.scale(), Timescale::SECOND);
assert_eq!(time_s.as_secs(), 5);
}
#[test]
fn test_convert_precision_loss() {
let time_ms = Timestamp::from_millis(1234).unwrap();
let time_s = time_ms.convert(Timescale::SECOND).unwrap();
assert_eq!(time_s.as_secs(), 1);
}
#[test]
fn test_convert_roundtrip() {
let original = Timestamp::from_millis(5000).unwrap();
let as_micros = original.convert(Timescale::MICRO).unwrap();
let back = as_micros.convert(Timescale::MILLI).unwrap();
assert_eq!(original.value(), back.value());
assert_eq!(original.scale(), back.scale());
}
#[test]
fn test_convert_same_scale() {
let time = Timestamp::from_millis(5000).unwrap();
let converted = time.convert(Timescale::MILLI).unwrap();
assert_eq!(time, converted);
}
#[test]
fn test_add_same_scale() {
let a = Timestamp::from_millis(1000).unwrap();
let b = Timestamp::from_millis(2000).unwrap();
let c = a.checked_add(b).unwrap();
assert_eq!(c.as_millis(), 3000);
assert_eq!(c.scale(), Timescale::MILLI);
}
#[test]
fn test_add_mismatched_scale() {
let a = Timestamp::from_millis(1000).unwrap();
let b = Timestamp::from_micros(1000).unwrap();
assert!(a.checked_add(b).is_err());
}
#[test]
fn test_new_const_matches_fallible() {
const C: Timestamp = Timestamp::new_const(42, Timescale::MICRO);
assert_eq!(C, Timestamp::new(42, Timescale::MICRO).unwrap());
}
#[test]
fn test_zero_is_scale_aware() {
assert!(Timestamp::ZERO.is_zero());
let zero_ms = Timestamp::from_millis(0).unwrap();
assert!(zero_ms.is_zero());
assert_ne!(Timestamp::ZERO, zero_ms);
assert_ne!(Timestamp::ZERO.cmp(&zero_ms), std::cmp::Ordering::Equal);
}
#[test]
fn test_sub_underflow() {
let a = Timestamp::from_millis(1000).unwrap();
let b = Timestamp::from_millis(2000).unwrap();
assert!(a.checked_sub(b).is_err());
}
#[test]
fn test_max_same_scale() {
let a = Timestamp::from_secs(5).unwrap();
let b = Timestamp::from_secs(10).unwrap();
assert_eq!(a.max(b), b);
assert_eq!(b.max(a), b);
}
#[test]
fn test_max_cross_scale() {
let a = Timestamp::from_millis(1).unwrap();
let b = Timestamp::from_secs(1).unwrap();
assert_eq!(a.max(b), b);
}
#[test]
fn test_ordering_same_scale() {
let a = Timestamp::from_secs(1).unwrap();
let b = Timestamp::from_secs(2).unwrap();
assert!(a < b);
assert!(b > a);
assert_eq!(a, a);
}
#[test]
fn test_ordering_across_known_scales() {
let one_sec = Timestamp::from_secs(1).unwrap();
let two_ms = Timestamp::from_millis(2).unwrap();
assert!(one_sec > two_ms);
assert!(two_ms < one_sec);
let one_sec_b = Timestamp::from_millis(1000).unwrap();
assert_ne!(one_sec.cmp(&one_sec_b), std::cmp::Ordering::Equal);
assert_ne!(one_sec, one_sec_b);
assert_eq!(one_sec.cmp(&one_sec), std::cmp::Ordering::Equal);
let mut items = [
Timestamp::from_secs(2).unwrap(),
Timestamp::from_millis(500).unwrap(),
Timestamp::from_micros(1_500_000).unwrap(),
];
items.sort();
assert_eq!(items[0], Timestamp::from_millis(500).unwrap());
assert_eq!(items[1], Timestamp::from_micros(1_500_000).unwrap());
assert_eq!(items[2], Timestamp::from_secs(2).unwrap());
}
#[test]
fn test_duration_conversion() {
let duration = std::time::Duration::from_secs(5);
let time: Timestamp = duration.try_into().unwrap();
assert_eq!(time.scale(), Timescale::NANO);
assert_eq!(time.as_secs(), 5);
let duration_back: std::time::Duration = time.into();
assert_eq!(duration_back.as_secs(), 5);
}
#[test]
fn test_debug_format_units() {
let t = Timestamp::from_millis(100_000).unwrap();
assert_eq!(format!("{:?}", t), "100s");
let t = Timestamp::from_millis(100).unwrap();
assert_eq!(format!("{:?}", t), "100ms");
let t = Timestamp::from_micros(1500).unwrap();
assert_eq!(format!("{:?}", t), "1500µs");
let t = Timestamp::from_micros(1000).unwrap();
assert_eq!(format!("{:?}", t), "1ms");
}
#[test]
fn test_new() {
let t = Timestamp::new(5000, Timescale::MILLI).unwrap();
assert_eq!(t.value(), 5000);
assert_eq!(t.scale(), Timescale::MILLI);
assert_eq!(t.as_millis(), 5000);
}
#[test]
fn test_custom_scale_convert() {
let scale_60 = Timescale::new(60).unwrap();
let t = Timestamp::new(120, scale_60)
.unwrap()
.convert(Timescale::MILLI)
.unwrap();
assert_eq!(t.scale(), Timescale::MILLI);
assert_eq!(t.as_millis(), 2000);
}
}