use crate::constants::*;
use chrono::{Duration, Local, NaiveTime, Utc};
use positive::Positive;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::fmt;
use utoipa::ToSchema;
#[cfg(test)]
use positive::pos_or_panic;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, 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 {
let trading_days = *TRADING_DAYS;
let trading_hours = *TRADING_HOURS;
let seconds_per_hour = *SECONDS_PER_HOUR;
let microseconds_per_second = *MICROSECONDS_PER_SECOND;
let weeks_per_year = *WEEKS_PER_YEAR;
let months_per_year = *MONTHS_PER_YEAR;
match self {
TimeFrame::Microsecond => {
trading_days * trading_hours * seconds_per_hour * microseconds_per_second
} TimeFrame::Millisecond => {
trading_days * trading_hours * seconds_per_hour * MILLISECONDS_PER_SECOND
} TimeFrame::Second => trading_days * trading_hours * seconds_per_hour, TimeFrame::Minute => trading_days * trading_hours * MINUTES_PER_HOUR, TimeFrame::Hour => trading_days * trading_hours, 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 => match Positive::new_decimal(dec!(365.0) / dec!(7.0)) {
Ok(v) => v,
Err(_) => unreachable!("365/7 is structurally positive non-zero"),
},
TimeFrame::Month => pos_lit(dec!(12.0)), TimeFrame::Quarter => pos_lit(dec!(4.0)), TimeFrame::Year => Positive::ONE, TimeFrame::Custom(periods) => *periods, }
}
#[must_use]
pub fn convert_time_frame(
value: Positive,
from_time_frame: &TimeFrame,
to_time_frame: &TimeFrame,
) -> Positive {
if from_time_frame == to_time_frame {
return value;
}
if value.is_zero() {
return 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 / from_units_per_year;
value * conversion_factor
}
#[must_use]
pub fn get_tomorrow_formatted() -> String {
let tomorrow = Local::now().date_naive() + Duration::days(1);
tomorrow.format("%d-%b-%Y").to_string().to_lowercase()
}
#[must_use]
pub fn get_x_days_formatted(days: i64) -> String {
let tomorrow = Local::now().date_naive() + Duration::days(days);
tomorrow.format("%d-%b-%Y").to_string().to_lowercase()
}
#[must_use]
pub fn get_x_days_formatted_pos(days: Positive) -> String {
let ceiling = days.ceiling().to_i64();
let tomorrow = Local::now().date_naive() + Duration::days(ceiling);
tomorrow.format("%d-%b-%Y").to_string().to_lowercase()
}
#[must_use]
pub fn get_today_formatted() -> String {
let today = Local::now().date_naive();
today.format("%d-%b-%Y").to_string().to_lowercase()
}
#[must_use]
pub fn get_today_or_tomorrow_formatted() -> String {
let cutoff_time = match NaiveTime::from_hms_opt(18, 30, 0) {
Some(t) => t,
None => unreachable!("18:30:00 is always a valid NaiveTime"),
};
let now = Utc::now();
let target_date = if now.time() > cutoff_time {
now.date_naive().succ_opt().unwrap_or(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_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::INFINITY).periods_per_year(),
Positive::INFINITY
);
}
#[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);
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);
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);
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);
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);
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);
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,
);
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)),
);
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);
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);
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,
);
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);
assert_pos_relative_eq!(result, Positive::ZERO, pos_or_panic!(1e-10));
}
}