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).unwrap(),
124 end: NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
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).unwrap(),
131 end: NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
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).unwrap(),
138 end: NaiveTime::from_hms_opt(13, 30, 0).unwrap(),
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).unwrap(),
145 end: NaiveTime::from_hms_opt(16, 0, 0).unwrap(),
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).unwrap(),
152 end: NaiveTime::from_hms_opt(17, 30, 0).unwrap(),
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).unwrap());
400 holidays.push(NaiveDate::from_ymd_opt(year, 7, 4).unwrap());
402 holidays.push(NaiveDate::from_ymd_opt(year, 12, 25).unwrap());
404 let first_thursday = (1..=7)
406 .map(|d| NaiveDate::from_ymd_opt(year, 11, d).unwrap())
407 .find(|d| d.weekday() == Weekday::Thu)
408 .unwrap();
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).unwrap() - Duration::days(1)
504 } else {
505 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() - Duration::days(1)
506 }
507 }
508
509 pub fn get_date_multiplier(&self, date: NaiveDate) -> f64 {
519 let mut multiplier = 1.0;
520
521 if self.is_weekend(date) {
523 multiplier *= self.seasonality_config.weekend_activity;
524 } else {
525 multiplier *= self.get_day_of_week_multiplier(date);
527 }
528
529 let holiday_mult = self.get_holiday_multiplier(date);
531 if holiday_mult < 1.0 {
532 multiplier *= holiday_mult;
533 }
534
535 if self.use_period_end_dynamics {
537 if let Some(ref dynamics) = self.period_end_dynamics {
538 let period_mult = dynamics.get_multiplier_for_date(date);
539 multiplier *= period_mult;
540 }
541 } else {
542 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
544 multiplier *= self.seasonality_config.year_end_multiplier;
545 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
546 multiplier *= self.seasonality_config.quarter_end_multiplier;
547 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
548 multiplier *= self.seasonality_config.month_end_multiplier;
549 }
550 }
551
552 if let Some(ref industry) = self.industry_seasonality {
554 let industry_mult = industry.get_multiplier(date);
555 multiplier *= industry_mult;
558 }
559
560 multiplier
561 }
562
563 pub fn get_period_end_multiplier(&self, date: NaiveDate) -> f64 {
568 if self.use_period_end_dynamics {
569 if let Some(ref dynamics) = self.period_end_dynamics {
570 return dynamics.get_multiplier_for_date(date);
571 }
572 }
573
574 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
576 self.seasonality_config.year_end_multiplier
577 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
578 self.seasonality_config.quarter_end_multiplier
579 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
580 self.seasonality_config.month_end_multiplier
581 } else {
582 1.0
583 }
584 }
585
586 pub fn get_base_date_multiplier(&self, date: NaiveDate) -> f64 {
588 let mut multiplier = 1.0;
589
590 if self.is_weekend(date) {
591 multiplier *= self.seasonality_config.weekend_activity;
592 } else {
593 multiplier *= self.get_day_of_week_multiplier(date);
595 }
596
597 let holiday_mult = self.get_holiday_multiplier(date);
598 if holiday_mult < 1.0 {
599 multiplier *= holiday_mult;
600 }
601
602 if self.use_period_end_dynamics {
604 if let Some(ref dynamics) = self.period_end_dynamics {
605 let period_mult = dynamics.get_multiplier_for_date(date);
606 multiplier *= period_mult;
607 }
608 } else {
609 if self.seasonality_config.year_end_spike && self.is_year_end(date) {
611 multiplier *= self.seasonality_config.year_end_multiplier;
612 } else if self.seasonality_config.quarter_end_spike && self.is_quarter_end(date) {
613 multiplier *= self.seasonality_config.quarter_end_multiplier;
614 } else if self.seasonality_config.month_end_spike && self.is_month_end(date) {
615 multiplier *= self.seasonality_config.month_end_multiplier;
616 }
617 }
618
619 multiplier
620 }
621
622 pub fn get_industry_multiplier(&self, date: NaiveDate) -> f64 {
624 self.industry_seasonality
625 .as_ref()
626 .map(|s| s.get_multiplier(date))
627 .unwrap_or(1.0)
628 }
629
630 pub fn sample_date(&mut self, start: NaiveDate, end: NaiveDate) -> NaiveDate {
632 let days = (end - start).num_days() as usize;
633 if days == 0 {
634 return start;
635 }
636
637 let mut weights: Vec<f64> = (0..=days)
639 .map(|d| {
640 let date = start + Duration::days(d as i64);
641 self.get_date_multiplier(date)
642 })
643 .collect();
644
645 let total: f64 = weights.iter().sum();
647 weights.iter_mut().for_each(|w| *w /= total);
648
649 let p: f64 = self.rng.gen();
651 let mut cumulative = 0.0;
652 for (i, weight) in weights.iter().enumerate() {
653 cumulative += weight;
654 if p < cumulative {
655 return start + Duration::days(i as i64);
656 }
657 }
658
659 end
660 }
661
662 pub fn sample_time(&mut self, is_human: bool) -> NaiveTime {
664 if !is_human {
665 let hour = if self.rng.gen::<f64>() < 0.7 {
667 self.rng.gen_range(22..=23).clamp(0, 23)
669 + if self.rng.gen_bool(0.5) {
670 0
671 } else {
672 self.rng.gen_range(0..=5)
673 }
674 } else {
675 self.rng.gen_range(0..24)
676 };
677 let minute = self.rng.gen_range(0..60);
678 let second = self.rng.gen_range(0..60);
679 return NaiveTime::from_hms_opt(hour.clamp(0, 23) as u32, minute, second).unwrap();
680 }
681
682 let hour = if self.rng.gen::<f64>() < self.working_hours_config.after_hours_probability {
684 if self.rng.gen_bool(0.5) {
686 self.rng.gen_range(6..self.working_hours_config.day_start)
687 } else {
688 self.rng.gen_range(self.working_hours_config.day_end..22)
689 }
690 } else {
691 let is_peak = self.rng.gen::<f64>() < 0.6; if is_peak && !self.working_hours_config.peak_hours.is_empty() {
694 *self
695 .working_hours_config
696 .peak_hours
697 .choose(&mut self.rng)
698 .unwrap()
699 } else {
700 self.rng.gen_range(
701 self.working_hours_config.day_start..self.working_hours_config.day_end,
702 )
703 }
704 };
705
706 let minute = self.rng.gen_range(0..60);
707 let second = self.rng.gen_range(0..60);
708
709 NaiveTime::from_hms_opt(hour as u32, minute, second).unwrap()
710 }
711
712 pub fn expected_count_for_date(&self, date: NaiveDate, daily_average: f64) -> u64 {
714 let multiplier = self.get_date_multiplier(date);
715 (daily_average * multiplier).round() as u64
716 }
717
718 pub fn reset(&mut self, seed: u64) {
720 self.rng = ChaCha8Rng::seed_from_u64(seed);
721 }
722}
723
724#[derive(Debug, Clone)]
726pub struct TimePeriod {
727 pub start_date: NaiveDate,
729 pub end_date: NaiveDate,
731 pub fiscal_year: u16,
733 pub fiscal_periods: Vec<u8>,
735}
736
737impl TimePeriod {
738 pub fn fiscal_year(year: u16) -> Self {
740 Self {
741 start_date: NaiveDate::from_ymd_opt(year as i32, 1, 1).unwrap(),
742 end_date: NaiveDate::from_ymd_opt(year as i32, 12, 31).unwrap(),
743 fiscal_year: year,
744 fiscal_periods: (1..=12).collect(),
745 }
746 }
747
748 pub fn months(year: u16, start_month: u8, num_months: u8) -> Self {
750 let start_date = NaiveDate::from_ymd_opt(year as i32, start_month as u32, 1).unwrap();
751 let end_month = ((start_month - 1 + num_months - 1) % 12) + 1;
752 let end_year = year + (start_month as u16 - 1 + num_months as u16 - 1) / 12;
753 let end_date = TemporalSampler::last_day_of_month(
754 NaiveDate::from_ymd_opt(end_year as i32, end_month as u32, 1).unwrap(),
755 );
756
757 Self {
758 start_date,
759 end_date,
760 fiscal_year: year,
761 fiscal_periods: (start_month..start_month + num_months).collect(),
762 }
763 }
764
765 pub fn total_days(&self) -> i64 {
767 (self.end_date - self.start_date).num_days() + 1
768 }
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774 use chrono::Timelike;
775
776 #[test]
777 fn test_is_weekend() {
778 let sampler = TemporalSampler::new(42);
779 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
780 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
781 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
782
783 assert!(sampler.is_weekend(saturday));
784 assert!(sampler.is_weekend(sunday));
785 assert!(!sampler.is_weekend(monday));
786 }
787
788 #[test]
789 fn test_is_month_end() {
790 let sampler = TemporalSampler::new(42);
791 let month_end = NaiveDate::from_ymd_opt(2024, 6, 28).unwrap();
792 let month_start = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
793
794 assert!(sampler.is_month_end(month_end));
795 assert!(!sampler.is_month_end(month_start));
796 }
797
798 #[test]
799 fn test_date_multiplier() {
800 let sampler = TemporalSampler::new(42);
801
802 let regular_day = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap(); assert!((sampler.get_date_multiplier(regular_day) - 1.0).abs() < 0.01);
805
806 let weekend = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(); assert!(sampler.get_date_multiplier(weekend) < 0.2);
809
810 let month_end = NaiveDate::from_ymd_opt(2024, 6, 28).unwrap();
812 assert!(sampler.get_date_multiplier(month_end) > 2.0);
813 }
814
815 #[test]
816 fn test_day_of_week_patterns() {
817 let sampler = TemporalSampler::new(42);
818
819 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
821 let tuesday = NaiveDate::from_ymd_opt(2024, 6, 11).unwrap();
822 let wednesday = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap();
823 let thursday = NaiveDate::from_ymd_opt(2024, 6, 13).unwrap();
824 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
825
826 let mon_mult = sampler.get_day_of_week_multiplier(monday);
828 assert!((mon_mult - 1.3).abs() < 0.01);
829
830 let tue_mult = sampler.get_day_of_week_multiplier(tuesday);
832 assert!((tue_mult - 1.1).abs() < 0.01);
833
834 let wed_mult = sampler.get_day_of_week_multiplier(wednesday);
836 let thu_mult = sampler.get_day_of_week_multiplier(thursday);
837 assert!((wed_mult - 1.0).abs() < 0.01);
838 assert!((thu_mult - 1.0).abs() < 0.01);
839
840 let fri_mult = sampler.get_day_of_week_multiplier(friday);
842 assert!((fri_mult - 0.85).abs() < 0.01);
843
844 assert!(sampler.get_date_multiplier(monday) > sampler.get_date_multiplier(friday));
847 }
848
849 #[test]
850 fn test_sample_time_human() {
851 let mut sampler = TemporalSampler::new(42);
852
853 for _ in 0..100 {
854 let time = sampler.sample_time(true);
855 let hour = time.hour();
857 assert!(hour < 24);
859 }
860 }
861
862 #[test]
863 fn test_time_period() {
864 let period = TimePeriod::fiscal_year(2024);
865 assert_eq!(period.total_days(), 366); let partial = TimePeriod::months(2024, 1, 6);
868 assert!(partial.total_days() > 180);
869 assert!(partial.total_days() < 185);
870 }
871}