use std::{
fmt,
ops::{Add, Sub},
str::FromStr,
time::Duration,
};
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
#[derive(
BorshSerialize,
BorshDeserialize,
Clone,
Copy,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct TimestampMs(u64);
impl TimestampMs {
pub const ZERO: Self = Self(0);
pub const MAX: Self = Self(u64::MAX);
pub const fn new(millis: u64) -> Self {
Self(millis)
}
pub const fn as_millis(self) -> u64 {
self.0
}
pub const fn to_le_bytes(self) -> [u8; 8] {
self.0.to_le_bytes()
}
pub const fn from_le_bytes(bytes: [u8; 8]) -> Self {
Self(u64::from_le_bytes(bytes))
}
pub const fn saturating_add(self, rhs: DurationMs) -> Self {
Self(self.0.saturating_add(rhs.0))
}
pub const fn saturating_sub(self, rhs: DurationMs) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
pub const fn checked_sub(self, rhs: DurationMs) -> Option<Self> {
match self.0.checked_sub(rhs.0) {
Some(v) => Some(Self(v)),
None => None,
}
}
pub const fn checked_add(self, rhs: DurationMs) -> Option<Self> {
match self.0.checked_add(rhs.0) {
Some(v) => Some(Self(v)),
None => None,
}
}
pub const fn saturating_duration_since(self, earlier: Self) -> DurationMs {
DurationMs(self.0.saturating_sub(earlier.0))
}
}
impl From<u64> for TimestampMs {
fn from(millis: u64) -> Self {
Self(millis)
}
}
impl From<TimestampMs> for u64 {
fn from(ts: TimestampMs) -> Self {
ts.0
}
}
impl fmt::Display for TimestampMs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for TimestampMs {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<u64>().map(Self)
}
}
impl Add<DurationMs> for TimestampMs {
type Output = TimestampMs;
fn add(self, rhs: DurationMs) -> Self::Output {
TimestampMs(self.0.saturating_add(rhs.0))
}
}
impl Sub<DurationMs> for TimestampMs {
type Output = TimestampMs;
fn sub(self, rhs: DurationMs) -> Self::Output {
TimestampMs(self.0.saturating_sub(rhs.0))
}
}
impl Sub<TimestampMs> for TimestampMs {
type Output = DurationMs;
fn sub(self, rhs: TimestampMs) -> Self::Output {
DurationMs(self.0.saturating_sub(rhs.0))
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
Clone,
Copy,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct DurationMs(u64);
impl DurationMs {
pub const ZERO: Self = Self(0);
pub const MAX: Self = Self(u64::MAX);
pub const fn new(millis: u64) -> Self {
Self(millis)
}
pub const fn from_secs(secs: u64) -> Self {
Self(secs.saturating_mul(1000))
}
pub const fn as_millis(self) -> u64 {
self.0
}
pub const fn saturating_add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0))
}
pub const fn saturating_sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
}
impl From<u64> for DurationMs {
fn from(millis: u64) -> Self {
Self(millis)
}
}
impl From<DurationMs> for u64 {
fn from(span: DurationMs) -> Self {
span.0
}
}
impl From<DurationMs> for Duration {
fn from(span: DurationMs) -> Self {
Duration::from_millis(span.0)
}
}
impl fmt::Display for DurationMs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for DurationMs {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<u64>().map(Self)
}
}
impl Add for DurationMs {
type Output = DurationMs;
fn add(self, rhs: Self) -> Self::Output {
DurationMs(self.0.saturating_add(rhs.0))
}
}
impl Sub for DurationMs {
type Output = DurationMs;
fn sub(self, rhs: Self) -> Self::Output {
DurationMs(self.0.saturating_sub(rhs.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instant_plus_duration_is_instant() {
let t = TimestampMs::new(1_000);
assert_eq!(
t.saturating_add(DurationMs::new(500)),
TimestampMs::new(1_500)
);
assert_eq!(t + DurationMs::new(500), TimestampMs::new(1_500));
}
#[test]
fn instant_minus_instant_is_duration() {
let a = TimestampMs::new(1_500);
let b = TimestampMs::new(1_000);
assert_eq!(a.saturating_duration_since(b), DurationMs::new(500));
assert_eq!(a - b, DurationMs::new(500));
}
#[test]
fn saturation_clamps() {
assert_eq!(
TimestampMs::ZERO.saturating_sub(DurationMs::new(1)),
TimestampMs::ZERO
);
assert_eq!(
TimestampMs::MAX.saturating_add(DurationMs::new(1)),
TimestampMs::MAX
);
assert_eq!(
TimestampMs::new(1).saturating_duration_since(TimestampMs::new(5)),
DurationMs::ZERO
);
}
#[test]
fn serde_is_transparent() {
let t = TimestampMs::new(42);
assert_eq!(serde_json::to_string(&t).unwrap(), "42");
let d: DurationMs = serde_json::from_str("7").unwrap();
assert_eq!(d, DurationMs::new(7));
}
#[test]
fn borsh_roundtrips() {
let t = TimestampMs::new(123);
let bytes = borsh::to_vec(&t).unwrap();
assert_eq!(bytes, 123u64.to_le_bytes());
assert_eq!(borsh::from_slice::<TimestampMs>(&bytes).unwrap(), t);
}
#[test]
fn bcs_matches_u64() {
for raw in [0u64, 1, 42, 0xdead_beef, u64::MAX] {
assert_eq!(
bcs::to_bytes(&TimestampMs::new(raw)).unwrap(),
bcs::to_bytes(&raw).unwrap()
);
assert_eq!(
bcs::to_bytes(&DurationMs::new(raw)).unwrap(),
bcs::to_bytes(&raw).unwrap()
);
assert_eq!(
bcs::from_bytes::<TimestampMs>(&bcs::to_bytes(&raw).unwrap()).unwrap(),
TimestampMs::new(raw)
);
}
}
#[test]
fn bincode_matches_u64() {
use bincode::Options;
for raw in [0u64, 1, 42, u64::MAX] {
assert_eq!(
bincode::serialize(&TimestampMs::new(raw)).unwrap(),
bincode::serialize(&raw).unwrap()
);
}
let cfg = bincode::DefaultOptions::new()
.with_big_endian()
.with_fixint_encoding();
assert_eq!(
cfg.serialize(&TimestampMs::new(42)).unwrap(),
cfg.serialize(&42u64).unwrap()
);
}
}