1use crate::constants::*;
8use crate::error::ClockError;
9#[cfg(feature = "clock")]
10use chrono::NaiveTime;
11use chrono::{DateTime, Duration, NaiveDate, Utc};
12use expiration_date::ExpirationDate;
13use expiration_date::error::ExpirationDateError;
14use positive::constants::DAYS_IN_A_YEAR;
15use positive::{Positive, PositiveError};
16use rust_decimal_macros::dec;
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20#[cfg(test)]
21use positive::pos_or_panic;
22
23#[cfg(feature = "clock")]
29#[must_use]
30#[inline]
31pub fn wall_clock() -> DateTime<Utc> {
32 Utc::now()
33}
34
35#[cfg(feature = "clock")]
42#[inline]
43pub fn now() -> Result<DateTime<Utc>, ClockError> {
44 Ok(wall_clock())
45}
46
47#[cfg(not(feature = "clock"))]
54#[inline]
55pub fn now() -> Result<DateTime<Utc>, ClockError> {
56 Err(ClockError::Unavailable)
57}
58
59#[must_use]
72#[inline]
73pub fn now_or(existing: DateTime<Utc>) -> DateTime<Utc> {
74 now().unwrap_or(existing)
75}
76
77#[inline]
103pub fn evaluation_time() -> Result<DateTime<Utc>, ClockError> {
104 now().or_else(|error| ExpirationDate::get_reference_datetime().ok_or(error))
105}
106
107#[inline]
117pub fn resolution_base() -> Result<DateTime<Utc>, ClockError> {
118 ExpirationDate::get_reference_datetime().map_or_else(now, Ok)
119}
120
121#[derive(Debug, Clone, Copy)]
140#[must_use = "a carrier does nothing until its work is run through it"]
141pub struct ReferenceCarrier {
142 #[cfg(not(feature = "clock"))]
143 reference: Option<DateTime<Utc>>,
144}
145
146#[cfg(not(feature = "clock"))]
148struct RestoreReference(Option<DateTime<Utc>>);
149
150#[cfg(not(feature = "clock"))]
151impl Drop for RestoreReference {
152 fn drop(&mut self) {
153 ExpirationDate::set_reference_datetime(self.0);
154 }
155}
156
157impl ReferenceCarrier {
158 #[cfg(not(feature = "clock"))]
160 #[inline]
161 pub fn capture() -> Self {
162 Self {
163 reference: ExpirationDate::get_reference_datetime(),
164 }
165 }
166
167 #[cfg(feature = "clock")]
169 #[inline]
170 pub fn capture() -> Self {
171 Self {}
172 }
173
174 #[cfg(not(feature = "clock"))]
177 #[inline]
178 pub fn run<R>(&self, work: impl FnOnce() -> R) -> R {
179 let _restore = RestoreReference(ExpirationDate::get_reference_datetime());
180 ExpirationDate::set_reference_datetime(self.reference);
181 work()
182 }
183
184 #[cfg(feature = "clock")]
186 #[inline]
187 pub fn run<R>(&self, work: impl FnOnce() -> R) -> R {
188 work()
189 }
190}
191
192#[inline]
204pub fn years_to_expiration(expiration: &ExpirationDate) -> Result<Positive, ExpirationDateError> {
205 match expiration {
206 ExpirationDate::Days(days) => {
209 Positive::new(days.to_f64() / DAYS_IN_A_YEAR.to_f64()) .map_err(Into::into)
211 }
212 ExpirationDate::DateTime(_) => expiration.get_years_at(evaluation_time()?),
213 }
214}
215
216#[inline]
228pub fn days_to_expiration(expiration: &ExpirationDate) -> Result<Positive, ExpirationDateError> {
229 match expiration {
230 ExpirationDate::Days(days) => Ok(*days),
231 ExpirationDate::DateTime(_) => expiration.get_days_at(evaluation_time()?),
232 }
233}
234
235pub fn expiration_date_string(expiration: &ExpirationDate) -> Result<String, ExpirationDateError> {
247 match expiration {
248 ExpirationDate::DateTime(datetime) => expiration.get_date_string_at(*datetime),
249 ExpirationDate::Days(_) => expiration.get_date_string_at(evaluation_time()?),
250 }
251}
252
253#[cfg(feature = "clock")]
255#[inline]
256fn local_today() -> Result<NaiveDate, ClockError> {
257 Ok(chrono::Local::now().date_naive())
258}
259
260#[cfg(not(feature = "clock"))]
262#[inline]
263fn local_today() -> Result<NaiveDate, ClockError> {
264 Err(ClockError::Unavailable)
265}
266
267#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
294#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
295pub enum TimeFrame {
296 Microsecond,
298 Millisecond,
300 Second,
302 Minute,
304 Hour,
306 Day,
308 Week,
310 Month,
312 Quarter,
314 Year,
316 Custom(Positive),
318}
319
320impl TimeFrame {
321 #[must_use]
348 pub fn periods_per_year(&self) -> Positive {
349 match self {
359 TimeFrame::Microsecond => pos_lit(dec!(5896800000000.0000)),
361 TimeFrame::Millisecond => pos_lit(dec!(5896800000.000)),
363 TimeFrame::Second => pos_lit(dec!(5896800.000)),
365 TimeFrame::Minute => pos_lit(dec!(98280.00)),
367 TimeFrame::Hour => pos_lit(dec!(1638.00)),
369 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, }
376 }
377}
378
379impl fmt::Display for TimeFrame {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 match self {
382 TimeFrame::Microsecond => write!(f, "microsecond"),
383 TimeFrame::Millisecond => write!(f, "millisecond"),
384 TimeFrame::Second => write!(f, "second"),
385 TimeFrame::Minute => write!(f, "minute"),
386 TimeFrame::Hour => write!(f, "hour"),
387 TimeFrame::Day => write!(f, "day"),
388 TimeFrame::Week => write!(f, "week"),
389 TimeFrame::Month => write!(f, "month"),
390 TimeFrame::Quarter => write!(f, "quarter"),
391 TimeFrame::Year => write!(f, "year"),
392 TimeFrame::Custom(periods) => write!(f, "custom ({periods})"),
393 }
394 }
395}
396
397fn pos_lit(d: rust_decimal::Decimal) -> Positive {
407 Positive::new_decimal(d).unwrap_or(Positive::ZERO)
408}
409
410#[must_use]
414pub fn units_per_year(time_frame: &TimeFrame) -> Positive {
415 match time_frame {
416 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)),
430 TimeFrame::Month => pos_lit(dec!(12.0)), TimeFrame::Quarter => pos_lit(dec!(4.0)), TimeFrame::Year => Positive::ONE, TimeFrame::Custom(periods) => *periods, }
435}
436
437pub fn convert_time_frame(
475 value: Positive,
476 from_time_frame: &TimeFrame,
477 to_time_frame: &TimeFrame,
478) -> Result<Positive, PositiveError> {
479 if from_time_frame == to_time_frame {
481 return Ok(value);
482 }
483
484 if value.is_zero() {
485 return Ok(Positive::ZERO);
486 }
487
488 let from_units_per_year = units_per_year(from_time_frame);
490 let to_units_per_year = units_per_year(to_time_frame);
491
492 let conversion_factor = to_units_per_year.checked_div(&from_units_per_year)?;
498 value.checked_mul(&conversion_factor)
500}
501
502pub fn get_tomorrow_formatted() -> Result<String, ExpirationDateError> {
522 get_x_days_formatted(1)
523}
524
525pub fn get_x_days_formatted(days: i64) -> Result<String, ExpirationDateError> {
549 let today = local_today()?;
550 let target = Duration::try_days(days)
551 .and_then(|delta| today.checked_add_signed(delta))
552 .ok_or_else(|| {
553 ExpirationDateError::ArithmeticOverflow(format!(
554 "a date {days} days from today is outside the representable calendar range"
555 ))
556 })?;
557 Ok(target.format("%d-%b-%Y").to_string().to_lowercase())
558}
559
560pub fn get_x_days_formatted_pos(days: Positive) -> Result<String, ExpirationDateError> {
591 let ceiling = days
594 .checked_ceiling()
595 .ok()
596 .and_then(|whole| whole.to_i64_checked())
597 .ok_or_else(|| unrepresentable_day_offset(days))?;
598 let today = local_today()?;
599 let target = Duration::try_days(ceiling)
600 .and_then(|delta| today.checked_add_signed(delta))
601 .ok_or_else(|| unrepresentable_day_offset(days))?;
602 Ok(target.format("%d-%b-%Y").to_string().to_lowercase())
603}
604
605#[cold]
607#[inline(never)]
608fn unrepresentable_day_offset(days: Positive) -> ExpirationDateError {
609 ExpirationDateError::ArithmeticOverflow(format!(
610 "a date {days} days from today is outside the representable calendar range"
611 ))
612}
613
614#[cfg(feature = "clock")]
629#[must_use]
630pub fn get_today_formatted() -> String {
631 let today = chrono::Local::now().date_naive();
632 today.format("%d-%b-%Y").to_string().to_lowercase()
633}
634
635#[cfg(feature = "clock")]
643const TODAY_CUTOFF: NaiveTime = match NaiveTime::from_hms_opt(18, 30, 0) {
644 Some(t) => t,
645 None => unreachable!(), };
647
648#[cfg(feature = "clock")]
672#[must_use]
673pub fn get_today_or_tomorrow_formatted() -> String {
674 let cutoff_time = TODAY_CUTOFF;
675 let now = wall_clock();
676 let target_date = if now.time() > cutoff_time {
678 now.date_naive()
679 .succ_opt()
680 .unwrap_or_else(|| now.date_naive()) } else {
682 now.date_naive()
683 };
684 target_date.format("%d-%b-%Y").to_string().to_lowercase()
685}
686
687#[cfg(test)]
688mod tests_timeframe {
689 use super::*;
690 use positive::assert_pos_relative_eq;
691
692 #[test]
693 fn test_microsecond_periods() {
694 let expected =
695 *TRADING_DAYS * *TRADING_HOURS * *SECONDS_PER_HOUR * *MICROSECONDS_PER_SECOND;
696 assert_eq!(TimeFrame::Microsecond.periods_per_year(), expected);
697 }
698
699 #[test]
700 fn test_millisecond_periods() {
701 let expected = *TRADING_DAYS * *TRADING_HOURS * *SECONDS_PER_HOUR * MILLISECONDS_PER_SECOND;
702 assert_eq!(TimeFrame::Millisecond.periods_per_year(), expected);
703 }
704
705 #[test]
706 fn test_second_periods() {
707 let expected = *TRADING_DAYS * *TRADING_HOURS * *SECONDS_PER_HOUR;
708 assert_eq!(TimeFrame::Second.periods_per_year(), expected);
709 }
710
711 #[test]
712 fn test_minute_periods() {
713 let expected = *TRADING_DAYS * *TRADING_HOURS * MINUTES_PER_HOUR;
714 assert_eq!(TimeFrame::Minute.periods_per_year(), expected);
715 }
716
717 #[test]
718 fn test_hour_periods() {
719 let expected = *TRADING_DAYS * *TRADING_HOURS;
720 assert_eq!(TimeFrame::Hour.periods_per_year(), expected);
721 }
722
723 #[test]
724 fn test_day_periods() {
725 assert_eq!(TimeFrame::Day.periods_per_year(), *TRADING_DAYS);
726 }
727
728 #[test]
732 fn test_periods_per_year_matches_products() {
733 let hours = *TRADING_DAYS * *TRADING_HOURS;
734 let seconds = hours * *SECONDS_PER_HOUR;
735 let cases = [
736 (TimeFrame::Microsecond, seconds * *MICROSECONDS_PER_SECOND),
737 (TimeFrame::Millisecond, seconds * MILLISECONDS_PER_SECOND),
738 (TimeFrame::Second, seconds),
739 (TimeFrame::Minute, hours * MINUTES_PER_HOUR),
740 (TimeFrame::Hour, hours),
741 ];
742 for (frame, product) in cases {
743 assert_eq!(
744 frame.periods_per_year().to_dec().to_string(),
745 product.to_dec().to_string(),
746 "{frame}"
747 );
748 }
749 }
750
751 #[test]
752 fn test_units_per_year_week_is_365_over_7() {
753 assert_eq!(
754 units_per_year(&TimeFrame::Week).to_dec().to_string(),
755 (dec!(365.0) / dec!(7.0)).to_string()
756 );
757 }
758
759 #[test]
760 fn test_week_periods() {
761 assert_eq!(TimeFrame::Week.periods_per_year(), 52.0);
762 }
763
764 #[test]
765 fn test_month_periods() {
766 assert_eq!(TimeFrame::Month.periods_per_year(), 12.0);
767 }
768
769 #[test]
770 fn test_quarter_periods() {
771 assert_eq!(TimeFrame::Quarter.periods_per_year(), 4.0);
772 }
773
774 #[test]
775 fn test_year_periods() {
776 assert_eq!(TimeFrame::Year.periods_per_year(), 1.0);
777 }
778
779 #[test]
780 fn test_custom_periods() {
781 let custom_periods = pos_or_panic!(123.45);
782 assert_eq!(
783 TimeFrame::Custom(custom_periods).periods_per_year(),
784 custom_periods
785 );
786 }
787
788 #[test]
789 fn test_relative_period_relationships() {
790 assert!(
792 TimeFrame::Microsecond.periods_per_year() > TimeFrame::Millisecond.periods_per_year()
793 );
794 assert!(TimeFrame::Millisecond.periods_per_year() > TimeFrame::Second.periods_per_year());
795 assert!(TimeFrame::Second.periods_per_year() > TimeFrame::Minute.periods_per_year());
796 assert!(TimeFrame::Minute.periods_per_year() > TimeFrame::Hour.periods_per_year());
797 assert!(TimeFrame::Hour.periods_per_year() > TimeFrame::Day.periods_per_year());
798 assert!(TimeFrame::Day.periods_per_year() > TimeFrame::Week.periods_per_year());
799 assert!(TimeFrame::Week.periods_per_year() > TimeFrame::Month.periods_per_year());
800 assert!(TimeFrame::Month.periods_per_year() > TimeFrame::Quarter.periods_per_year());
801 assert!(TimeFrame::Quarter.periods_per_year() > TimeFrame::Year.periods_per_year());
802 }
803
804 #[test]
805 fn test_specific_conversion_ratios() {
806 assert_pos_relative_eq!(
808 TimeFrame::Hour.periods_per_year() / TimeFrame::Day.periods_per_year(),
809 *TRADING_HOURS,
810 pos_or_panic!(1e-10)
811 );
812
813 assert_pos_relative_eq!(
814 TimeFrame::Minute.periods_per_year() / TimeFrame::Hour.periods_per_year(),
815 MINUTES_PER_HOUR,
816 pos_or_panic!(1e-10)
817 );
818
819 assert_pos_relative_eq!(
820 TimeFrame::Second.periods_per_year() / TimeFrame::Minute.periods_per_year(),
821 MINUTES_PER_HOUR,
822 pos_or_panic!(1e-10)
823 );
824 }
825
826 #[test]
827 fn test_trading_days_relationship() {
828 assert_pos_relative_eq!(
830 TimeFrame::Day.periods_per_year(),
831 *TRADING_DAYS,
832 pos_or_panic!(1e-10)
833 );
834
835 assert_pos_relative_eq!(
836 TimeFrame::Hour.periods_per_year() / *TRADING_HOURS,
837 *TRADING_DAYS,
838 pos_or_panic!(1e-10)
839 );
840 }
841
842 #[test]
843 fn test_custom_edge_cases() {
844 assert_eq!(TimeFrame::Custom(Positive::ZERO).periods_per_year(), 0.0);
846 assert_eq!(
847 TimeFrame::Custom(Positive::MAX).periods_per_year(),
848 Positive::MAX
849 );
850 }
851
852 #[test]
853 fn test_timeframe_debug() {
854 assert_eq!(format!("{:?}", TimeFrame::Day), "Day");
855 assert_eq!(
856 format!("{:?}", TimeFrame::Custom(pos_or_panic!(1.5))),
857 "Custom(1.5)"
858 );
859 }
860
861 #[test]
862 fn test_timeframe_clone() {
863 let tf = TimeFrame::Day;
864 let cloned = tf;
865 assert_eq!(tf.periods_per_year(), cloned.periods_per_year());
866 }
867
868 #[test]
869 fn test_timeframe_copy() {
870 let tf = TimeFrame::Day;
871 let copied = tf;
872 assert_eq!(tf.periods_per_year(), copied.periods_per_year());
873 }
874}
875
876#[cfg(test)]
877mod tests_timeframe_convert {
878 use super::*;
879 use positive::assert_pos_relative_eq;
880
881 #[test]
882 fn test_convert_seconds_to_minutes() {
883 let result =
884 convert_time_frame(pos_or_panic!(60.0), &TimeFrame::Second, &TimeFrame::Minute)
885 .unwrap();
886 assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
887 }
888
889 #[test]
890 fn test_convert_hours_to_days() {
891 let result =
892 convert_time_frame(pos_or_panic!(12.0), &TimeFrame::Hour, &TimeFrame::Day).unwrap();
893 assert_pos_relative_eq!(result, pos_or_panic!(0.5), pos_or_panic!(1e-10));
894 }
895
896 #[test]
897 fn test_convert_days_to_weeks() {
898 let result =
899 convert_time_frame(pos_or_panic!(7.0), &TimeFrame::Day, &TimeFrame::Week).unwrap();
900 assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
901 }
902
903 #[test]
904 fn test_convert_weeks_to_days() {
905 let result = convert_time_frame(Positive::TWO, &TimeFrame::Week, &TimeFrame::Day).unwrap();
906 assert_pos_relative_eq!(result, pos_or_panic!(14.0), pos_or_panic!(1e-10));
907 }
908
909 #[test]
910 fn test_convert_months_to_quarters() {
911 let result =
912 convert_time_frame(pos_or_panic!(3.0), &TimeFrame::Month, &TimeFrame::Quarter).unwrap();
913 assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
914 }
915
916 #[test]
917 fn test_convert_minutes_to_hours() {
918 let result =
919 convert_time_frame(pos_or_panic!(120.0), &TimeFrame::Minute, &TimeFrame::Hour).unwrap();
920 assert_pos_relative_eq!(result, Positive::TWO, pos_or_panic!(1e-10));
921 }
922
923 #[test]
924 fn test_convert_custom_to_day() {
925 let result = convert_time_frame(
926 pos_or_panic!(10.0),
927 &TimeFrame::Custom(pos_or_panic!(365.0)),
928 &TimeFrame::Day,
929 )
930 .unwrap();
931 assert_pos_relative_eq!(result, pos_or_panic!(10.0), pos_or_panic!(1e-10));
932 }
933
934 #[test]
935 fn test_convert_day_to_custom() {
936 let result = convert_time_frame(
937 Positive::TWO,
938 &TimeFrame::Day,
939 &TimeFrame::Custom(pos_or_panic!(365.0)),
940 )
941 .unwrap();
942 assert_pos_relative_eq!(result, Positive::TWO, pos_or_panic!(1e-10));
943 }
944
945 #[test]
946 fn test_convert_same_timeframe() {
947 let result =
948 convert_time_frame(pos_or_panic!(42.0), &TimeFrame::Hour, &TimeFrame::Hour).unwrap();
949 assert_pos_relative_eq!(result, pos_or_panic!(42.0), pos_or_panic!(1e-10));
950 }
951
952 #[test]
953 fn test_convert_weeks_to_months() {
954 let result =
955 convert_time_frame(pos_or_panic!(4.0), &TimeFrame::Week, &TimeFrame::Month).unwrap();
956 assert_pos_relative_eq!(
958 result,
959 pos_or_panic!(0.920_547_945_255_920_4),
960 pos_or_panic!(1e-10)
961 );
962 }
963
964 #[test]
965 fn test_convert_milliseconds_to_seconds() {
966 let result = convert_time_frame(
967 pos_or_panic!(1000.0),
968 &TimeFrame::Millisecond,
969 &TimeFrame::Second,
970 )
971 .unwrap();
972 assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(1e-10));
973 }
974
975 #[test]
976 fn test_zero() {
977 let result =
978 convert_time_frame(Positive::ZERO, &TimeFrame::Millisecond, &TimeFrame::Second)
979 .unwrap();
980 assert_pos_relative_eq!(result, Positive::ZERO, pos_or_panic!(1e-10));
981 }
982}
983
984#[cfg(all(test, feature = "clock"))]
985mod tests_x_days_formatted_pos {
986 use super::*;
987
988 #[test]
989 fn test_get_x_days_formatted_pos_rounds_up_and_matches_the_integer_form() {
990 let date = get_x_days_formatted_pos(pos_or_panic!(6.2)).expect("seven days ahead");
991 assert_eq!(Some(date), get_x_days_formatted(7).ok());
992 }
993
994 #[test]
995 fn test_get_x_days_formatted_pos_zero_days_is_today() {
996 let date = get_x_days_formatted_pos(Positive::ZERO).expect("today");
997 assert_eq!(date, get_today_formatted());
998 }
999
1000 #[test]
1001 fn test_get_x_days_formatted_pos_beyond_the_calendar_reports_overflow() {
1002 let result = get_x_days_formatted_pos(pos_or_panic!(1_000_000_000.0));
1004 assert!(matches!(
1005 result,
1006 Err(ExpirationDateError::ArithmeticOverflow(_))
1007 ));
1008 }
1009
1010 #[test]
1011 fn test_get_x_days_formatted_pos_beyond_i64_reports_overflow() {
1012 let result = get_x_days_formatted_pos(Positive::MAX);
1013 assert!(matches!(
1014 result,
1015 Err(ExpirationDateError::ArithmeticOverflow(_))
1016 ));
1017 }
1018}
1019
1020#[cfg(all(test, feature = "clock"))]
1021mod tests_panic_paths {
1022 use super::*;
1023 use chrono::{Duration, Local};
1024
1025 #[test]
1028 fn test_convert_time_frame_from_custom_zero_is_error() {
1029 let result = convert_time_frame(
1030 Positive::ONE,
1031 &TimeFrame::Custom(Positive::ZERO),
1032 &TimeFrame::Day,
1033 );
1034 assert!(result.is_err());
1035 }
1036
1037 #[test]
1040 fn test_convert_time_frame_overflow_is_error() {
1041 let result = convert_time_frame(Positive::MAX, &TimeFrame::Year, &TimeFrame::Day);
1042 assert!(result.is_err());
1043 }
1044
1045 #[test]
1046 fn test_convert_time_frame_values_unchanged() {
1047 let result = convert_time_frame(Positive::TWO, &TimeFrame::Week, &TimeFrame::Day);
1048 assert_eq!(
1049 result.ok(),
1050 Some(
1051 Positive::TWO * units_per_year(&TimeFrame::Day) / units_per_year(&TimeFrame::Week)
1052 )
1053 );
1054 }
1055
1056 #[test]
1059 fn test_get_x_days_formatted_i64_max_is_error() {
1060 assert!(matches!(
1061 get_x_days_formatted(i64::MAX),
1062 Err(ExpirationDateError::ArithmeticOverflow(_))
1063 ));
1064 assert!(get_x_days_formatted(i64::MIN).is_err());
1065 }
1066
1067 #[test]
1070 fn test_get_x_days_formatted_billion_days_is_error() {
1071 assert!(matches!(
1072 get_x_days_formatted(1_000_000_000),
1073 Err(ExpirationDateError::ArithmeticOverflow(_))
1074 ));
1075 }
1076
1077 #[test]
1078 fn test_get_x_days_formatted_values_unchanged() {
1079 let expected = (Local::now().date_naive() + Duration::days(2))
1080 .format("%d-%b-%Y")
1081 .to_string()
1082 .to_lowercase();
1083 assert_eq!(get_x_days_formatted(2).ok(), Some(expected));
1084 let tomorrow = (Local::now().date_naive() + Duration::days(1))
1085 .format("%d-%b-%Y")
1086 .to_string()
1087 .to_lowercase();
1088 assert_eq!(get_tomorrow_formatted().ok(), Some(tomorrow));
1089 }
1090}
1091
1092#[cfg(test)]
1093mod tests_clock_helpers {
1094 use super::*;
1095 use chrono::TimeZone;
1096
1097 fn base() -> DateTime<Utc> {
1098 match Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).single() {
1099 Some(base) => base,
1100 None => panic!("2030-01-01 is one instant"),
1101 }
1102 }
1103
1104 #[test]
1107 fn test_years_to_expiration_days_matches_get_years_at_at_any_base() {
1108 for days in [0.0, 0.5, 1.0, 7.0, 30.0, 30.25, 365.0, 3650.0, 1e9] {
1109 let expiration = ExpirationDate::Days(pos_or_panic!(days));
1110 let years = years_to_expiration(&expiration).unwrap();
1111 assert_eq!(years, expiration.get_years_at(base()).unwrap());
1112 assert_eq!(
1113 years,
1114 expiration
1115 .get_years_at(base() + Duration::days(777))
1116 .unwrap()
1117 );
1118 }
1119 }
1120
1121 #[test]
1122 fn test_days_to_expiration_days_is_its_own_count() {
1123 let expiration = ExpirationDate::Days(pos_or_panic!(30.25));
1124 assert_eq!(
1125 days_to_expiration(&expiration).unwrap(),
1126 pos_or_panic!(30.25)
1127 );
1128 }
1129
1130 #[test]
1131 fn test_expiration_date_string_datetime_needs_no_clock() {
1132 let expiration = ExpirationDate::DateTime(base() + Duration::days(30));
1133 assert_eq!(expiration_date_string(&expiration).unwrap(), "2030-01-31");
1134 }
1135
1136 #[test]
1137 fn test_resolution_base_prefers_the_reference_datetime() {
1138 ExpirationDate::set_reference_datetime(Some(base()));
1139 let resolved = resolution_base();
1140 ExpirationDate::set_reference_datetime(None);
1141 assert_eq!(resolved, Ok(base()));
1142 }
1143
1144 #[cfg(feature = "clock")]
1147 mod with_clock {
1148 use super::*;
1149
1150 #[test]
1151 fn test_days_answers_match_the_clock_reading_methods() {
1152 for days in [0.0, 0.5, 1.0, 30.0, 30.25, 365.0] {
1153 let expiration = ExpirationDate::Days(pos_or_panic!(days));
1154 assert_eq!(
1155 years_to_expiration(&expiration).unwrap(),
1156 expiration.get_years().unwrap()
1157 );
1158 assert_eq!(
1159 days_to_expiration(&expiration).unwrap(),
1160 expiration.get_days().unwrap()
1161 );
1162 assert_eq!(
1163 expiration_date_string(&expiration).unwrap(),
1164 expiration.get_date_string().unwrap()
1165 );
1166 }
1167 }
1168
1169 #[test]
1170 fn test_datetime_answers_match_the_clock_reading_methods() {
1171 let tolerance = 1e-6;
1174 let expiration =
1175 ExpirationDate::DateTime(wall_clock() + Duration::days(30) + Duration::hours(12));
1176 let years = years_to_expiration(&expiration).unwrap().to_f64();
1177 assert!((years - expiration.get_years().unwrap().to_f64()).abs() < tolerance);
1178 let days = days_to_expiration(&expiration).unwrap().to_f64();
1179 assert!((days - expiration.get_days().unwrap().to_f64()).abs() < 1e-3);
1180 assert_eq!(
1181 expiration_date_string(&expiration).unwrap(),
1182 expiration.get_date_string().unwrap()
1183 );
1184 }
1185
1186 #[test]
1187 fn test_the_reference_datetime_does_not_move_the_evaluation_time() {
1188 let long_ago = base() - Duration::days(36_500);
1189 ExpirationDate::set_reference_datetime(Some(long_ago));
1190 let evaluated = evaluation_time();
1191 ExpirationDate::set_reference_datetime(None);
1192 assert!(evaluated.unwrap() > long_ago);
1193 }
1194
1195 #[test]
1196 fn test_now_or_reads_the_clock() {
1197 let long_ago = base() - Duration::days(365_000);
1198 assert!(now_or(long_ago) > long_ago);
1199 assert!(now().is_ok());
1200 }
1201 }
1202
1203 #[test]
1204 fn test_reference_carrier_returns_what_the_work_returns() {
1205 let carrier = ReferenceCarrier::capture();
1206 assert_eq!(carrier.run(|| 2 + 2), 4);
1207 }
1208
1209 #[cfg(not(feature = "clock"))]
1210 mod without_clock {
1211 use super::*;
1212
1213 #[test]
1216 fn test_reference_carrier_sets_the_reference_on_another_thread() {
1217 ExpirationDate::set_reference_datetime(Some(base()));
1218 let carrier = ReferenceCarrier::capture();
1219 ExpirationDate::set_reference_datetime(None);
1220 let (inside, after) = std::thread::spawn(move || {
1221 let inside = carrier.run(evaluation_time);
1222 (inside, ExpirationDate::get_reference_datetime())
1223 })
1224 .join()
1225 .unwrap();
1226 assert_eq!(inside, Ok(base()));
1227 assert_eq!(after, None);
1228 }
1229
1230 #[test]
1231 fn test_reference_carrier_restores_what_the_thread_had() {
1232 let own = base() + Duration::days(1);
1233 ExpirationDate::set_reference_datetime(Some(base()));
1234 let carrier = ReferenceCarrier::capture();
1235 ExpirationDate::set_reference_datetime(Some(own));
1236 assert_eq!(carrier.run(evaluation_time), Ok(base()));
1237 assert_eq!(ExpirationDate::get_reference_datetime(), Some(own));
1238 let panicked = std::panic::catch_unwind(|| {
1239 carrier.run(|| panic!("work panics"));
1240 });
1241 assert!(panicked.is_err());
1242 assert_eq!(ExpirationDate::get_reference_datetime(), Some(own));
1243 ExpirationDate::set_reference_datetime(None);
1244 }
1245
1246 #[test]
1249 fn test_reference_carrier_without_a_reference_gives_the_typed_error() {
1250 let carrier = ReferenceCarrier::capture();
1251 ExpirationDate::set_reference_datetime(Some(base()));
1252 assert_eq!(carrier.run(evaluation_time), Err(ClockError::Unavailable));
1253 ExpirationDate::set_reference_datetime(None);
1254 }
1255
1256 #[test]
1257 fn test_now_is_the_typed_error() {
1258 assert_eq!(now(), Err(ClockError::Unavailable));
1259 }
1260
1261 #[test]
1262 fn test_now_or_keeps_the_given_instant() {
1263 assert_eq!(now_or(base()), base());
1264 }
1265
1266 #[test]
1269 fn test_now_or_ignores_the_reference_datetime() {
1270 ExpirationDate::set_reference_datetime(Some(base() + Duration::days(1)));
1271 let stamped = now_or(base());
1272 ExpirationDate::set_reference_datetime(None);
1273 assert_eq!(stamped, base());
1274 }
1275
1276 #[test]
1279 fn test_evaluation_time_moves_a_datetime_with_the_reference() {
1280 let expiration = ExpirationDate::DateTime(base() + Duration::days(30));
1281 let at = |reference: DateTime<Utc>| {
1282 ExpirationDate::set_reference_datetime(Some(reference));
1283 let days = days_to_expiration(&expiration);
1284 ExpirationDate::set_reference_datetime(None);
1285 days.unwrap()
1286 };
1287 assert_eq!(at(base()), pos_or_panic!(30.0));
1288 assert_eq!(at(base() + Duration::days(10)), pos_or_panic!(20.0));
1289 }
1290
1291 #[test]
1292 fn test_evaluation_time_falls_back_only_to_the_reference_datetime() {
1293 assert_eq!(evaluation_time(), Err(ClockError::Unavailable));
1294 assert_eq!(resolution_base(), Err(ClockError::Unavailable));
1295 ExpirationDate::set_reference_datetime(Some(base()));
1296 let evaluated = evaluation_time();
1297 ExpirationDate::set_reference_datetime(None);
1298 assert_eq!(evaluated, Ok(base()));
1299 }
1300
1301 #[test]
1302 fn test_datetime_without_a_reference_is_invalid_datetime() {
1303 let expiration = ExpirationDate::DateTime(base());
1304 let expected = ClockError::Unavailable.to_string();
1305 for error in [
1306 years_to_expiration(&expiration).unwrap_err(),
1307 days_to_expiration(&expiration).unwrap_err(),
1308 ] {
1309 assert!(
1310 matches!(error, ExpirationDateError::InvalidDateTime(ref text) if *text == expected)
1311 );
1312 }
1313 }
1314
1315 #[test]
1316 fn test_datetime_with_a_reference_matches_the_pure_forms() {
1317 let expiration = ExpirationDate::DateTime(base() + Duration::days(30));
1318 ExpirationDate::set_reference_datetime(Some(base()));
1319 let years = years_to_expiration(&expiration);
1320 let days = days_to_expiration(&expiration);
1321 ExpirationDate::set_reference_datetime(None);
1322 assert_eq!(years.unwrap(), expiration.get_years_at(base()).unwrap());
1323 assert_eq!(days.unwrap(), expiration.get_days_at(base()).unwrap());
1324 }
1325
1326 #[test]
1327 fn test_days_string_needs_a_reference_datetime() {
1328 let expiration = ExpirationDate::Days(pos_or_panic!(30.0));
1329 assert!(expiration_date_string(&expiration).is_err());
1330 ExpirationDate::set_reference_datetime(Some(base()));
1331 let text = expiration_date_string(&expiration);
1332 ExpirationDate::set_reference_datetime(None);
1333 assert_eq!(text.unwrap(), "2030-01-31");
1334 }
1335
1336 #[test]
1337 fn test_local_date_helpers_report_the_missing_clock() {
1338 assert!(matches!(
1339 get_tomorrow_formatted(),
1340 Err(ExpirationDateError::InvalidDateTime(_))
1341 ));
1342 assert!(matches!(
1343 get_x_days_formatted_pos(Positive::ONE),
1344 Err(ExpirationDateError::InvalidDateTime(_))
1345 ));
1346 }
1347 }
1348}