use chrono::{Datelike, Duration, NaiveDate};
use rand::{Rng, RngExt};
use std::sync::Arc;
use super::business_day::BusinessDayCalculator;
use super::holidays::{HolidayCalendar, Region};
#[derive(Debug, Clone)]
pub struct TemporalContext {
region: Region,
start_date: NaiveDate,
end_date: NaiveDate,
calculator: BusinessDayCalculator,
}
impl TemporalContext {
pub fn new(region: Region, start_date: NaiveDate, end_date: NaiveDate) -> Self {
let start_year = start_date.year();
let end_year = end_date.year() + 1;
let mut merged = HolidayCalendar::new(region, start_year);
for year in start_year..=end_year {
let yearly = HolidayCalendar::for_region(region, year);
for holiday in yearly.holidays {
merged.add_holiday(holiday);
}
}
let calculator = BusinessDayCalculator::new(merged);
Self {
region,
start_date,
end_date,
calculator,
}
}
pub fn shared(region: Region, start_date: NaiveDate, end_date: NaiveDate) -> Arc<Self> {
Arc::new(Self::new(region, start_date, end_date))
}
pub fn region(&self) -> Region {
self.region
}
pub fn start_date(&self) -> NaiveDate {
self.start_date
}
pub fn end_date(&self) -> NaiveDate {
self.end_date
}
pub fn calculator(&self) -> &BusinessDayCalculator {
&self.calculator
}
pub fn is_business_day(&self, date: NaiveDate) -> bool {
self.calculator.is_business_day(date)
}
pub fn adjust_to_business_day(&self, date: NaiveDate) -> NaiveDate {
self.calculator.next_business_day(date, true)
}
pub fn adjust_to_previous_business_day(&self, date: NaiveDate) -> NaiveDate {
self.calculator.prev_business_day(date, true)
}
pub fn sample_business_day_in_range<R: Rng + ?Sized>(
&self,
rng: &mut R,
start: NaiveDate,
end: NaiveDate,
) -> NaiveDate {
let span_days = (end - start).num_days().max(0) as u32;
let raw_offset = rng.random_range(0..=span_days) as i64;
let raw_date = start + Duration::days(raw_offset);
let snapped = self.adjust_to_business_day(raw_date);
if snapped > end {
self.adjust_to_previous_business_day(end)
} else {
snapped
}
}
}
pub fn parse_region_code(code: &str) -> Region {
match code.to_uppercase().as_str() {
"US" => Region::US,
"DE" => Region::DE,
"GB" | "UK" => Region::GB,
"FR" => Region::FR,
"IT" => Region::IT,
"ES" => Region::ES,
"CA" => Region::CA,
"CN" => Region::CN,
"JP" => Region::JP,
"IN" => Region::IN,
"BR" => Region::BR,
"MX" => Region::MX,
"AU" => Region::AU,
"SG" => Region::SG,
"KR" => Region::KR,
_ => Region::US,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use chrono::Weekday;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
fn d(y: i32, m: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, day).unwrap()
}
#[test]
fn weekend_is_not_business_day() {
let ctx = TemporalContext::new(Region::US, d(2024, 1, 1), d(2024, 12, 31));
assert_eq!(d(2024, 1, 6).weekday(), Weekday::Sat);
assert!(!ctx.is_business_day(d(2024, 1, 6)));
assert!(ctx.is_business_day(d(2024, 1, 8)));
}
#[test]
fn adjust_snaps_weekend_forward() {
let ctx = TemporalContext::new(Region::US, d(2024, 1, 1), d(2024, 12, 31));
let adjusted = ctx.adjust_to_business_day(d(2024, 1, 6));
assert_eq!(adjusted, d(2024, 1, 8));
}
#[test]
fn sample_never_returns_weekend() {
let ctx = TemporalContext::new(Region::US, d(2024, 1, 1), d(2024, 12, 31));
let mut rng = ChaCha8Rng::seed_from_u64(42);
for _ in 0..1000 {
let date = ctx.sample_business_day_in_range(&mut rng, d(2024, 1, 1), d(2024, 12, 31));
assert!(
ctx.is_business_day(date),
"sampled non-business day: {date}"
);
}
}
#[test]
fn multi_year_span_includes_both_years() {
let ctx = TemporalContext::new(Region::US, d(2024, 1, 1), d(2025, 12, 31));
assert!(!ctx.is_business_day(d(2024, 7, 4)));
assert!(!ctx.is_business_day(d(2025, 7, 4)));
}
#[test]
fn parse_region_code_fallback() {
assert_eq!(parse_region_code("US"), Region::US);
assert_eq!(parse_region_code("de"), Region::DE);
assert_eq!(parse_region_code("UK"), Region::GB);
assert_eq!(parse_region_code("ZZ"), Region::US); }
}