1use chrono::{Datelike, Duration, NaiveDate, NaiveTime, Weekday};
7use rand::prelude::*;
8use rand_chacha::ChaCha8Rng;
9use serde::{Deserialize, Serialize};
10
11use super::holidays::HolidayCalendar;
12use super::seasonality::IndustrySeasonality;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SeasonalityConfig {
17 pub month_end_spike: bool,
19 pub month_end_multiplier: f64,
21 pub month_end_lead_days: u32,
23
24 pub quarter_end_spike: bool,
26 pub quarter_end_multiplier: f64,
28
29 pub year_end_spike: bool,
31 pub year_end_multiplier: f64,
33
34 pub weekend_activity: f64,
36 pub holiday_activity: f64,
38
39 pub day_of_week_patterns: bool,
41 pub monday_multiplier: f64,
43 pub tuesday_multiplier: f64,
45 pub wednesday_multiplier: f64,
47 pub thursday_multiplier: f64,
49 pub friday_multiplier: f64,
51}
52
53impl Default for SeasonalityConfig {
54 fn default() -> Self {
55 Self {
56 month_end_spike: true,
57 month_end_multiplier: 2.5,
58 month_end_lead_days: 5,
59 quarter_end_spike: true,
60 quarter_end_multiplier: 4.0,
61 year_end_spike: true,
62 year_end_multiplier: 6.0,
63 weekend_activity: 0.1,
64 holiday_activity: 0.05,
65 day_of_week_patterns: true,
67 monday_multiplier: 1.3, tuesday_multiplier: 1.1, wednesday_multiplier: 1.0, thursday_multiplier: 1.0, friday_multiplier: 0.85, }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct WorkingHoursConfig {
79 pub day_start: u8,
81 pub day_end: u8,
83 pub peak_hours: Vec<u8>,
85 pub peak_weight: f64,
87 pub after_hours_probability: f64,
89}
90
91impl Default for WorkingHoursConfig {
92 fn default() -> Self {
93 Self {
94 day_start: 8,
95 day_end: 18,
96 peak_hours: vec![9, 10, 11, 14, 15, 16],
97 peak_weight: 1.5,
98 after_hours_probability: 0.05,
99 }
100 }
101}
102
103pub struct TemporalSampler {
105 rng: ChaCha8Rng,
106 seasonality_config: SeasonalityConfig,
107 working_hours_config: WorkingHoursConfig,
108 holidays: Vec<NaiveDate>,
110 industry_seasonality: Option<IndustrySeasonality>,
112 holiday_calendar: Option<HolidayCalendar>,
114}
115
116impl TemporalSampler {
117 pub fn new(seed: u64) -> Self {
119 Self::with_config(
120 seed,
121 SeasonalityConfig::default(),
122 WorkingHoursConfig::default(),
123 Vec::new(),
124 )
125 }
126
127 pub fn with_config(
129 seed: u64,
130 seasonality_config: SeasonalityConfig,
131 working_hours_config: WorkingHoursConfig,
132 holidays: Vec<NaiveDate>,
133 ) -> Self {
134 Self {
135 rng: ChaCha8Rng::seed_from_u64(seed),
136 seasonality_config,
137 working_hours_config,
138 holidays,
139 industry_seasonality: None,
140 holiday_calendar: None,
141 }
142 }
143
144 #[allow(clippy::too_many_arguments)]
146 pub fn with_full_config(
147 seed: u64,
148 seasonality_config: SeasonalityConfig,
149 working_hours_config: WorkingHoursConfig,
150 holidays: Vec<NaiveDate>,
151 industry_seasonality: Option<IndustrySeasonality>,
152 holiday_calendar: Option<HolidayCalendar>,
153 ) -> Self {
154 Self {
155 rng: ChaCha8Rng::seed_from_u64(seed),
156 seasonality_config,
157 working_hours_config,
158 holidays,
159 industry_seasonality,
160 holiday_calendar,
161 }
162 }
163
164 pub fn with_industry_seasonality(mut self, seasonality: IndustrySeasonality) -> Self {
166 self.industry_seasonality = Some(seasonality);
167 self
168 }
169
170 pub fn with_holiday_calendar(mut self, calendar: HolidayCalendar) -> Self {
172 self.holiday_calendar = Some(calendar);
173 self
174 }
175
176 pub fn set_industry_seasonality(&mut self, seasonality: IndustrySeasonality) {
178 self.industry_seasonality = Some(seasonality);
179 }
180
181 pub fn set_holiday_calendar(&mut self, calendar: HolidayCalendar) {
183 self.holiday_calendar = Some(calendar);
184 }
185
186 pub fn industry_seasonality(&self) -> Option<&IndustrySeasonality> {
188 self.industry_seasonality.as_ref()
189 }
190
191 pub fn holiday_calendar(&self) -> Option<&HolidayCalendar> {
193 self.holiday_calendar.as_ref()
194 }
195
196 pub fn generate_us_holidays(year: i32) -> Vec<NaiveDate> {
198 let mut holidays = Vec::new();
199
200 holidays.push(NaiveDate::from_ymd_opt(year, 1, 1).unwrap());
202 holidays.push(NaiveDate::from_ymd_opt(year, 7, 4).unwrap());
204 holidays.push(NaiveDate::from_ymd_opt(year, 12, 25).unwrap());
206 let first_thursday = (1..=7)
208 .map(|d| NaiveDate::from_ymd_opt(year, 11, d).unwrap())
209 .find(|d| d.weekday() == Weekday::Thu)
210 .unwrap();
211 let thanksgiving = first_thursday + Duration::weeks(3);
212 holidays.push(thanksgiving);
213
214 holidays
215 }
216
217 pub fn is_weekend(&self, date: NaiveDate) -> bool {
219 matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
220 }
221
222 pub fn get_day_of_week_multiplier(&self, date: NaiveDate) -> f64 {
231 if !self.seasonality_config.day_of_week_patterns {
232 return 1.0;
233 }
234
235 match date.weekday() {
236 Weekday::Mon => self.seasonality_config.monday_multiplier,
237 Weekday::Tue => self.seasonality_config.tuesday_multiplier,
238 Weekday::Wed => self.seasonality_config.wednesday_multiplier,
239 Weekday::Thu => self.seasonality_config.thursday_multiplier,
240 Weekday::Fri => self.seasonality_config.friday_multiplier,
241 Weekday::Sat | Weekday::Sun => 1.0, }
243 }
244
245 pub fn is_holiday(&self, date: NaiveDate) -> bool {
247 if self.holidays.contains(&date) {
249 return true;
250 }
251
252 if let Some(ref calendar) = self.holiday_calendar {
254 if calendar.is_holiday(date) {
255 return true;
256 }
257 }
258
259 false
260 }
261
262 fn get_holiday_multiplier(&self, date: NaiveDate) -> f64 {
264 if let Some(ref calendar) = self.holiday_calendar {
266 let mult = calendar.get_multiplier(date);
267 if mult < 1.0 {
268 return mult;
269 }
270 }
271
272 if self.holidays.contains(&date) {
274 return self.seasonality_config.holiday_activity;
275 }
276
277 1.0
278 }
279
280 pub fn is_month_end(&self, date: NaiveDate) -> bool {
282 let last_day = Self::last_day_of_month(date);
283 let days_until_end = (last_day - date).num_days();
284 days_until_end >= 0 && days_until_end < self.seasonality_config.month_end_lead_days as i64
285 }
286
287 pub fn is_quarter_end(&self, date: NaiveDate) -> bool {
289 let month = date.month();
290 let is_quarter_end_month = matches!(month, 3 | 6 | 9 | 12);
291 is_quarter_end_month && self.is_month_end(date)
292 }
293
294 pub fn is_year_end(&self, date: NaiveDate) -> bool {
296 date.month() == 12 && self.is_month_end(date)
297 }
298
299 pub fn last_day_of_month(date: NaiveDate) -> NaiveDate {
301 let year = date.year();
302 let month = date.month();
303
304 if month == 12 {
305 NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() - Duration::days(1)
306 } else {
307 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() - Duration::days(1)
308 }
309 }
310
311 pub fn get_date_multiplier(&self, date: NaiveDate) -> f64 {
320 let mut multiplier = 1.0;
321
322 if self.is_weekend(date) {
324 multiplier *= self.seasonality_config.weekend_activity;
325 } else {
326 multiplier *= self.get_day_of_week_multiplier(date);
328 }
329
330 let holiday_mult = self.get_holiday_multiplier(date);
332 if holiday_mult < 1.0 {
333 multiplier *= holiday_mult;
334 }
335
336 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
338 multiplier *= self.seasonality_config.year_end_multiplier;
339 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
340 multiplier *= self.seasonality_config.quarter_end_multiplier;
341 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
342 multiplier *= self.seasonality_config.month_end_multiplier;
343 }
344
345 if let Some(ref industry) = self.industry_seasonality {
347 let industry_mult = industry.get_multiplier(date);
348 multiplier *= industry_mult;
351 }
352
353 multiplier
354 }
355
356 pub fn get_base_date_multiplier(&self, date: NaiveDate) -> f64 {
358 let mut multiplier = 1.0;
359
360 if self.is_weekend(date) {
361 multiplier *= self.seasonality_config.weekend_activity;
362 } else {
363 multiplier *= self.get_day_of_week_multiplier(date);
365 }
366
367 let holiday_mult = self.get_holiday_multiplier(date);
368 if holiday_mult < 1.0 {
369 multiplier *= holiday_mult;
370 }
371
372 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
373 multiplier *= self.seasonality_config.year_end_multiplier;
374 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
375 multiplier *= self.seasonality_config.quarter_end_multiplier;
376 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
377 multiplier *= self.seasonality_config.month_end_multiplier;
378 }
379
380 multiplier
381 }
382
383 pub fn get_industry_multiplier(&self, date: NaiveDate) -> f64 {
385 self.industry_seasonality
386 .as_ref()
387 .map(|s| s.get_multiplier(date))
388 .unwrap_or(1.0)
389 }
390
391 pub fn sample_date(&mut self, start: NaiveDate, end: NaiveDate) -> NaiveDate {
393 let days = (end - start).num_days() as usize;
394 if days == 0 {
395 return start;
396 }
397
398 let mut weights: Vec<f64> = (0..=days)
400 .map(|d| {
401 let date = start + Duration::days(d as i64);
402 self.get_date_multiplier(date)
403 })
404 .collect();
405
406 let total: f64 = weights.iter().sum();
408 weights.iter_mut().for_each(|w| *w /= total);
409
410 let p: f64 = self.rng.gen();
412 let mut cumulative = 0.0;
413 for (i, weight) in weights.iter().enumerate() {
414 cumulative += weight;
415 if p < cumulative {
416 return start + Duration::days(i as i64);
417 }
418 }
419
420 end
421 }
422
423 pub fn sample_time(&mut self, is_human: bool) -> NaiveTime {
425 if !is_human {
426 let hour = if self.rng.gen::<f64>() < 0.7 {
428 self.rng.gen_range(22..=23).clamp(0, 23)
430 + if self.rng.gen_bool(0.5) {
431 0
432 } else {
433 self.rng.gen_range(0..=5)
434 }
435 } else {
436 self.rng.gen_range(0..24)
437 };
438 let minute = self.rng.gen_range(0..60);
439 let second = self.rng.gen_range(0..60);
440 return NaiveTime::from_hms_opt(hour.clamp(0, 23) as u32, minute, second).unwrap();
441 }
442
443 let hour = if self.rng.gen::<f64>() < self.working_hours_config.after_hours_probability {
445 if self.rng.gen_bool(0.5) {
447 self.rng.gen_range(6..self.working_hours_config.day_start)
448 } else {
449 self.rng.gen_range(self.working_hours_config.day_end..22)
450 }
451 } else {
452 let is_peak = self.rng.gen::<f64>() < 0.6; if is_peak && !self.working_hours_config.peak_hours.is_empty() {
455 *self
456 .working_hours_config
457 .peak_hours
458 .choose(&mut self.rng)
459 .unwrap()
460 } else {
461 self.rng.gen_range(
462 self.working_hours_config.day_start..self.working_hours_config.day_end,
463 )
464 }
465 };
466
467 let minute = self.rng.gen_range(0..60);
468 let second = self.rng.gen_range(0..60);
469
470 NaiveTime::from_hms_opt(hour as u32, minute, second).unwrap()
471 }
472
473 pub fn expected_count_for_date(&self, date: NaiveDate, daily_average: f64) -> u64 {
475 let multiplier = self.get_date_multiplier(date);
476 (daily_average * multiplier).round() as u64
477 }
478
479 pub fn reset(&mut self, seed: u64) {
481 self.rng = ChaCha8Rng::seed_from_u64(seed);
482 }
483}
484
485#[derive(Debug, Clone)]
487pub struct TimePeriod {
488 pub start_date: NaiveDate,
490 pub end_date: NaiveDate,
492 pub fiscal_year: u16,
494 pub fiscal_periods: Vec<u8>,
496}
497
498impl TimePeriod {
499 pub fn fiscal_year(year: u16) -> Self {
501 Self {
502 start_date: NaiveDate::from_ymd_opt(year as i32, 1, 1).unwrap(),
503 end_date: NaiveDate::from_ymd_opt(year as i32, 12, 31).unwrap(),
504 fiscal_year: year,
505 fiscal_periods: (1..=12).collect(),
506 }
507 }
508
509 pub fn months(year: u16, start_month: u8, num_months: u8) -> Self {
511 let start_date = NaiveDate::from_ymd_opt(year as i32, start_month as u32, 1).unwrap();
512 let end_month = ((start_month - 1 + num_months - 1) % 12) + 1;
513 let end_year = year + (start_month as u16 - 1 + num_months as u16 - 1) / 12;
514 let end_date = TemporalSampler::last_day_of_month(
515 NaiveDate::from_ymd_opt(end_year as i32, end_month as u32, 1).unwrap(),
516 );
517
518 Self {
519 start_date,
520 end_date,
521 fiscal_year: year,
522 fiscal_periods: (start_month..start_month + num_months).collect(),
523 }
524 }
525
526 pub fn total_days(&self) -> i64 {
528 (self.end_date - self.start_date).num_days() + 1
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use chrono::Timelike;
536
537 #[test]
538 fn test_is_weekend() {
539 let sampler = TemporalSampler::new(42);
540 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
541 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
542 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
543
544 assert!(sampler.is_weekend(saturday));
545 assert!(sampler.is_weekend(sunday));
546 assert!(!sampler.is_weekend(monday));
547 }
548
549 #[test]
550 fn test_is_month_end() {
551 let sampler = TemporalSampler::new(42);
552 let month_end = NaiveDate::from_ymd_opt(2024, 6, 28).unwrap();
553 let month_start = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
554
555 assert!(sampler.is_month_end(month_end));
556 assert!(!sampler.is_month_end(month_start));
557 }
558
559 #[test]
560 fn test_date_multiplier() {
561 let sampler = TemporalSampler::new(42);
562
563 let regular_day = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap(); assert!((sampler.get_date_multiplier(regular_day) - 1.0).abs() < 0.01);
566
567 let weekend = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(); assert!(sampler.get_date_multiplier(weekend) < 0.2);
570
571 let month_end = NaiveDate::from_ymd_opt(2024, 6, 28).unwrap();
573 assert!(sampler.get_date_multiplier(month_end) > 2.0);
574 }
575
576 #[test]
577 fn test_day_of_week_patterns() {
578 let sampler = TemporalSampler::new(42);
579
580 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
582 let tuesday = NaiveDate::from_ymd_opt(2024, 6, 11).unwrap();
583 let wednesday = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap();
584 let thursday = NaiveDate::from_ymd_opt(2024, 6, 13).unwrap();
585 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
586
587 let mon_mult = sampler.get_day_of_week_multiplier(monday);
589 assert!((mon_mult - 1.3).abs() < 0.01);
590
591 let tue_mult = sampler.get_day_of_week_multiplier(tuesday);
593 assert!((tue_mult - 1.1).abs() < 0.01);
594
595 let wed_mult = sampler.get_day_of_week_multiplier(wednesday);
597 let thu_mult = sampler.get_day_of_week_multiplier(thursday);
598 assert!((wed_mult - 1.0).abs() < 0.01);
599 assert!((thu_mult - 1.0).abs() < 0.01);
600
601 let fri_mult = sampler.get_day_of_week_multiplier(friday);
603 assert!((fri_mult - 0.85).abs() < 0.01);
604
605 assert!(sampler.get_date_multiplier(monday) > sampler.get_date_multiplier(friday));
608 }
609
610 #[test]
611 fn test_sample_time_human() {
612 let mut sampler = TemporalSampler::new(42);
613
614 for _ in 0..100 {
615 let time = sampler.sample_time(true);
616 let hour = time.hour();
618 assert!(hour < 24);
620 }
621 }
622
623 #[test]
624 fn test_time_period() {
625 let period = TimePeriod::fiscal_year(2024);
626 assert_eq!(period.total_days(), 366); let partial = TimePeriod::months(2024, 1, 6);
629 assert!(partial.total_days() > 180);
630 assert!(partial.total_days() < 185);
631 }
632}