Skip to main content

optionstratlib_core/utils/
time.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 23/10/24
5******************************************************************************/
6
7use 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/// Reads the wall clock.
24///
25/// The only place in the library that does, together with the local-date
26/// helper behind [`get_x_days_formatted`]. Exists only with the `clock`
27/// feature; use [`now`] where the code has an error channel.
28#[cfg(feature = "clock")]
29#[must_use]
30#[inline]
31pub fn wall_clock() -> DateTime<Utc> {
32    Utc::now()
33}
34
35/// The current instant, from the wall clock.
36///
37/// # Errors
38///
39/// Returns [`ClockError::Unavailable`] in a build without the `clock` feature.
40/// There is no fallback to a made-up instant.
41#[cfg(feature = "clock")]
42#[inline]
43pub fn now() -> Result<DateTime<Utc>, ClockError> {
44    Ok(wall_clock())
45}
46
47/// The current instant, from the wall clock.
48///
49/// # Errors
50///
51/// Returns [`ClockError::Unavailable`] in a build without the `clock` feature,
52/// as this one is. There is no fallback to a made-up instant.
53#[cfg(not(feature = "clock"))]
54#[inline]
55pub fn now() -> Result<DateTime<Utc>, ClockError> {
56    Err(ClockError::Unavailable)
57}
58
59/// The current instant when the build has a clock, `existing` otherwise.
60///
61/// For the code that re-stamps something that already carries a time, such as
62/// a leg rebuilt into a strategy or the P&L of a dated transaction.
63///
64/// **The two builds stamp differently**: the same call gives the time of the call
65/// with the `clock` feature and `existing` without it, and features unify, so
66/// another crate in the build can change which one this is. That is acceptable
67/// because the result only labels a record and goes into no price, Greek or P&L
68/// figure, and because `existing` came from the caller, so it is never a made-up
69/// time. Code whose result depends on the instant must not use this: take the time
70/// as an argument, or use [`now`] and report [`ClockError`].
71#[must_use]
72#[inline]
73pub fn now_or(existing: DateTime<Utc>) -> DateTime<Utc> {
74    now().unwrap_or(existing)
75}
76
77/// The instant a time-to-expiry is measured from.
78///
79/// With the `clock` feature, the wall clock; the thread's reference datetime is
80/// ignored, as `ExpirationDate::get_years` ignores it. Without it, the
81/// thread's reference datetime.
82///
83/// **The two builds can evaluate differently**: with a reference datetime set, a
84/// `DateTime` expiration is measured from the wall clock with the `clock` feature
85/// and from the reference without it, so the same call can price differently, and
86/// features unify. That is acceptable because the default build has to keep
87/// measuring from the wall clock, and without a clock the reference datetime is the
88/// only time the caller can have chosen: it is never made up, and with none this
89/// fails. Code that needs the same answer in every build takes the time as an
90/// argument: the `*_at` forms read neither.
91///
92/// **`clock` is not behaviourally additive here.** Features unify, so if any crate in
93/// the final graph enables `clock`, a replay engine driven by the reference datetime
94/// silently measures a `DateTime` from the wall clock instead, with no compile error.
95/// A consumer that depends on the reference should assert at startup that the build
96/// has no clock: `assert!(now().is_err())`.
97///
98/// # Errors
99///
100/// Returns [`ClockError::Unavailable`] when the build has no clock and the
101/// thread has no reference datetime.
102#[inline]
103pub fn evaluation_time() -> Result<DateTime<Utc>, ClockError> {
104    now().or_else(|error| ExpirationDate::get_reference_datetime().ok_or(error))
105}
106
107/// The instant a relative expiration is resolved from.
108///
109/// The thread's reference datetime when one is set, the wall clock otherwise,
110/// as `ExpirationDate::get_date` does.
111///
112/// # Errors
113///
114/// Returns [`ClockError::Unavailable`] when the thread has no reference
115/// datetime and the build has no clock.
116#[inline]
117pub fn resolution_base() -> Result<DateTime<Utc>, ClockError> {
118    ExpirationDate::get_reference_datetime().map_or_else(now, Ok)
119}
120
121/// Carries the calling thread's reference datetime into a parallel section.
122///
123/// The reference datetime belongs to one thread, so a rayon worker or a blocking
124/// task does not see the one its caller set. Without the `clock` feature that would
125/// make the same call give an answer serially and fail in parallel, so every parallel
126/// section that can reach [`evaluation_time`] or [`resolution_base`] captures the
127/// reference first and runs its work through it:
128///
129/// ```
130/// use optionstratlib_core::utils::time::ReferenceCarrier;
131///
132/// let carrier = ReferenceCarrier::capture();
133/// let work = move |x: i32| carrier.run(|| x + 1);
134/// assert_eq!(work(1), 2);
135/// ```
136///
137/// With the `clock` feature it does nothing: a worker there measures from the wall
138/// clock, as it always has, so a default build is unchanged.
139#[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/// Puts back the reference datetime a worker had before it ran carried work.
147#[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    /// Captures the calling thread's reference datetime.
159    #[cfg(not(feature = "clock"))]
160    #[inline]
161    pub fn capture() -> Self {
162        Self {
163            reference: ExpirationDate::get_reference_datetime(),
164        }
165    }
166
167    /// Does nothing: a build with a clock carries no reference across threads.
168    #[cfg(feature = "clock")]
169    #[inline]
170    pub fn capture() -> Self {
171        Self {}
172    }
173
174    /// Runs `work` with the captured reference datetime set on this thread, and puts
175    /// back the one the thread had, also if `work` panics.
176    #[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    /// Runs `work`.
185    #[cfg(feature = "clock")]
186    #[inline]
187    pub fn run<R>(&self, work: impl FnOnce() -> R) -> R {
188        work()
189    }
190}
191
192/// The years from now to `expiration`, on Actual/365 Fixed.
193///
194/// Gives what `ExpirationDate::get_years` gives and also exists without the
195/// `clock` feature. A `Days` needs no clock: its count over 365 is the answer,
196/// in every build. A `DateTime` is measured from [`evaluation_time`].
197///
198/// # Errors
199///
200/// Returns [`ExpirationDateError::InvalidDateTime`] for a `DateTime` when
201/// [`evaluation_time`] fails, and the conversion errors of
202/// [`ExpirationDate::get_years_at`].
203#[inline]
204pub fn years_to_expiration(expiration: &ExpirationDate) -> Result<Positive, ExpirationDateError> {
205    match expiration {
206        // The formula of `expiration_date` for a `Days`, which `get_years_at`
207        // answers without reading its base; a test pins the two together.
208        ExpirationDate::Days(days) => {
209            Positive::new(days.to_f64() / DAYS_IN_A_YEAR.to_f64()) // scan-banned: allow -- the formula of `expiration_date`, kept bit for bit so a default build stays numerically identical
210                .map_err(Into::into)
211        }
212        ExpirationDate::DateTime(_) => expiration.get_years_at(evaluation_time()?),
213    }
214}
215
216/// The fractional days from now to `expiration`.
217///
218/// Gives what `ExpirationDate::get_days` gives and also exists without the
219/// `clock` feature. A `Days` returns its own count, in every build. A `DateTime`
220/// is measured from [`evaluation_time`], and is zero once it has passed.
221///
222/// # Errors
223///
224/// Returns [`ExpirationDateError::InvalidDateTime`] for a `DateTime` when
225/// [`evaluation_time`] fails, and the conversion errors of
226/// [`ExpirationDate::get_days_at`].
227#[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
235/// The expiration as a `YYYY-MM-DD` string.
236///
237/// Gives what `ExpirationDate::get_date_string` gives and also exists without
238/// the `clock` feature. A `DateTime` needs no clock. A `Days` counts from 18:30
239/// UTC on the day of [`evaluation_time`].
240///
241/// # Errors
242///
243/// Returns [`ExpirationDateError::InvalidDateTime`] for a `Days` when
244/// [`evaluation_time`] fails, and the overflow errors of
245/// [`ExpirationDate::get_date_string_at`].
246pub 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/// Today's date in the local time zone.
254#[cfg(feature = "clock")]
255#[inline]
256fn local_today() -> Result<NaiveDate, ClockError> {
257    Ok(chrono::Local::now().date_naive())
258}
259
260/// Today's date in the local time zone: unavailable in a build with no clock.
261#[cfg(not(feature = "clock"))]
262#[inline]
263fn local_today() -> Result<NaiveDate, ClockError> {
264    Err(ClockError::Unavailable)
265}
266
267/// Represents different timeframes for volatility calculations.
268///
269/// This enum provides a standardized way to represent various time periods
270/// used in financial calculations, including common periods like days, weeks,
271/// months, and years, as well as custom periods defined by the user.
272///
273/// The `TimeFrame` enum is used throughout the library to specify the timeframe
274/// for calculations like volatility, returns, and other time-dependent metrics.
275///
276/// # Examples
277///
278/// ```
279/// use optionstratlib_core::utils::time::TimeFrame;
280/// use positive::pos_or_panic;
281///
282/// // Using standard timeframes
283/// let daily = TimeFrame::Day;
284/// let weekly = TimeFrame::Week;
285///
286/// // Using custom timeframes
287/// let custom_period = TimeFrame::Custom(pos_or_panic!(360.0));
288///
289/// // Accessing the number of periods per year
290/// let periods_per_year = daily.periods_per_year(); // Returns 252.0
291/// let custom_periods = custom_period.periods_per_year(); // Returns 360.0
292/// ```
293#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd)]
294#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
295pub enum TimeFrame {
296    /// 1-microsecond data.
297    Microsecond,
298    /// 1-millisecond data.
299    Millisecond,
300    /// 1-second data.
301    Second,
302    /// 1-minute data.
303    Minute,
304    /// 1-hour data.
305    Hour,
306    /// Daily data.
307    Day,
308    /// Weekly data.
309    Week,
310    /// Monthly data.
311    Month,
312    /// Quarterly data.
313    Quarter,
314    /// Yearly data.
315    Year,
316    /// Custom periods per year.
317    Custom(Positive),
318}
319
320impl TimeFrame {
321    /// Returns the number of periods in a trading year for this timeframe.
322    ///
323    /// This function calculates the number of periods that occur within a trading year
324    /// based on the chosen `TimeFrame`.  A trading year is assumed to have 252 days
325    /// and 6.5 trading hours per day.
326    ///
327    /// For custom timeframes, the number of periods is directly specified by the user.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// use optionstratlib_core::utils::time::TimeFrame;
333    /// use positive::pos_or_panic;
334    ///
335    /// let daily = TimeFrame::Day;
336    /// let periods_per_year = daily.periods_per_year(); // Returns 252
337    /// assert_eq!(periods_per_year, pos_or_panic!(252.0));
338    ///
339    /// let hourly = TimeFrame::Hour;
340    /// let periods_per_year = hourly.periods_per_year(); // Returns 1638
341    /// assert_eq!(periods_per_year, pos_or_panic!(1638.0));
342    ///
343    /// let custom = TimeFrame::Custom(pos_or_panic!(360.0));
344    /// let periods_per_year = custom.periods_per_year(); // Returns 360
345    /// assert_eq!(periods_per_year, pos_or_panic!(360.0));
346    /// ```
347    #[must_use]
348    pub fn periods_per_year(&self) -> Positive {
349        // The sub-day frames are products of `TRADING_DAYS` (252.0),
350        // `TRADING_HOURS` (6.5), `SECONDS_PER_HOUR` (3600.0),
351        // `MINUTES_PER_HOUR` (60), `MILLISECONDS_PER_SECOND` (1000) and
352        // `MICROSECONDS_PER_SECOND` (1_000_000.0). They are spelled out as
353        // literals, with the scale the `Positive` product carried, rather
354        // than multiplied here: `Positive * Positive` aborts on overflow and
355        // this method has no error channel, while the literals need no
356        // arithmetic at all (#788). `test_periods_per_year_matches_products`
357        // pins each literal to the product of the constants.
358        match self {
359            // 252.0 × 6.5 × 3600.0 × 1_000_000.0 microseconds in a trading year
360            TimeFrame::Microsecond => pos_lit(dec!(5896800000000.0000)),
361            // 252.0 × 6.5 × 3600.0 × 1000 milliseconds in a trading year
362            TimeFrame::Millisecond => pos_lit(dec!(5896800000.000)),
363            // 252.0 × 6.5 × 3600.0 seconds in a trading year
364            TimeFrame::Second => pos_lit(dec!(5896800.000)),
365            // 252.0 × 6.5 × 60 minutes in a trading year
366            TimeFrame::Minute => pos_lit(dec!(98280.00)),
367            // 252.0 × 6.5 hours in a trading year
368            TimeFrame::Hour => pos_lit(dec!(1638.00)),
369            TimeFrame::Day => *TRADING_DAYS, // Trading days in a year
370            TimeFrame::Week => *WEEKS_PER_YEAR, // Weeks in a year
371            TimeFrame::Month => *MONTHS_PER_YEAR, // Months in a year
372            TimeFrame::Quarter => QUARTERS_PER_YEAR, // Quarters in a year
373            TimeFrame::Year => Positive::ONE, // Base unit
374            TimeFrame::Custom(periods) => *periods, // Custom periods per year
375        }
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
397/// Returns the number of units per year for each TimeFrame.
398///
399/// # Arguments
400///
401/// * `time_frame` - The TimeFrame to get the units per year for
402///
403/// # Returns
404///
405/// A `Positive` representing how many of the given time frame fit in a year
406fn pos_lit(d: rust_decimal::Decimal) -> Positive {
407    Positive::new_decimal(d).unwrap_or(Positive::ZERO)
408}
409
410/// Returns how many units of the given `TimeFrame` fit into a calendar year
411/// as a `Positive`. Used by annualisation helpers (volatility, yield
412/// curves) to scale per-period values to a standardised annual basis.
413#[must_use]
414pub fn units_per_year(time_frame: &TimeFrame) -> Positive {
415    match time_frame {
416        TimeFrame::Microsecond => pos_lit(dec!(31536000000000.0)), // 365 * 24 * 60 * 60 * 1_000_000
417        TimeFrame::Millisecond => pos_lit(dec!(31536000000.0)),    // 365 * 24 * 60 * 60 * 1_000
418        TimeFrame::Second => pos_lit(dec!(31536000.0)),            // 365 * 24 * 60 * 60
419        TimeFrame::Minute => pos_lit(dec!(525600.0)),              // 365 * 24 * 60
420        TimeFrame::Hour => pos_lit(dec!(8760.0)),                  // 365 * 24
421        TimeFrame::Day => pos_lit(dec!(365.0)),                    // 365
422        // 365 / 7: the exact `Decimal` quotient `dec!(365.0) / dec!(7.0)`
423        // (28 significant digits, scale 27), spelled out so that no division
424        // runs here (#788). Exact `Decimal` keeps the round-trip identity
425        // Week→Day→Week (an f64 literal would accumulate ~1 ulp of error and
426        // break the strict assertion in tests like
427        // `test_step_next_with_weeks`). `test_units_per_year_week_is_365_over_7`
428        // pins the literal to the quotient.
429        TimeFrame::Week => pos_lit(dec!(52.142857142857142857142857143)),
430        TimeFrame::Month => pos_lit(dec!(12.0)),  // 12
431        TimeFrame::Quarter => pos_lit(dec!(4.0)), // 4
432        TimeFrame::Year => Positive::ONE,         // 1
433        TimeFrame::Custom(periods) => *periods,   // Custom periods per year
434    }
435}
436
437/// Converts a value from one TimeFrame to another.
438///
439/// # Arguments
440///
441/// * `value` - The value to convert
442/// * `from_time_frame` - The source TimeFrame
443/// * `to_time_frame` - The target TimeFrame
444///
445/// # Returns
446///
447/// A Decimal representing the converted value
448///
449/// # Errors
450///
451/// Returns [`PositiveError`] when the source frame has no units in a year
452/// (`TimeFrame::Custom(Positive::ZERO)`), so the conversion factor is
453/// undefined, and when the factor or the converted value leaves the
454/// `Positive` range. Both aborted inside the `Positive` operators before
455/// #788.
456///
457/// # Examples
458///
459/// ```
460///
461/// use optionstratlib_core::utils::time::convert_time_frame;
462/// use optionstratlib_core::utils::TimeFrame;
463/// use positive::{pos_or_panic, Positive, assert_pos_relative_eq};
464///
465/// // Convert 60 seconds to minutes
466/// let result = convert_time_frame(pos_or_panic!(60.0), &TimeFrame::Second, &TimeFrame::Minute)?;
467/// assert_pos_relative_eq!(result, Positive::ONE, pos_or_panic!(0.0000001));
468///
469/// // Convert 12 hours to days
470/// let result = convert_time_frame(pos_or_panic!(12.0), &TimeFrame::Hour, &TimeFrame::Day)?;
471/// assert_pos_relative_eq!(result, pos_or_panic!(0.5), pos_or_panic!(0.0000001));
472/// # Ok::<(), positive::PositiveError>(())
473/// ```
474pub fn convert_time_frame(
475    value: Positive,
476    from_time_frame: &TimeFrame,
477    to_time_frame: &TimeFrame,
478) -> Result<Positive, PositiveError> {
479    // If the time frames are the same, return the original value
480    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    // Get the units per year for each time frame
489    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    // Calculate the conversion factor
493    // The conversion factor is the ratio of units per year
494    // For example, to convert from seconds to minutes:
495    // seconds per year / minutes per year = 31536000 / 525600 = 60
496    // So 60 seconds = 1 minute
497    let conversion_factor = to_units_per_year.checked_div(&from_units_per_year)?;
498    // Apply the conversion
499    value.checked_mul(&conversion_factor)
500}
501
502/// Returns tomorrow's date in "dd-mmm-yyyy" format (lowercase).
503///
504/// # Examples
505///
506/// ```
507/// # #[cfg(feature = "clock")] {
508/// use tracing::info;
509/// use optionstratlib_core::utils::time::get_tomorrow_formatted;
510/// let tomorrow = get_tomorrow_formatted()?;
511/// info!("{}", tomorrow); // Output will vary depending on the current date.
512/// # }
513/// # Ok::<(), optionstratlib_core::model::ExpirationDateError>(())
514/// ```
515///
516/// # Errors
517///
518/// Returns [`ExpirationDateError::ArithmeticOverflow`] when today is the last
519/// date the calendar can hold, and [`ExpirationDateError::InvalidDateTime`] in a
520/// build without the `clock` feature; see [`get_x_days_formatted`].
521pub fn get_tomorrow_formatted() -> Result<String, ExpirationDateError> {
522    get_x_days_formatted(1)
523}
524
525/// Formats a date a specified number of days from the current date.
526///
527/// This function calculates the date that is `days` days from the current date and
528/// formats it as a lowercase string in the format "dd-mmm-yyyy".  For example,
529/// if the current date is 2024-11-20 and `days` is 1, the returned string will be
530/// "21-nov-2024".
531///
532/// # Arguments
533///
534/// * `days`: The number of days to offset from the current date.  This can be
535///   positive or negative.
536///
537/// # Returns
538///
539/// A lowercase string representing the calculated date in "dd-mmm-yyyy" format.
540///
541/// # Errors
542///
543/// Returns [`ExpirationDateError::ArithmeticOverflow`] when `days` is beyond
544/// the range of a day count (`TimeDelta::days` aborted on it) or the date
545/// it lands on is outside the calendar (`NaiveDate + TimeDelta` aborted on
546/// it), both before #788, and [`ExpirationDateError::InvalidDateTime`] in a
547/// build without the `clock` feature.
548pub 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
560/// Returns a formatted date string representing the date `x` days in the future.
561///
562/// This function takes a `Positive` number of days, calculates the ceiling value
563/// as an integer, adds that many days to the current local date, and returns the
564/// resulting date formatted in the "dd-MMM-yyyy" format in lowercase.
565///
566/// # Arguments
567///
568/// * `days` - A `Positive` value representing a positive number of days.
569///
570/// # Returns
571///
572/// A `String` containing the formatted date in lowercase. The format of the date
573/// is "dd-MMM-yyyy", where:
574/// - `dd` is the day of the month, zero-padded to 2 digits.
575/// - `MMM` is the three-letter abbreviated name of the month.
576/// - `yyyy` is the 4-digit year.
577///
578/// # Errors
579///
580/// Returns [`ExpirationDateError::ArithmeticOverflow`] when the rounded-up day
581/// count does not fit an `i64`, does not fit a `chrono` day span, or moves the
582/// date outside the calendar range `chrono` can represent. The date is never
583/// replaced by today's: a caller asking for a date `days` ahead either gets
584/// that date or an error. Returns [`ExpirationDateError::InvalidDateTime`] in a
585/// build without the `clock` feature.
586///
587/// # Note
588/// - The function uses the local time zone and the `chrono` crate for date manipulation.
589/// - The `Positive` type is expected to provide a `.ceiling()` method that converts it to an integer-compatible representation.
590pub fn get_x_days_formatted_pos(days: Positive) -> Result<String, ExpirationDateError> {
591    // `checked_ceiling` rather than `ceiling`, which aborts when the result
592    // leaves the `Positive` range (#788).
593    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/// Reports a day offset no calendar date can represent.
606#[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/// Returns the current date formatted as "dd-mmm-yyyy" in lowercase.
615///
616/// # Examples
617///
618/// ```
619/// # #[cfg(feature = "clock")] {
620/// use chrono::Local;
621/// use optionstratlib_core::utils::time::get_today_formatted;
622///
623/// let today_formatted = get_today_formatted();
624/// let expected_format = Local::now().date_naive().format("%d-%b-%Y").to_string().to_lowercase();
625/// assert_eq!(today_formatted, expected_format);
626/// # }
627/// ```
628#[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/// UTC time of day after which [`get_today_or_tomorrow_formatted`] rolls
636/// over to the next date.
637///
638/// `NaiveTime::from_hms_opt` is a `const fn`, so the `match` is evaluated by
639/// the compiler: a literal outside chrono's ranges would fail the build with
640/// `evaluation of constant value failed`, and no run time reaches the `None`
641/// arm (#788), as with `DEFAULT_BINOMIAL_STEPS`.
642#[cfg(feature = "clock")]
643const TODAY_CUTOFF: NaiveTime = match NaiveTime::from_hms_opt(18, 30, 0) {
644    Some(t) => t,
645    None => unreachable!(), // scan-banned: allow -- `const` context: an unreachable arm here fails compilation, it cannot abort at run time
646};
647
648/// Formats the current date or the next day's date based on the current UTC time.
649///
650/// The function checks the current UTC time against a cutoff time of 18:30:00.
651/// If the current time is past the cutoff, the date for the next day is returned.
652/// Otherwise, the current date is returned.  The returned date is formatted
653/// as `dd-mmm-yyyy` in lowercase. Note that getting the next day is done safely,
654/// handling potential overflow (e.g. the last day of the year).
655///
656/// Returns:
657///
658/// A lowercase String representing the formatted date.
659///
660/// # Examples
661///
662/// ```
663/// # #[cfg(feature = "clock")] {
664/// use chrono::{Utc, NaiveTime, Timelike};
665/// use tracing::info;
666/// use optionstratlib_core::utils::time::get_today_or_tomorrow_formatted;
667///
668/// info!("{}", get_today_or_tomorrow_formatted());
669/// # }
670/// ```
671#[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    // Get the date we should use based on current UTC time
677    let target_date = if now.time() > cutoff_time {
678        now.date_naive()
679            .succ_opt()
680            .unwrap_or_else(|| now.date_naive()) // Get next day safely
681    } 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    // The literals replaced `Positive` products (#788); they must carry the
729    // same value and the same scale, so the comparison is on the decimal
730    // representation, not only on numeric equality.
731    #[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        // Test that higher timeframes have fewer periods
791        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        // Test specific conversion ratios between timeframes
807        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        // Verify relationships with trading days
829        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        // Test edge cases for custom periods
845        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        // Approximately 0.92 months (4 weeks / 4.33 weeks per month)
957        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        // A billion days is a valid `chrono` span but no `NaiveDate` holds it.
1003        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    // `to / from` with a zero `from` aborted with `Positive invariant broken
1026    // in div: result would be non-positive` (#788).
1027    #[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    // `value * factor` aborted with `Positive arithmetic overflow in mul`
1038    // (#788).
1039    #[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    // `Duration::days(i64::MAX)` aborted with `TimeDelta::days out of
1057    // bounds` (#788).
1058    #[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    // `NaiveDate + TimeDelta` aborted with `` `NaiveDate + TimeDelta`
1068    // overflowed `` a billion days out (#788).
1069    #[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    /// `years_to_expiration` answers a `Days` without a clock: the answer must
1105    /// be what `get_years_at` gives, whatever the base, in every build.
1106    #[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    /// With the clock on the helpers give exactly what the `expiration_date`
1145    /// methods give, so a default build is numerically unchanged.
1146    #[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            // The two reads are moments apart, so the comparison leaves room for
1172            // them, and the half day keeps a whole-day count from flipping between.
1173            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        /// The carrier takes the reference to a thread that does not have it, and
1214        /// leaves that thread as it was.
1215        #[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        /// A carrier captured with no reference clears the one on the worker: the
1247        /// caller's state decides, not what an earlier task left.
1248        #[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        /// The caller's instant wins over the reference datetime: `now_or` only
1267        /// ever gives back what it was handed.
1268        #[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        /// A `DateTime` is measured from the reference, so moving the reference
1277        /// moves the answer: the difference from a build with the clock.
1278        #[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}