use crate::constants::*;
use crate::error::ClockError;
#[cfg(feature = "clock")]
use chrono::NaiveTime;
use chrono::{DateTime, Duration, NaiveDate, Utc};
use expiration_date::ExpirationDate;
use expiration_date::error::ExpirationDateError;
use positive::constants::DAYS_IN_A_YEAR;
use positive::{Positive, PositiveError};
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(test)]
use positive::pos_or_panic;
#[cfg(feature = "clock")]
#[must_use]
#[inline]
pub fn wall_clock() -> DateTime<Utc> {
Utc::now()
}
#[cfg(feature = "clock")]
#[inline]
pub fn now() -> Result<DateTime<Utc>, ClockError> {
Ok(wall_clock())
}
#[cfg(not(feature = "clock"))]
#[inline]
pub fn now() -> Result<DateTime<Utc>, ClockError> {
Err(ClockError::Unavailable)
}
#[must_use]
#[inline]
pub fn now_or(existing: DateTime<Utc>) -> DateTime<Utc> {
now().unwrap_or(existing)
}
#[inline]
pub fn evaluation_time() -> Result<DateTime<Utc>, ClockError> {
now().or_else(|error| ExpirationDate::get_reference_datetime().ok_or(error))
}
#[inline]
pub fn resolution_base() -> Result<DateTime<Utc>, ClockError> {
ExpirationDate::get_reference_datetime().map_or_else(now, Ok)
}
#[derive(Debug, Clone, Copy)]
#[must_use = "a carrier does nothing until its work is run through it"]
pub struct ReferenceCarrier {
#[cfg(not(feature = "clock"))]
reference: Option<DateTime<Utc>>,
}
#[cfg(not(feature = "clock"))]
struct RestoreReference(Option<DateTime<Utc>>);
#[cfg(not(feature = "clock"))]
impl Drop for RestoreReference {
fn drop(&mut self) {
ExpirationDate::set_reference_datetime(self.0);
}
}
impl ReferenceCarrier {
#[cfg(not(feature = "clock"))]
#[inline]
pub fn capture() -> Self {
Self {
reference: ExpirationDate::get_reference_datetime(),
}
}
#[cfg(feature = "clock")]
#[inline]
pub fn capture() -> Self {
Self {}
}
#[cfg(not(feature = "clock"))]
#[inline]
pub fn run<R>(&self, work: impl FnOnce() -> R) -> R {
let _restore = RestoreReference(ExpirationDate::get_reference_datetime());
ExpirationDate::set_reference_datetime(self.reference);
work()
}
#[cfg(feature = "clock")]
#[inline]
pub fn run<R>(&self, work: impl FnOnce() -> R) -> R {
work()
}
}
#[inline]
pub fn years_to_expiration(expiration: &ExpirationDate) -> Result<Positive, ExpirationDateError> {
match expiration {
ExpirationDate::Days(days) => {
Positive::new(days.to_f64() / DAYS_IN_A_YEAR.to_f64()) .map_err(Into::into)
}
ExpirationDate::DateTime(_) => expiration.get_years_at(evaluation_time()?),
}
}
#[inline]
pub fn days_to_expiration(expiration: &ExpirationDate) -> Result<Positive, ExpirationDateError> {
match expiration {
ExpirationDate::Days(days) => Ok(*days),
ExpirationDate::DateTime(_) => expiration.get_days_at(evaluation_time()?),
}
}
pub fn expiration_date_string(expiration: &ExpirationDate) -> Result<String, ExpirationDateError> {
match expiration {
ExpirationDate::DateTime(datetime) => expiration.get_date_string_at(*datetime),
ExpirationDate::Days(_) => expiration.get_date_string_at(evaluation_time()?),
}
}
#[cfg(feature = "clock")]
#[inline]
fn local_today() -> Result<NaiveDate, ClockError> {
Ok(chrono::Local::now().date_naive())
}
#[cfg(not(feature = "clock"))]
#[inline]
fn local_today() -> Result<NaiveDate, ClockError> {
Err(ClockError::Unavailable)
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub enum TimeFrame {
Microsecond,
Millisecond,
Second,
Minute,
Hour,
Day,
Week,
Month,
Quarter,
Year,
Custom(Positive),
}
impl TimeFrame {
#[must_use]
pub fn periods_per_year(&self) -> Positive {
match self {
TimeFrame::Microsecond => pos_lit(dec!(5896800000000.0000)),
TimeFrame::Millisecond => pos_lit(dec!(5896800000.000)),
TimeFrame::Second => pos_lit(dec!(5896800.000)),
TimeFrame::Minute => pos_lit(dec!(98280.00)),
TimeFrame::Hour => pos_lit(dec!(1638.00)),
TimeFrame::Day => *TRADING_DAYS, TimeFrame::Week => *WEEKS_PER_YEAR, TimeFrame::Month => *MONTHS_PER_YEAR, TimeFrame::Quarter => QUARTERS_PER_YEAR, TimeFrame::Year => Positive::ONE, TimeFrame::Custom(periods) => *periods, }
}
}
impl fmt::Display for TimeFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimeFrame::Microsecond => write!(f, "microsecond"),
TimeFrame::Millisecond => write!(f, "millisecond"),
TimeFrame::Second => write!(f, "second"),
TimeFrame::Minute => write!(f, "minute"),
TimeFrame::Hour => write!(f, "hour"),
TimeFrame::Day => write!(f, "day"),
TimeFrame::Week => write!(f, "week"),
TimeFrame::Month => write!(f, "month"),
TimeFrame::Quarter => write!(f, "quarter"),
TimeFrame::Year => write!(f, "year"),
TimeFrame::Custom(periods) => write!(f, "custom ({periods})"),
}
}
}
fn pos_lit(d: rust_decimal::Decimal) -> Positive {
Positive::new_decimal(d).unwrap_or(Positive::ZERO)
}
#[must_use]
pub fn units_per_year(time_frame: &TimeFrame) -> Positive {
match time_frame {
TimeFrame::Microsecond => pos_lit(dec!(31536000000000.0)), TimeFrame::Millisecond => pos_lit(dec!(31536000000.0)), TimeFrame::Second => pos_lit(dec!(31536000.0)), TimeFrame::Minute => pos_lit(dec!(525600.0)), TimeFrame::Hour => pos_lit(dec!(8760.0)), TimeFrame::Day => pos_lit(dec!(365.0)), TimeFrame::Week => pos_lit(dec!(52.142857142857142857142857143)),
TimeFrame::Month => pos_lit(dec!(12.0)), TimeFrame::Quarter => pos_lit(dec!(4.0)), TimeFrame::Year => Positive::ONE, TimeFrame::Custom(periods) => *periods, }
}
pub fn convert_time_frame(
value: Positive,
from_time_frame: &TimeFrame,
to_time_frame: &TimeFrame,
) -> Result<Positive, PositiveError> {
if from_time_frame == to_time_frame {
return Ok(value);
}
if value.is_zero() {
return Ok(Positive::ZERO);
}
let from_units_per_year = units_per_year(from_time_frame);
let to_units_per_year = units_per_year(to_time_frame);
let conversion_factor = to_units_per_year.checked_div(&from_units_per_year)?;
value.checked_mul(&conversion_factor)
}
pub fn get_tomorrow_formatted() -> Result<String, ExpirationDateError> {
get_x_days_formatted(1)
}
pub fn get_x_days_formatted(days: i64) -> Result<String, ExpirationDateError> {
let today = local_today()?;
let target = Duration::try_days(days)
.and_then(|delta| today.checked_add_signed(delta))
.ok_or_else(|| {
ExpirationDateError::ArithmeticOverflow(format!(
"a date {days} days from today is outside the representable calendar range"
))
})?;
Ok(target.format("%d-%b-%Y").to_string().to_lowercase())
}
pub fn get_x_days_formatted_pos(days: Positive) -> Result<String, ExpirationDateError> {
let ceiling = days
.checked_ceiling()
.ok()
.and_then(|whole| whole.to_i64_checked())
.ok_or_else(|| unrepresentable_day_offset(days))?;
let today = local_today()?;
let target = Duration::try_days(ceiling)
.and_then(|delta| today.checked_add_signed(delta))
.ok_or_else(|| unrepresentable_day_offset(days))?;
Ok(target.format("%d-%b-%Y").to_string().to_lowercase())
}
#[cold]
#[inline(never)]
fn unrepresentable_day_offset(days: Positive) -> ExpirationDateError {
ExpirationDateError::ArithmeticOverflow(format!(
"a date {days} days from today is outside the representable calendar range"
))
}
#[cfg(feature = "clock")]
#[must_use]
pub fn get_today_formatted() -> String {
let today = chrono::Local::now().date_naive();
today.format("%d-%b-%Y").to_string().to_lowercase()
}
#[cfg(feature = "clock")]
const TODAY_CUTOFF: NaiveTime = match NaiveTime::from_hms_opt(18, 30, 0) {
Some(t) => t,
None => unreachable!(), };
#[cfg(feature = "clock")]
#[must_use]
pub fn get_today_or_tomorrow_formatted() -> String {
let cutoff_time = TODAY_CUTOFF;
let now = wall_clock();
let target_date = if now.time() > cutoff_time {
now.date_naive()
.succ_opt()
.unwrap_or_else(|| now.date_naive()) } else {
now.date_naive()
};
target_date.format("%d-%b-%Y").to_string().to_lowercase()
}
#[cfg(test)]
mod tests_timeframe {
use super::*;
use positive::assert_pos_relative_eq;
#[test]
fn test_microsecond_periods() {
let expected =
*TRADING_DAYS * *TRADING_HOURS * *SECONDS_PER_HOUR * *MICROSECONDS_PER_SECOND;
assert_eq!(TimeFrame::Microsecond.periods_per_year(), expected);
}
#[test]
fn test_millisecond_periods() {
let expected = *TRADING_DAYS * *TRADING_HOURS * *SECONDS_PER_HOUR * MILLISECONDS_PER_SECOND;
assert_eq!(TimeFrame::Millisecond.periods_per_year(), expected);
}
#[test]
fn test_second_periods() {
let expected = *TRADING_DAYS * *TRADING_HOURS * *SECONDS_PER_HOUR;
assert_eq!(TimeFrame::Second.periods_per_year(), expected);
}
#[test]
fn test_minute_periods() {
let expected = *TRADING_DAYS * *TRADING_HOURS * MINUTES_PER_HOUR;
assert_eq!(TimeFrame::Minute.periods_per_year(), expected);
}
#[test]
fn test_hour_periods() {
let expected = *TRADING_DAYS * *TRADING_HOURS;
assert_eq!(TimeFrame::Hour.periods_per_year(), expected);
}
#[test]
fn test_day_periods() {
assert_eq!(TimeFrame::Day.periods_per_year(), *TRADING_DAYS);
}
#[test]
fn test_periods_per_year_matches_products() {
let hours = *TRADING_DAYS * *TRADING_HOURS;
let seconds = hours * *SECONDS_PER_HOUR;
let cases = [
(TimeFrame::Microsecond, seconds * *MICROSECONDS_PER_SECOND),
(TimeFrame::Millisecond, seconds * MILLISECONDS_PER_SECOND),
(TimeFrame::Second, seconds),
(TimeFrame::Minute, hours * MINUTES_PER_HOUR),
(TimeFrame::Hour, hours),
];
for (frame, product) in cases {
assert_eq!(
frame.periods_per_year().to_dec().to_string(),
product.to_dec().to_string(),
"{frame}"
);
}
}
#[test]
fn test_units_per_year_week_is_365_over_7() {
assert_eq!(
units_per_year(&TimeFrame::Week).to_dec().to_string(),
(dec!(365.0) / dec!(7.0)).to_string()
);
}
#[test]
fn test_week_periods() {
assert_eq!(TimeFrame::Week.periods_per_year(), 52.0);
}
#[test]
fn test_month_periods() {
assert_eq!(TimeFrame::Month.periods_per_year(), 12.0);
}
#[test]
fn test_quarter_periods() {
assert_eq!(TimeFrame::Quarter.periods_per_year(), 4.0);
}
#[test]
fn test_year_periods() {
assert_eq!(TimeFrame::Year.periods_per_year(), 1.0);
}
#[test]
fn test_custom_periods() {
let custom_periods = pos_or_panic!(123.45);
assert_eq!(
TimeFrame::Custom(custom_periods).periods_per_year(),
custom_periods
);
}
#[test]
fn test_relative_period_relationships() {
assert!(
TimeFrame::Microsecond.periods_per_year() > TimeFrame::Millisecond.periods_per_year()
);
assert!(TimeFrame::Millisecond.periods_per_year() > TimeFrame::Second.periods_per_year());
assert!(TimeFrame::Second.periods_per_year() > TimeFrame::Minute.periods_per_year());
assert!(TimeFrame::Minute.periods_per_year() > TimeFrame::Hour.periods_per_year());
assert!(TimeFrame::Hour.periods_per_year() > TimeFrame::Day.periods_per_year());
assert!(TimeFrame::Day.periods_per_year() > TimeFrame::Week.periods_per_year());
assert!(TimeFrame::Week.periods_per_year() > TimeFrame::Month.periods_per_year());
assert!(TimeFrame::Month.periods_per_year() > TimeFrame::Quarter.periods_per_year());
assert!(TimeFrame::Quarter.periods_per_year() > TimeFrame::Year.periods_per_year());
}
#[test]
fn test_specific_conversion_ratios() {
assert_pos_relative_eq!(
TimeFrame::Hour.periods_per_year() / TimeFrame::Day.periods_per_year(),
*TRADING_HOURS,
pos_or_panic!(1e-10)
);
assert_pos_relative_eq!(
TimeFrame::Minute.periods_per_year() / TimeFrame::Hour.periods_per_year(),
MINUTES_PER_HOUR,
pos_or_panic!(1e-10)
);
assert_pos_relative_eq!(
TimeFrame::Second.periods_per_year() / TimeFrame::Minute.periods_per_year(),
MINUTES_PER_HOUR,
pos_or_panic!(1e-10)
);
}
#[test]
fn test_trading_days_relationship() {
assert_pos_relative_eq!(
TimeFrame::Day.periods_per_year(),
*TRADING_DAYS,
pos_or_panic!(1e-10)
);
assert_pos_relative_eq!(
TimeFrame::Hour.periods_per_year() / *TRADING_HOURS,
*TRADING_DAYS,
pos_or_panic!(1e-10)
);
}
#[test]
fn test_custom_edge_cases() {
assert_eq!(TimeFrame::Custom(Positive::ZERO).periods_per_year(), 0.0);
assert_eq!(
TimeFrame::Custom(Positive::MAX).periods_per_year(),
Positive::MAX
);
}
#[test]
fn test_timeframe_debug() {
assert_eq!(format!("{:?}", TimeFrame::Day), "Day");
assert_eq!(
format!("{:?}", TimeFrame::Custom(pos_or_panic!(1.5))),
"Custom(1.5)"
);
}
#[test]
fn test_timeframe_clone() {
let tf = TimeFrame::Day;
let cloned = tf;
assert_eq!(tf.periods_per_year(), cloned.periods_per_year());
}
#[test]
fn test_timeframe_copy() {
let tf = TimeFrame::Day;
let copied = tf;
assert_eq!(tf.periods_per_year(), copied.periods_per_year());
}
}
#[cfg(test)]
mod tests_timeframe_convert {
use super::*;
use positive::assert_pos_relative_eq;
#[test]
fn test_convert_seconds_to_minutes() {
let result =
convert_time_frame(pos_or_panic!(60.0), &TimeFrame::Second, &TimeFrame::Minute)
.unwrap();
assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
}
#[test]
fn test_convert_hours_to_days() {
let result =
convert_time_frame(pos_or_panic!(12.0), &TimeFrame::Hour, &TimeFrame::Day).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(0.5), pos_or_panic!(1e-10));
}
#[test]
fn test_convert_days_to_weeks() {
let result =
convert_time_frame(pos_or_panic!(7.0), &TimeFrame::Day, &TimeFrame::Week).unwrap();
assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
}
#[test]
fn test_convert_weeks_to_days() {
let result = convert_time_frame(Positive::TWO, &TimeFrame::Week, &TimeFrame::Day).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(14.0), pos_or_panic!(1e-10));
}
#[test]
fn test_convert_months_to_quarters() {
let result =
convert_time_frame(pos_or_panic!(3.0), &TimeFrame::Month, &TimeFrame::Quarter).unwrap();
assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
}
#[test]
fn test_convert_minutes_to_hours() {
let result =
convert_time_frame(pos_or_panic!(120.0), &TimeFrame::Minute, &TimeFrame::Hour).unwrap();
assert_pos_relative_eq!(result, Positive::TWO, pos_or_panic!(1e-10));
}
#[test]
fn test_convert_custom_to_day() {
let result = convert_time_frame(
pos_or_panic!(10.0),
&TimeFrame::Custom(pos_or_panic!(365.0)),
&TimeFrame::Day,
)
.unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(10.0), pos_or_panic!(1e-10));
}
#[test]
fn test_convert_day_to_custom() {
let result = convert_time_frame(
Positive::TWO,
&TimeFrame::Day,
&TimeFrame::Custom(pos_or_panic!(365.0)),
)
.unwrap();
assert_pos_relative_eq!(result, Positive::TWO, pos_or_panic!(1e-10));
}
#[test]
fn test_convert_same_timeframe() {
let result =
convert_time_frame(pos_or_panic!(42.0), &TimeFrame::Hour, &TimeFrame::Hour).unwrap();
assert_pos_relative_eq!(result, pos_or_panic!(42.0), pos_or_panic!(1e-10));
}
#[test]
fn test_convert_weeks_to_months() {
let result =
convert_time_frame(pos_or_panic!(4.0), &TimeFrame::Week, &TimeFrame::Month).unwrap();
assert_pos_relative_eq!(
result,
pos_or_panic!(0.920_547_945_255_920_4),
pos_or_panic!(1e-10)
);
}
#[test]
fn test_convert_milliseconds_to_seconds() {
let result = convert_time_frame(
pos_or_panic!(1000.0),
&TimeFrame::Millisecond,
&TimeFrame::Second,
)
.unwrap();
assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
}
#[test]
fn test_zero() {
let result =
convert_time_frame(Positive::ZERO, &TimeFrame::Millisecond, &TimeFrame::Second)
.unwrap();
assert_pos_relative_eq!(result, Positive::ZERO, pos_or_panic!(1e-10));
}
}
#[cfg(all(test, feature = "clock"))]
mod tests_x_days_formatted_pos {
use super::*;
#[test]
fn test_get_x_days_formatted_pos_rounds_up_and_matches_the_integer_form() {
let date = get_x_days_formatted_pos(pos_or_panic!(6.2)).expect("seven days ahead");
assert_eq!(Some(date), get_x_days_formatted(7).ok());
}
#[test]
fn test_get_x_days_formatted_pos_zero_days_is_today() {
let date = get_x_days_formatted_pos(Positive::ZERO).expect("today");
assert_eq!(date, get_today_formatted());
}
#[test]
fn test_get_x_days_formatted_pos_beyond_the_calendar_reports_overflow() {
let result = get_x_days_formatted_pos(pos_or_panic!(1_000_000_000.0));
assert!(matches!(
result,
Err(ExpirationDateError::ArithmeticOverflow(_))
));
}
#[test]
fn test_get_x_days_formatted_pos_beyond_i64_reports_overflow() {
let result = get_x_days_formatted_pos(Positive::MAX);
assert!(matches!(
result,
Err(ExpirationDateError::ArithmeticOverflow(_))
));
}
}
#[cfg(all(test, feature = "clock"))]
mod tests_panic_paths {
use super::*;
use chrono::{Duration, Local};
#[test]
fn test_convert_time_frame_from_custom_zero_is_error() {
let result = convert_time_frame(
Positive::ONE,
&TimeFrame::Custom(Positive::ZERO),
&TimeFrame::Day,
);
assert!(result.is_err());
}
#[test]
fn test_convert_time_frame_overflow_is_error() {
let result = convert_time_frame(Positive::MAX, &TimeFrame::Year, &TimeFrame::Day);
assert!(result.is_err());
}
#[test]
fn test_convert_time_frame_values_unchanged() {
let result = convert_time_frame(Positive::TWO, &TimeFrame::Week, &TimeFrame::Day);
assert_eq!(
result.ok(),
Some(
Positive::TWO * units_per_year(&TimeFrame::Day) / units_per_year(&TimeFrame::Week)
)
);
}
#[test]
fn test_get_x_days_formatted_i64_max_is_error() {
assert!(matches!(
get_x_days_formatted(i64::MAX),
Err(ExpirationDateError::ArithmeticOverflow(_))
));
assert!(get_x_days_formatted(i64::MIN).is_err());
}
#[test]
fn test_get_x_days_formatted_billion_days_is_error() {
assert!(matches!(
get_x_days_formatted(1_000_000_000),
Err(ExpirationDateError::ArithmeticOverflow(_))
));
}
#[test]
fn test_get_x_days_formatted_values_unchanged() {
let expected = (Local::now().date_naive() + Duration::days(2))
.format("%d-%b-%Y")
.to_string()
.to_lowercase();
assert_eq!(get_x_days_formatted(2).ok(), Some(expected));
let tomorrow = (Local::now().date_naive() + Duration::days(1))
.format("%d-%b-%Y")
.to_string()
.to_lowercase();
assert_eq!(get_tomorrow_formatted().ok(), Some(tomorrow));
}
}
#[cfg(test)]
mod tests_clock_helpers {
use super::*;
use chrono::TimeZone;
fn base() -> DateTime<Utc> {
match Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).single() {
Some(base) => base,
None => panic!("2030-01-01 is one instant"),
}
}
#[test]
fn test_years_to_expiration_days_matches_get_years_at_at_any_base() {
for days in [0.0, 0.5, 1.0, 7.0, 30.0, 30.25, 365.0, 3650.0, 1e9] {
let expiration = ExpirationDate::Days(pos_or_panic!(days));
let years = years_to_expiration(&expiration).unwrap();
assert_eq!(years, expiration.get_years_at(base()).unwrap());
assert_eq!(
years,
expiration
.get_years_at(base() + Duration::days(777))
.unwrap()
);
}
}
#[test]
fn test_days_to_expiration_days_is_its_own_count() {
let expiration = ExpirationDate::Days(pos_or_panic!(30.25));
assert_eq!(
days_to_expiration(&expiration).unwrap(),
pos_or_panic!(30.25)
);
}
#[test]
fn test_expiration_date_string_datetime_needs_no_clock() {
let expiration = ExpirationDate::DateTime(base() + Duration::days(30));
assert_eq!(expiration_date_string(&expiration).unwrap(), "2030-01-31");
}
#[test]
fn test_resolution_base_prefers_the_reference_datetime() {
ExpirationDate::set_reference_datetime(Some(base()));
let resolved = resolution_base();
ExpirationDate::set_reference_datetime(None);
assert_eq!(resolved, Ok(base()));
}
#[cfg(feature = "clock")]
mod with_clock {
use super::*;
#[test]
fn test_days_answers_match_the_clock_reading_methods() {
for days in [0.0, 0.5, 1.0, 30.0, 30.25, 365.0] {
let expiration = ExpirationDate::Days(pos_or_panic!(days));
assert_eq!(
years_to_expiration(&expiration).unwrap(),
expiration.get_years().unwrap()
);
assert_eq!(
days_to_expiration(&expiration).unwrap(),
expiration.get_days().unwrap()
);
assert_eq!(
expiration_date_string(&expiration).unwrap(),
expiration.get_date_string().unwrap()
);
}
}
#[test]
fn test_datetime_answers_match_the_clock_reading_methods() {
let tolerance = 1e-6;
let expiration =
ExpirationDate::DateTime(wall_clock() + Duration::days(30) + Duration::hours(12));
let years = years_to_expiration(&expiration).unwrap().to_f64();
assert!((years - expiration.get_years().unwrap().to_f64()).abs() < tolerance);
let days = days_to_expiration(&expiration).unwrap().to_f64();
assert!((days - expiration.get_days().unwrap().to_f64()).abs() < 1e-3);
assert_eq!(
expiration_date_string(&expiration).unwrap(),
expiration.get_date_string().unwrap()
);
}
#[test]
fn test_the_reference_datetime_does_not_move_the_evaluation_time() {
let long_ago = base() - Duration::days(36_500);
ExpirationDate::set_reference_datetime(Some(long_ago));
let evaluated = evaluation_time();
ExpirationDate::set_reference_datetime(None);
assert!(evaluated.unwrap() > long_ago);
}
#[test]
fn test_now_or_reads_the_clock() {
let long_ago = base() - Duration::days(365_000);
assert!(now_or(long_ago) > long_ago);
assert!(now().is_ok());
}
}
#[test]
fn test_reference_carrier_returns_what_the_work_returns() {
let carrier = ReferenceCarrier::capture();
assert_eq!(carrier.run(|| 2 + 2), 4);
}
#[cfg(not(feature = "clock"))]
mod without_clock {
use super::*;
#[test]
fn test_reference_carrier_sets_the_reference_on_another_thread() {
ExpirationDate::set_reference_datetime(Some(base()));
let carrier = ReferenceCarrier::capture();
ExpirationDate::set_reference_datetime(None);
let (inside, after) = std::thread::spawn(move || {
let inside = carrier.run(evaluation_time);
(inside, ExpirationDate::get_reference_datetime())
})
.join()
.unwrap();
assert_eq!(inside, Ok(base()));
assert_eq!(after, None);
}
#[test]
fn test_reference_carrier_restores_what_the_thread_had() {
let own = base() + Duration::days(1);
ExpirationDate::set_reference_datetime(Some(base()));
let carrier = ReferenceCarrier::capture();
ExpirationDate::set_reference_datetime(Some(own));
assert_eq!(carrier.run(evaluation_time), Ok(base()));
assert_eq!(ExpirationDate::get_reference_datetime(), Some(own));
let panicked = std::panic::catch_unwind(|| {
carrier.run(|| panic!("work panics"));
});
assert!(panicked.is_err());
assert_eq!(ExpirationDate::get_reference_datetime(), Some(own));
ExpirationDate::set_reference_datetime(None);
}
#[test]
fn test_reference_carrier_without_a_reference_gives_the_typed_error() {
let carrier = ReferenceCarrier::capture();
ExpirationDate::set_reference_datetime(Some(base()));
assert_eq!(carrier.run(evaluation_time), Err(ClockError::Unavailable));
ExpirationDate::set_reference_datetime(None);
}
#[test]
fn test_now_is_the_typed_error() {
assert_eq!(now(), Err(ClockError::Unavailable));
}
#[test]
fn test_now_or_keeps_the_given_instant() {
assert_eq!(now_or(base()), base());
}
#[test]
fn test_now_or_ignores_the_reference_datetime() {
ExpirationDate::set_reference_datetime(Some(base() + Duration::days(1)));
let stamped = now_or(base());
ExpirationDate::set_reference_datetime(None);
assert_eq!(stamped, base());
}
#[test]
fn test_evaluation_time_moves_a_datetime_with_the_reference() {
let expiration = ExpirationDate::DateTime(base() + Duration::days(30));
let at = |reference: DateTime<Utc>| {
ExpirationDate::set_reference_datetime(Some(reference));
let days = days_to_expiration(&expiration);
ExpirationDate::set_reference_datetime(None);
days.unwrap()
};
assert_eq!(at(base()), pos_or_panic!(30.0));
assert_eq!(at(base() + Duration::days(10)), pos_or_panic!(20.0));
}
#[test]
fn test_evaluation_time_falls_back_only_to_the_reference_datetime() {
assert_eq!(evaluation_time(), Err(ClockError::Unavailable));
assert_eq!(resolution_base(), Err(ClockError::Unavailable));
ExpirationDate::set_reference_datetime(Some(base()));
let evaluated = evaluation_time();
ExpirationDate::set_reference_datetime(None);
assert_eq!(evaluated, Ok(base()));
}
#[test]
fn test_datetime_without_a_reference_is_invalid_datetime() {
let expiration = ExpirationDate::DateTime(base());
let expected = ClockError::Unavailable.to_string();
for error in [
years_to_expiration(&expiration).unwrap_err(),
days_to_expiration(&expiration).unwrap_err(),
] {
assert!(
matches!(error, ExpirationDateError::InvalidDateTime(ref text) if *text == expected)
);
}
}
#[test]
fn test_datetime_with_a_reference_matches_the_pure_forms() {
let expiration = ExpirationDate::DateTime(base() + Duration::days(30));
ExpirationDate::set_reference_datetime(Some(base()));
let years = years_to_expiration(&expiration);
let days = days_to_expiration(&expiration);
ExpirationDate::set_reference_datetime(None);
assert_eq!(years.unwrap(), expiration.get_years_at(base()).unwrap());
assert_eq!(days.unwrap(), expiration.get_days_at(base()).unwrap());
}
#[test]
fn test_days_string_needs_a_reference_datetime() {
let expiration = ExpirationDate::Days(pos_or_panic!(30.0));
assert!(expiration_date_string(&expiration).is_err());
ExpirationDate::set_reference_datetime(Some(base()));
let text = expiration_date_string(&expiration);
ExpirationDate::set_reference_datetime(None);
assert_eq!(text.unwrap(), "2030-01-31");
}
#[test]
fn test_local_date_helpers_report_the_missing_clock() {
assert!(matches!(
get_tomorrow_formatted(),
Err(ExpirationDateError::InvalidDateTime(_))
));
assert!(matches!(
get_x_days_formatted_pos(Positive::ONE),
Err(ExpirationDateError::InvalidDateTime(_))
));
}
}
}