diff --git a/src/protocol/clock.rs b/src/protocol/clock/mod.rs similarity index 99% rename from src/protocol/clock.rs rename to src/protocol/clock/mod.rs index a72f3699e..4e15950e6 100644 --- a/src/protocol/clock.rs +++ b/src/protocol/clock/mod.rs @@ -1,5 +1,5 @@ use std::num::IntErrorKind; -pub use std::time::Duration; +use std::time::Duration; pub type DurationSinceUnixEpoch = Duration; @@ -240,9 +240,11 @@ mod stopped_clock { #[test] fn it_should_get_app_start_time() { - const TIME_AT_WRITING_THIS_TEST: Duration = Duration::new(1662983731, 000022312); + const TIME_AT_WRITING_THIS_TEST: Duration = Duration::new(1662983731, 22312); assert!(get_app_start_time() > TIME_AT_WRITING_THIS_TEST); } } } } + +pub mod time_extent; diff --git a/src/protocol/clock/time_extent.rs b/src/protocol/clock/time_extent.rs new file mode 100644 index 000000000..d0713645b --- /dev/null +++ b/src/protocol/clock/time_extent.rs @@ -0,0 +1,554 @@ +use std::num::{IntErrorKind, TryFromIntError}; +use std::time::Duration; + +use super::{ClockType, StoppedClock, TimeNow, WorkingClock}; + +pub trait Extent: Sized + Default { + type Base; + type Multiplier; + type Product; + + fn new(unit: &Self::Base, count: &Self::Multiplier) -> Self; + + fn increase(&self, add: Self::Multiplier) -> Result; + fn decrease(&self, sub: Self::Multiplier) -> Result; + + fn total(&self) -> Option>; + fn total_next(&self) -> Option>; +} + +pub type TimeExtentBase = Duration; +pub type TimeExtentMultiplier = u64; +pub type TimeExtentProduct = TimeExtentBase; + +#[derive(Debug, Default, Hash, PartialEq, Eq)] +pub struct TimeExtent { + pub increment: TimeExtentBase, + pub amount: TimeExtentMultiplier, +} + +pub const ZERO: TimeExtent = TimeExtent { + increment: TimeExtentBase::ZERO, + amount: TimeExtentMultiplier::MIN, +}; +pub const MAX: TimeExtent = TimeExtent { + increment: TimeExtentBase::MAX, + amount: TimeExtentMultiplier::MAX, +}; + +impl TimeExtent { + pub const fn from_sec(seconds: u64, amount: &TimeExtentMultiplier) -> Self { + Self { + increment: TimeExtentBase::from_secs(seconds), + amount: *amount, + } + } +} + +fn checked_duration_from_nanos(time: u128) -> Result { + const NANOS_PER_SEC: u32 = 1_000_000_000; + + let secs = time.div_euclid(NANOS_PER_SEC as u128); + let nanos = time.rem_euclid(NANOS_PER_SEC as u128); + + assert!(nanos < NANOS_PER_SEC as u128); + + match u64::try_from(secs) { + Err(error) => Err(error), + Ok(secs) => Ok(Duration::new(secs, nanos.try_into().unwrap())), + } +} + +impl Extent for TimeExtent { + type Base = TimeExtentBase; + type Multiplier = TimeExtentMultiplier; + type Product = TimeExtentProduct; + + fn new(increment: &Self::Base, amount: &Self::Multiplier) -> Self { + Self { + increment: *increment, + amount: *amount, + } + } + + fn increase(&self, add: Self::Multiplier) -> Result { + match self.amount.checked_add(add) { + None => Err(IntErrorKind::PosOverflow), + Some(amount) => Ok(Self { + increment: self.increment, + amount, + }), + } + } + + fn decrease(&self, sub: Self::Multiplier) -> Result { + match self.amount.checked_sub(sub) { + None => Err(IntErrorKind::NegOverflow), + Some(amount) => Ok(Self { + increment: self.increment, + amount, + }), + } + } + + fn total(&self) -> Option> { + self.increment + .as_nanos() + .checked_mul(self.amount as u128) + .map(checked_duration_from_nanos) + } + + fn total_next(&self) -> Option> { + self.increment + .as_nanos() + .checked_mul((self.amount as u128) + 1) + .map(checked_duration_from_nanos) + } +} + +pub trait MakeTimeExtent: Sized +where + Clock: TimeNow, +{ + fn now(increment: &TimeExtentBase) -> Option> { + Clock::now() + .as_nanos() + .checked_div((*increment).as_nanos()) + .map(|amount| match TimeExtentMultiplier::try_from(amount) { + Err(error) => Err(error), + Ok(amount) => Ok(TimeExtent::new(increment, &amount)), + }) + } + + fn now_after(increment: &TimeExtentBase, add_time: &Duration) -> Option> { + match Clock::add(add_time) { + None => None, + Some(time) => { + time.as_nanos() + .checked_div(increment.as_nanos()) + .map(|amount| match TimeExtentMultiplier::try_from(amount) { + Err(error) => Err(error), + Ok(amount) => Ok(TimeExtent::new(increment, &amount)), + }) + } + } + } + fn now_before(increment: &TimeExtentBase, sub_time: &Duration) -> Option> { + match Clock::sub(sub_time) { + None => None, + Some(time) => { + time.as_nanos() + .checked_div(increment.as_nanos()) + .map(|amount| match TimeExtentMultiplier::try_from(amount) { + Err(error) => Err(error), + Ok(amount) => Ok(TimeExtent::new(increment, &amount)), + }) + } + } + } +} + +#[derive(Debug)] +pub struct TimeExtentMaker {} + +pub type WorkingTimeExtentMaker = TimeExtentMaker<{ ClockType::WorkingClock as usize }>; +pub type StoppedTimeExtentMaker = TimeExtentMaker<{ ClockType::StoppedClock as usize }>; + +impl MakeTimeExtent for WorkingTimeExtentMaker {} +impl MakeTimeExtent for StoppedTimeExtentMaker {} + +#[cfg(not(test))] +pub type DefaultTimeExtentMaker = WorkingTimeExtentMaker; + +#[cfg(test)] +pub type DefaultTimeExtentMaker = StoppedTimeExtentMaker; + +#[cfg(test)] +mod test { + + use crate::protocol::clock::time_extent::{ + checked_duration_from_nanos, DefaultTimeExtentMaker, Extent, MakeTimeExtent, TimeExtent, TimeExtentBase, + TimeExtentMultiplier, TimeExtentProduct, MAX, ZERO, + }; + use crate::protocol::clock::{DefaultClock, DurationSinceUnixEpoch, StoppedTime}; + + const TIME_EXTENT_VAL: TimeExtent = TimeExtent::from_sec(2, &239812388723); + + mod fn_checked_duration_from_nanos { + use std::time::Duration; + + use super::*; + + const NANOS_PER_SEC: u32 = 1_000_000_000; + + #[test] + fn it_should_give_zero_for_zero_input() { + assert_eq!(checked_duration_from_nanos(0).unwrap(), Duration::ZERO); + } + + #[test] + fn it_should_be_the_same_as_duration_implementation_for_u64_numbers() { + assert_eq!( + checked_duration_from_nanos(1232143214343432).unwrap(), + Duration::from_nanos(1232143214343432) + ); + assert_eq!( + checked_duration_from_nanos(u64::MAX as u128).unwrap(), + Duration::from_nanos(u64::MAX) + ); + } + + #[test] + fn it_should_work_for_some_numbers_larger_than_u64() { + assert_eq!( + checked_duration_from_nanos(TIME_EXTENT_VAL.amount as u128 * NANOS_PER_SEC as u128).unwrap(), + Duration::from_secs(TIME_EXTENT_VAL.amount) + ); + } + + #[test] + fn it_should_fail_for_numbers_that_are_too_large() { + assert_eq!( + checked_duration_from_nanos(u128::MAX).unwrap_err(), + u64::try_from(u128::MAX).unwrap_err() + ); + } + } + + mod time_extent { + use super::*; + + mod fn_default { + + use super::*; + + #[test] + fn it_should_default_initialize_to_zero() { + assert_eq!(TimeExtent::default(), ZERO); + } + } + + mod fn_from_sec { + use super::*; + + #[test] + fn it_should_make_empty_for_zero() { + assert_eq!(TimeExtent::from_sec(u64::MIN, &TimeExtentMultiplier::MIN), ZERO); + } + #[test] + fn it_should_make_from_seconds() { + assert_eq!( + TimeExtent::from_sec(TIME_EXTENT_VAL.increment.as_secs(), &TIME_EXTENT_VAL.amount), + TIME_EXTENT_VAL + ); + } + } + + mod fn_new { + use super::*; + + #[test] + fn it_should_make_empty_for_zero() { + assert_eq!(TimeExtent::new(&TimeExtentBase::ZERO, &TimeExtentMultiplier::MIN), ZERO); + } + + #[test] + fn it_should_make_new() { + assert_eq!( + TimeExtent::new(&TimeExtentBase::from_millis(2), &TIME_EXTENT_VAL.amount), + TimeExtent { + increment: TimeExtentBase::from_millis(2), + amount: TIME_EXTENT_VAL.amount + } + ); + } + } + + mod fn_increase { + use std::num::IntErrorKind; + + use super::*; + + #[test] + fn it_should_not_increase_for_zero() { + assert_eq!(ZERO.increase(0).unwrap(), ZERO); + } + + #[test] + fn it_should_increase() { + assert_eq!( + TIME_EXTENT_VAL.increase(50).unwrap(), + TimeExtent { + increment: TIME_EXTENT_VAL.increment, + amount: TIME_EXTENT_VAL.amount + 50, + } + ); + } + + #[test] + fn it_should_fail_when_attempting_to_increase_beyond_bounds() { + assert_eq!(TIME_EXTENT_VAL.increase(u64::MAX), Err(IntErrorKind::PosOverflow)); + } + } + + mod fn_decrease { + use std::num::IntErrorKind; + + use super::*; + + #[test] + fn it_should_not_decrease_for_zero() { + assert_eq!(ZERO.decrease(0).unwrap(), ZERO); + } + + #[test] + fn it_should_decrease() { + assert_eq!( + TIME_EXTENT_VAL.decrease(50).unwrap(), + TimeExtent { + increment: TIME_EXTENT_VAL.increment, + amount: TIME_EXTENT_VAL.amount - 50, + } + ); + } + + #[test] + fn it_should_fail_when_attempting_to_decrease_beyond_bounds() { + assert_eq!(TIME_EXTENT_VAL.decrease(u64::MAX), Err(IntErrorKind::NegOverflow)); + } + } + + mod fn_total { + use super::*; + + #[test] + fn it_should_be_zero_for_zero() { + assert_eq!(ZERO.total().unwrap().unwrap(), TimeExtentProduct::ZERO); + } + + #[test] + fn it_should_give_a_total() { + assert_eq!( + TIME_EXTENT_VAL.total().unwrap().unwrap(), + TimeExtentProduct::from_secs(TIME_EXTENT_VAL.increment.as_secs() * TIME_EXTENT_VAL.amount) + ); + + assert_eq!( + TimeExtent::new(&TimeExtentBase::from_millis(2), &(TIME_EXTENT_VAL.amount * 1000)) + .total() + .unwrap() + .unwrap(), + TimeExtentProduct::from_secs(TIME_EXTENT_VAL.increment.as_secs() * TIME_EXTENT_VAL.amount) + ); + + assert_eq!( + TimeExtent::new(&TimeExtentBase::from_secs(1), &(u64::MAX)) + .total() + .unwrap() + .unwrap(), + TimeExtentProduct::from_secs(u64::MAX) + ); + } + + #[test] + fn it_should_fail_when_too_large() { + assert_eq!(MAX.total(), None); + } + + #[test] + fn it_should_fail_when_product_is_too_large() { + let time_extent = TimeExtent { + increment: MAX.increment, + amount: 2, + }; + assert_eq!( + time_extent.total().unwrap().unwrap_err(), + u64::try_from(u128::MAX).unwrap_err() + ); + } + } + + mod fn_total_next { + use super::*; + + #[test] + fn it_should_be_zero_for_zero() { + assert_eq!(ZERO.total_next().unwrap().unwrap(), TimeExtentProduct::ZERO); + } + + #[test] + fn it_should_give_a_total() { + assert_eq!( + TIME_EXTENT_VAL.total_next().unwrap().unwrap(), + TimeExtentProduct::from_secs(TIME_EXTENT_VAL.increment.as_secs() * (TIME_EXTENT_VAL.amount + 1)) + ); + + assert_eq!( + TimeExtent::new(&TimeExtentBase::from_millis(2), &(TIME_EXTENT_VAL.amount * 1000)) + .total_next() + .unwrap() + .unwrap(), + TimeExtentProduct::new( + TIME_EXTENT_VAL.increment.as_secs() * (TIME_EXTENT_VAL.amount), + TimeExtentBase::from_millis(2).as_nanos().try_into().unwrap() + ) + ); + + assert_eq!( + TimeExtent::new(&TimeExtentBase::from_secs(1), &(u64::MAX - 1)) + .total_next() + .unwrap() + .unwrap(), + TimeExtentProduct::from_secs(u64::MAX) + ); + } + + #[test] + fn it_should_fail_when_too_large() { + assert_eq!(MAX.total_next(), None); + } + + #[test] + fn it_should_fail_when_product_is_too_large() { + let time_extent = TimeExtent { + increment: MAX.increment, + amount: 2, + }; + assert_eq!( + time_extent.total_next().unwrap().unwrap_err(), + u64::try_from(u128::MAX).unwrap_err() + ); + } + } + } + + mod make_time_extent { + use super::*; + + mod fn_now { + use super::*; + + #[test] + fn it_should_give_a_time_extent() { + assert_eq!( + DefaultTimeExtentMaker::now(&TIME_EXTENT_VAL.increment).unwrap().unwrap(), + TimeExtent { + increment: TIME_EXTENT_VAL.increment, + amount: 0 + } + ); + + DefaultClock::local_set(&DurationSinceUnixEpoch::from_secs(TIME_EXTENT_VAL.amount * 2)); + + assert_eq!( + DefaultTimeExtentMaker::now(&TIME_EXTENT_VAL.increment).unwrap().unwrap(), + TIME_EXTENT_VAL + ); + } + + #[test] + fn it_should_fail_for_zero() { + assert_eq!(DefaultTimeExtentMaker::now(&TimeExtentBase::ZERO), None); + } + + #[test] + fn it_should_fail_if_amount_exceeds_bounds() { + DefaultClock::local_set(&DurationSinceUnixEpoch::MAX); + assert_eq!( + DefaultTimeExtentMaker::now(&TimeExtentBase::from_millis(1)) + .unwrap() + .unwrap_err(), + u64::try_from(u128::MAX).unwrap_err() + ); + } + } + + mod fn_now_after { + use std::time::Duration; + + use super::*; + + #[test] + fn it_should_give_a_time_extent() { + assert_eq!( + DefaultTimeExtentMaker::now_after( + &TIME_EXTENT_VAL.increment, + &Duration::from_secs(TIME_EXTENT_VAL.amount * 2) + ) + .unwrap() + .unwrap(), + TIME_EXTENT_VAL + ); + } + + #[test] + fn it_should_fail_for_zero() { + assert_eq!( + DefaultTimeExtentMaker::now_after(&TimeExtentBase::ZERO, &Duration::ZERO), + None + ); + + DefaultClock::local_set(&DurationSinceUnixEpoch::MAX); + assert_eq!(DefaultTimeExtentMaker::now_after(&TimeExtentBase::ZERO, &Duration::MAX), None); + } + + #[test] + fn it_should_fail_if_amount_exceeds_bounds() { + DefaultClock::local_set(&DurationSinceUnixEpoch::MAX); + assert_eq!( + DefaultTimeExtentMaker::now_after(&TimeExtentBase::from_millis(1), &Duration::ZERO) + .unwrap() + .unwrap_err(), + u64::try_from(u128::MAX).unwrap_err() + ); + } + } + mod fn_now_before { + use std::time::Duration; + + use super::*; + + #[test] + fn it_should_give_a_time_extent() { + DefaultClock::local_set(&DurationSinceUnixEpoch::MAX); + + assert_eq!( + DefaultTimeExtentMaker::now_before( + &TimeExtentBase::from_secs(u32::MAX as u64), + &Duration::from_secs(u32::MAX as u64) + ) + .unwrap() + .unwrap(), + TimeExtent { + increment: TimeExtentBase::from_secs(u32::MAX as u64), + amount: 4294967296 + } + ); + } + + #[test] + fn it_should_fail_for_zero() { + assert_eq!( + DefaultTimeExtentMaker::now_before(&TimeExtentBase::ZERO, &Duration::ZERO), + None + ); + + assert_eq!( + DefaultTimeExtentMaker::now_before(&TimeExtentBase::ZERO, &Duration::MAX), + None + ); + } + + #[test] + fn it_should_fail_if_amount_exceeds_bounds() { + DefaultClock::local_set(&DurationSinceUnixEpoch::MAX); + assert_eq!( + DefaultTimeExtentMaker::now_before(&TimeExtentBase::from_millis(1), &Duration::ZERO) + .unwrap() + .unwrap_err(), + u64::try_from(u128::MAX).unwrap_err() + ); + } + } + } +}