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::period_end::PeriodEndDynamics;
13use super::seasonality::IndustrySeasonality;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SeasonalityConfig {
18 pub month_end_spike: bool,
20 pub month_end_multiplier: f64,
22 pub month_end_lead_days: u32,
24
25 pub quarter_end_spike: bool,
27 pub quarter_end_multiplier: f64,
29
30 pub year_end_spike: bool,
32 pub year_end_multiplier: f64,
34
35 pub weekend_activity: f64,
37 pub holiday_activity: f64,
39
40 pub day_of_week_patterns: bool,
42 pub monday_multiplier: f64,
44 pub tuesday_multiplier: f64,
46 pub wednesday_multiplier: f64,
48 pub thursday_multiplier: f64,
50 pub friday_multiplier: f64,
52}
53
54impl Default for SeasonalityConfig {
55 fn default() -> Self {
56 Self {
57 month_end_spike: true,
58 month_end_multiplier: 2.5,
59 month_end_lead_days: 5,
60 quarter_end_spike: true,
61 quarter_end_multiplier: 4.0,
62 year_end_spike: true,
63 year_end_multiplier: 6.0,
64 weekend_activity: 0.1,
65 holiday_activity: 0.05,
66 day_of_week_patterns: true,
68 monday_multiplier: 1.3, tuesday_multiplier: 1.1, wednesday_multiplier: 1.0, thursday_multiplier: 1.0, friday_multiplier: 0.85, }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct WorkingHoursConfig {
80 pub day_start: u8,
82 pub day_end: u8,
84 pub peak_hours: Vec<u8>,
86 pub peak_weight: f64,
88 pub after_hours_probability: f64,
90}
91
92impl Default for WorkingHoursConfig {
93 fn default() -> Self {
94 Self {
95 day_start: 8,
96 day_end: 18,
97 peak_hours: vec![9, 10, 11, 14, 15, 16],
98 peak_weight: 1.5,
99 after_hours_probability: 0.05,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct IntraDayPatterns {
110 pub enabled: bool,
112 pub segments: Vec<IntraDaySegment>,
114}
115
116impl Default for IntraDayPatterns {
117 fn default() -> Self {
118 Self {
119 enabled: true,
120 segments: vec![
121 IntraDaySegment {
122 name: "morning_spike".to_string(),
123 start: NaiveTime::from_hms_opt(8, 30, 0).expect("valid date/time components"),
124 end: NaiveTime::from_hms_opt(10, 0, 0).expect("valid date/time components"),
125 multiplier: 1.8,
126 posting_type: PostingType::Both,
127 },
128 IntraDaySegment {
129 name: "mid_morning".to_string(),
130 start: NaiveTime::from_hms_opt(10, 0, 0).expect("valid date/time components"),
131 end: NaiveTime::from_hms_opt(12, 0, 0).expect("valid date/time components"),
132 multiplier: 1.2,
133 posting_type: PostingType::Both,
134 },
135 IntraDaySegment {
136 name: "lunch_dip".to_string(),
137 start: NaiveTime::from_hms_opt(12, 0, 0).expect("valid date/time components"),
138 end: NaiveTime::from_hms_opt(13, 30, 0).expect("valid date/time components"),
139 multiplier: 0.4,
140 posting_type: PostingType::Human,
141 },
142 IntraDaySegment {
143 name: "afternoon".to_string(),
144 start: NaiveTime::from_hms_opt(13, 30, 0).expect("valid date/time components"),
145 end: NaiveTime::from_hms_opt(16, 0, 0).expect("valid date/time components"),
146 multiplier: 1.1,
147 posting_type: PostingType::Both,
148 },
149 IntraDaySegment {
150 name: "eod_rush".to_string(),
151 start: NaiveTime::from_hms_opt(16, 0, 0).expect("valid date/time components"),
152 end: NaiveTime::from_hms_opt(17, 30, 0).expect("valid date/time components"),
153 multiplier: 1.5,
154 posting_type: PostingType::Both,
155 },
156 ],
157 }
158 }
159}
160
161impl IntraDayPatterns {
162 pub fn disabled() -> Self {
164 Self {
165 enabled: false,
166 segments: Vec::new(),
167 }
168 }
169
170 pub fn with_segments(segments: Vec<IntraDaySegment>) -> Self {
172 Self {
173 enabled: true,
174 segments,
175 }
176 }
177
178 pub fn get_multiplier(&self, time: NaiveTime, is_human: bool) -> f64 {
180 if !self.enabled {
181 return 1.0;
182 }
183
184 for segment in &self.segments {
185 if time >= segment.start && time < segment.end {
186 let applies = match segment.posting_type {
188 PostingType::Human => is_human,
189 PostingType::System => !is_human,
190 PostingType::Both => true,
191 };
192 if applies {
193 return segment.multiplier;
194 }
195 }
196 }
197
198 1.0 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct IntraDaySegment {
205 pub name: String,
207 pub start: NaiveTime,
209 pub end: NaiveTime,
211 pub multiplier: f64,
213 pub posting_type: PostingType,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum PostingType {
221 Human,
223 System,
225 Both,
227}
228
229pub struct TemporalSampler {
231 rng: ChaCha8Rng,
232 seasonality_config: SeasonalityConfig,
233 working_hours_config: WorkingHoursConfig,
234 holidays: Vec<NaiveDate>,
236 industry_seasonality: Option<IndustrySeasonality>,
238 holiday_calendar: Option<HolidayCalendar>,
240 period_end_dynamics: Option<PeriodEndDynamics>,
242 use_period_end_dynamics: bool,
244 intra_day_patterns: Option<IntraDayPatterns>,
246}
247
248impl TemporalSampler {
249 pub fn new(seed: u64) -> Self {
251 Self::with_config(
252 seed,
253 SeasonalityConfig::default(),
254 WorkingHoursConfig::default(),
255 Vec::new(),
256 )
257 }
258
259 pub fn with_config(
261 seed: u64,
262 seasonality_config: SeasonalityConfig,
263 working_hours_config: WorkingHoursConfig,
264 holidays: Vec<NaiveDate>,
265 ) -> Self {
266 Self {
267 rng: ChaCha8Rng::seed_from_u64(seed),
268 seasonality_config,
269 working_hours_config,
270 holidays,
271 industry_seasonality: None,
272 holiday_calendar: None,
273 period_end_dynamics: None,
274 use_period_end_dynamics: false,
275 intra_day_patterns: None,
276 }
277 }
278
279 #[allow(clippy::too_many_arguments)]
281 pub fn with_full_config(
282 seed: u64,
283 seasonality_config: SeasonalityConfig,
284 working_hours_config: WorkingHoursConfig,
285 holidays: Vec<NaiveDate>,
286 industry_seasonality: Option<IndustrySeasonality>,
287 holiday_calendar: Option<HolidayCalendar>,
288 ) -> Self {
289 Self {
290 rng: ChaCha8Rng::seed_from_u64(seed),
291 seasonality_config,
292 working_hours_config,
293 holidays,
294 industry_seasonality,
295 holiday_calendar,
296 period_end_dynamics: None,
297 use_period_end_dynamics: false,
298 intra_day_patterns: None,
299 }
300 }
301
302 #[allow(clippy::too_many_arguments)]
304 pub fn with_period_end_dynamics(
305 seed: u64,
306 seasonality_config: SeasonalityConfig,
307 working_hours_config: WorkingHoursConfig,
308 holidays: Vec<NaiveDate>,
309 industry_seasonality: Option<IndustrySeasonality>,
310 holiday_calendar: Option<HolidayCalendar>,
311 period_end_dynamics: PeriodEndDynamics,
312 ) -> Self {
313 Self {
314 rng: ChaCha8Rng::seed_from_u64(seed),
315 seasonality_config,
316 working_hours_config,
317 holidays,
318 industry_seasonality,
319 holiday_calendar,
320 period_end_dynamics: Some(period_end_dynamics),
321 use_period_end_dynamics: true,
322 intra_day_patterns: None,
323 }
324 }
325
326 pub fn set_intra_day_patterns(&mut self, patterns: IntraDayPatterns) {
328 self.intra_day_patterns = Some(patterns);
329 }
330
331 pub fn get_intra_day_multiplier(&self, time: NaiveTime, is_human: bool) -> f64 {
333 self.intra_day_patterns
334 .as_ref()
335 .map(|p| p.get_multiplier(time, is_human))
336 .unwrap_or(1.0)
337 }
338
339 pub fn with_industry_seasonality(mut self, seasonality: IndustrySeasonality) -> Self {
341 self.industry_seasonality = Some(seasonality);
342 self
343 }
344
345 pub fn with_holiday_calendar(mut self, calendar: HolidayCalendar) -> Self {
347 self.holiday_calendar = Some(calendar);
348 self
349 }
350
351 pub fn set_industry_seasonality(&mut self, seasonality: IndustrySeasonality) {
353 self.industry_seasonality = Some(seasonality);
354 }
355
356 pub fn set_holiday_calendar(&mut self, calendar: HolidayCalendar) {
358 self.holiday_calendar = Some(calendar);
359 }
360
361 pub fn with_period_end(mut self, dynamics: PeriodEndDynamics) -> Self {
363 self.period_end_dynamics = Some(dynamics);
364 self.use_period_end_dynamics = true;
365 self
366 }
367
368 pub fn set_period_end_dynamics(&mut self, dynamics: PeriodEndDynamics) {
370 self.period_end_dynamics = Some(dynamics);
371 self.use_period_end_dynamics = true;
372 }
373
374 pub fn period_end_dynamics(&self) -> Option<&PeriodEndDynamics> {
376 self.period_end_dynamics.as_ref()
377 }
378
379 pub fn set_use_period_end_dynamics(&mut self, enabled: bool) {
381 self.use_period_end_dynamics = enabled;
382 }
383
384 pub fn industry_seasonality(&self) -> Option<&IndustrySeasonality> {
386 self.industry_seasonality.as_ref()
387 }
388
389 pub fn holiday_calendar(&self) -> Option<&HolidayCalendar> {
391 self.holiday_calendar.as_ref()
392 }
393
394 pub fn generate_us_holidays(year: i32) -> Vec<NaiveDate> {
396 let mut holidays = Vec::new();
397
398 holidays.push(NaiveDate::from_ymd_opt(year, 1, 1).expect("valid date/time components"));
400 holidays.push(NaiveDate::from_ymd_opt(year, 7, 4).expect("valid date/time components"));
402 holidays.push(NaiveDate::from_ymd_opt(year, 12, 25).expect("valid date/time components"));
404 let first_thursday = (1..=7)
406 .map(|d| NaiveDate::from_ymd_opt(year, 11, d).expect("valid date/time components"))
407 .find(|d| d.weekday() == Weekday::Thu)
408 .expect("valid date/time components");
409 let thanksgiving = first_thursday + Duration::weeks(3);
410 holidays.push(thanksgiving);
411
412 holidays
413 }
414
415 pub fn is_weekend(&self, date: NaiveDate) -> bool {
417 matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
418 }
419
420 pub fn get_day_of_week_multiplier(&self, date: NaiveDate) -> f64 {
429 if !self.seasonality_config.day_of_week_patterns {
430 return 1.0;
431 }
432
433 match date.weekday() {
434 Weekday::Mon => self.seasonality_config.monday_multiplier,
435 Weekday::Tue => self.seasonality_config.tuesday_multiplier,
436 Weekday::Wed => self.seasonality_config.wednesday_multiplier,
437 Weekday::Thu => self.seasonality_config.thursday_multiplier,
438 Weekday::Fri => self.seasonality_config.friday_multiplier,
439 Weekday::Sat | Weekday::Sun => 1.0, }
441 }
442
443 pub fn is_holiday(&self, date: NaiveDate) -> bool {
445 if self.holidays.contains(&date) {
447 return true;
448 }
449
450 if let Some(ref calendar) = self.holiday_calendar {
452 if calendar.is_holiday(date) {
453 return true;
454 }
455 }
456
457 false
458 }
459
460 fn get_holiday_multiplier(&self, date: NaiveDate) -> f64 {
462 if let Some(ref calendar) = self.holiday_calendar {
464 let mult = calendar.get_multiplier(date);
465 if mult < 1.0 {
466 return mult;
467 }
468 }
469
470 if self.holidays.contains(&date) {
472 return self.seasonality_config.holiday_activity;
473 }
474
475 1.0
476 }
477
478 pub fn is_month_end(&self, date: NaiveDate) -> bool {
480 let last_day = Self::last_day_of_month(date);
481 let days_until_end = (last_day - date).num_days();
482 days_until_end >= 0 && days_until_end < self.seasonality_config.month_end_lead_days as i64
483 }
484
485 pub fn is_quarter_end(&self, date: NaiveDate) -> bool {
487 let month = date.month();
488 let is_quarter_end_month = matches!(month, 3 | 6 | 9 | 12);
489 is_quarter_end_month && self.is_month_end(date)
490 }
491
492 pub fn is_year_end(&self, date: NaiveDate) -> bool {
494 date.month() == 12 && self.is_month_end(date)
495 }
496
497 pub fn last_day_of_month(date: NaiveDate) -> NaiveDate {
499 let year = date.year();
500 let month = date.month();
501
502 if month == 12 {
503 NaiveDate::from_ymd_opt(year + 1, 1, 1).expect("valid date/time components")
504 - Duration::days(1)
505 } else {
506 NaiveDate::from_ymd_opt(year, month + 1, 1).expect("valid date/time components")
507 - Duration::days(1)
508 }
509 }
510
511 pub fn get_date_multiplier(&self, date: NaiveDate) -> f64 {
521 let mut multiplier = 1.0;
522
523 if self.is_weekend(date) {
525 multiplier *= self.seasonality_config.weekend_activity;
526 } else {
527 multiplier *= self.get_day_of_week_multiplier(date);
529 }
530
531 let holiday_mult = self.get_holiday_multiplier(date);
533 if holiday_mult < 1.0 {
534 multiplier *= holiday_mult;
535 }
536
537 if self.use_period_end_dynamics {
539 if let Some(ref dynamics) = self.period_end_dynamics {
540 let period_mult = dynamics.get_multiplier_for_date(date);
541 multiplier *= period_mult;
542 }
543 } else {
544 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
546 multiplier *= self.seasonality_config.year_end_multiplier;
547 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
548 multiplier *= self.seasonality_config.quarter_end_multiplier;
549 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
550 multiplier *= self.seasonality_config.month_end_multiplier;
551 }
552 }
553
554 if let Some(ref industry) = self.industry_seasonality {
556 let industry_mult = industry.get_multiplier(date);
557 multiplier *= industry_mult;
560 }
561
562 multiplier
563 }
564
565 pub fn get_period_end_multiplier(&self, date: NaiveDate) -> f64 {
570 if self.use_period_end_dynamics {
571 if let Some(ref dynamics) = self.period_end_dynamics {
572 return dynamics.get_multiplier_for_date(date);
573 }
574 }
575
576 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
578 self.seasonality_config.year_end_multiplier
579 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
580 self.seasonality_config.quarter_end_multiplier
581 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
582 self.seasonality_config.month_end_multiplier
583 } else {
584 1.0
585 }
586 }
587
588 pub fn get_base_date_multiplier(&self, date: NaiveDate) -> f64 {
590 let mut multiplier = 1.0;
591
592 if self.is_weekend(date) {
593 multiplier *= self.seasonality_config.weekend_activity;
594 } else {
595 multiplier *= self.get_day_of_week_multiplier(date);
597 }
598
599 let holiday_mult = self.get_holiday_multiplier(date);
600 if holiday_mult < 1.0 {
601 multiplier *= holiday_mult;
602 }
603
604 if self.use_period_end_dynamics {
606 if let Some(ref dynamics) = self.period_end_dynamics {
607 let period_mult = dynamics.get_multiplier_for_date(date);
608 multiplier *= period_mult;
609 }
610 } else {
611 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
613 multiplier *= self.seasonality_config.year_end_multiplier;
614 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
615 multiplier *= self.seasonality_config.quarter_end_multiplier;
616 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
617 multiplier *= self.seasonality_config.month_end_multiplier;
618 }
619 }
620
621 multiplier
622 }
623
624 pub fn get_industry_multiplier(&self, date: NaiveDate) -> f64 {
626 self.industry_seasonality
627 .as_ref()
628 .map(|s| s.get_multiplier(date))
629 .unwrap_or(1.0)
630 }
631
632 pub fn sample_date(&mut self, start: NaiveDate, end: NaiveDate) -> NaiveDate {
634 let days = (end - start).num_days() as usize;
635 if days == 0 {
636 return start;
637 }
638
639 let mut weights: Vec<f64> = (0..=days)
641 .map(|d| {
642 let date = start + Duration::days(d as i64);
643 self.get_date_multiplier(date)
644 })
645 .collect();
646
647 let total: f64 = weights.iter().sum();
649 weights.iter_mut().for_each(|w| *w /= total);
650
651 let p: f64 = self.rng.gen();
653 let mut cumulative = 0.0;
654 for (i, weight) in weights.iter().enumerate() {
655 cumulative += weight;
656 if p < cumulative {
657 return start + Duration::days(i as i64);
658 }
659 }
660
661 end
662 }
663
664 pub fn sample_time(&mut self, is_human: bool) -> NaiveTime {
666 if !is_human {
667 let hour = if self.rng.gen::<f64>() < 0.7 {
669 self.rng.gen_range(22..=23).clamp(0, 23)
671 + if self.rng.gen_bool(0.5) {
672 0
673 } else {
674 self.rng.gen_range(0..=5)
675 }
676 } else {
677 self.rng.gen_range(0..24)
678 };
679 let minute = self.rng.gen_range(0..60);
680 let second = self.rng.gen_range(0..60);
681 return NaiveTime::from_hms_opt(hour.clamp(0, 23) as u32, minute, second)
682 .expect("valid date/time components");
683 }
684
685 let hour = if self.rng.gen::<f64>() < self.working_hours_config.after_hours_probability {
687 if self.rng.gen_bool(0.5) {
689 self.rng.gen_range(6..self.working_hours_config.day_start)
690 } else {
691 self.rng.gen_range(self.working_hours_config.day_end..22)
692 }
693 } else {
694 let is_peak = self.rng.gen::<f64>() < 0.6; if is_peak && !self.working_hours_config.peak_hours.is_empty() {
697 *self
698 .working_hours_config
699 .peak_hours
700 .choose(&mut self.rng)
701 .expect("valid date/time components")
702 } else {
703 self.rng.gen_range(
704 self.working_hours_config.day_start..self.working_hours_config.day_end,
705 )
706 }
707 };
708
709 let minute = self.rng.gen_range(0..60);
710 let second = self.rng.gen_range(0..60);
711
712 NaiveTime::from_hms_opt(hour as u32, minute, second).expect("valid date/time components")
713 }
714
715 pub fn expected_count_for_date(&self, date: NaiveDate, daily_average: f64) -> u64 {
717 let multiplier = self.get_date_multiplier(date);
718 (daily_average * multiplier).round() as u64
719 }
720
721 pub fn reset(&mut self, seed: u64) {
723 self.rng = ChaCha8Rng::seed_from_u64(seed);
724 }
725}
726
727#[derive(Debug, Clone)]
729pub struct TimePeriod {
730 pub start_date: NaiveDate,
732 pub end_date: NaiveDate,
734 pub fiscal_year: u16,
736 pub fiscal_periods: Vec<u8>,
738}
739
740impl TimePeriod {
741 pub fn fiscal_year(year: u16) -> Self {
743 Self {
744 start_date: NaiveDate::from_ymd_opt(year as i32, 1, 1)
745 .expect("valid date/time components"),
746 end_date: NaiveDate::from_ymd_opt(year as i32, 12, 31)
747 .expect("valid date/time components"),
748 fiscal_year: year,
749 fiscal_periods: (1..=12).collect(),
750 }
751 }
752
753 pub fn months(year: u16, start_month: u8, num_months: u8) -> Self {
755 let start_date = NaiveDate::from_ymd_opt(year as i32, start_month as u32, 1)
756 .expect("valid date/time components");
757 let end_month = ((start_month - 1 + num_months - 1) % 12) + 1;
758 let end_year = year + (start_month as u16 - 1 + num_months as u16 - 1) / 12;
759 let end_date = TemporalSampler::last_day_of_month(
760 NaiveDate::from_ymd_opt(end_year as i32, end_month as u32, 1)
761 .expect("valid date/time components"),
762 );
763
764 Self {
765 start_date,
766 end_date,
767 fiscal_year: year,
768 fiscal_periods: (start_month..start_month + num_months).collect(),
769 }
770 }
771
772 pub fn total_days(&self) -> i64 {
774 (self.end_date - self.start_date).num_days() + 1
775 }
776}
777
778#[cfg(test)]
779#[allow(clippy::unwrap_used)]
780mod tests {
781 use super::*;
782 use chrono::Timelike;
783
784 #[test]
785 fn test_is_weekend() {
786 let sampler = TemporalSampler::new(42);
787 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
788 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
789 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
790
791 assert!(sampler.is_weekend(saturday));
792 assert!(sampler.is_weekend(sunday));
793 assert!(!sampler.is_weekend(monday));
794 }
795
796 #[test]
797 fn test_is_month_end() {
798 let sampler = TemporalSampler::new(42);
799 let month_end = NaiveDate::from_ymd_opt(2024, 6, 28).unwrap();
800 let month_start = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
801
802 assert!(sampler.is_month_end(month_end));
803 assert!(!sampler.is_month_end(month_start));
804 }
805
806 #[test]
807 fn test_date_multiplier() {
808 let sampler = TemporalSampler::new(42);
809
810 let regular_day = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap(); assert!((sampler.get_date_multiplier(regular_day) - 1.0).abs() < 0.01);
813
814 let weekend = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(); assert!(sampler.get_date_multiplier(weekend) < 0.2);
817
818 let month_end = NaiveDate::from_ymd_opt(2024, 6, 28).unwrap();
820 assert!(sampler.get_date_multiplier(month_end) > 2.0);
821 }
822
823 #[test]
824 fn test_day_of_week_patterns() {
825 let sampler = TemporalSampler::new(42);
826
827 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
829 let tuesday = NaiveDate::from_ymd_opt(2024, 6, 11).unwrap();
830 let wednesday = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap();
831 let thursday = NaiveDate::from_ymd_opt(2024, 6, 13).unwrap();
832 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
833
834 let mon_mult = sampler.get_day_of_week_multiplier(monday);
836 assert!((mon_mult - 1.3).abs() < 0.01);
837
838 let tue_mult = sampler.get_day_of_week_multiplier(tuesday);
840 assert!((tue_mult - 1.1).abs() < 0.01);
841
842 let wed_mult = sampler.get_day_of_week_multiplier(wednesday);
844 let thu_mult = sampler.get_day_of_week_multiplier(thursday);
845 assert!((wed_mult - 1.0).abs() < 0.01);
846 assert!((thu_mult - 1.0).abs() < 0.01);
847
848 let fri_mult = sampler.get_day_of_week_multiplier(friday);
850 assert!((fri_mult - 0.85).abs() < 0.01);
851
852 assert!(sampler.get_date_multiplier(monday) > sampler.get_date_multiplier(friday));
855 }
856
857 #[test]
858 fn test_sample_time_human() {
859 let mut sampler = TemporalSampler::new(42);
860
861 for _ in 0..100 {
862 let time = sampler.sample_time(true);
863 let hour = time.hour();
865 assert!(hour < 24);
867 }
868 }
869
870 #[test]
871 fn test_time_period() {
872 let period = TimePeriod::fiscal_year(2024);
873 assert_eq!(period.total_days(), 366); let partial = TimePeriod::months(2024, 1, 6);
876 assert!(partial.total_days() > 180);
877 assert!(partial.total_days() < 185);
878 }
879}