1use chrono::{DateTime, TimeDelta, Utc};
2use chrono_tz::Tz;
3use itertools::Itertools;
4use std::collections::HashMap;
5
6use crate::{CostPeriod, CostPeriodMatching, CostPeriods, TariffCalculationMethod};
7
8#[derive(Clone, Debug)]
10pub struct PowerTariffMatches {
11 calc_method: TariffCalculationMethod,
12 cost_period_matching: CostPeriodMatching,
13 items: Vec<PeriodPeakMatches>,
14 current_power_average: Option<PartialPowerAverage>,
15}
16
17impl PowerTariffMatches {
18 pub fn new(
19 calc_method: TariffCalculationMethod,
20 periods: CostPeriods,
21 averages: &[PowerAverage],
22 current_power_average: Option<PartialPowerAverage>,
23 ) -> Self {
24 let cost_period_matching = periods.match_method();
25 let items = PeriodPeakMatches::new(calc_method, &periods, averages, cost_period_matching);
26
27 Self {
28 calc_method,
29 cost_period_matching,
30 items,
31 current_power_average,
32 }
33 }
34
35 pub fn new_dummy() -> Self {
36 Self::new(
37 TariffCalculationMethod::AverageDays(99),
38 CostPeriods::new_first(&[]),
39 &[],
40 None,
41 )
42 }
43
44 pub fn calc_method(&self) -> TariffCalculationMethod {
45 self.calc_method
46 }
47
48 pub fn cost_period_matching(&self) -> CostPeriodMatching {
49 self.cost_period_matching
50 }
51
52 pub fn items(&self) -> &[PeriodPeakMatches] {
53 &self.items
54 }
55
56 pub fn current_power_average(&self) -> Option<PartialPowerAverage> {
57 self.current_power_average
58 }
59}
60
61#[derive(Clone, Debug)]
63pub struct PeriodPeakMatches {
64 period: CostPeriod,
65 peaks: PowerPeaks,
66}
67
68impl PeriodPeakMatches {
69 pub fn period(&self) -> &CostPeriod {
70 &self.period
71 }
72 pub fn peaks(&self) -> &PowerPeaks {
73 &self.peaks
74 }
75
76 fn new(
77 calc_method: TariffCalculationMethod,
78 periods: &CostPeriods,
79 averages: &[PowerAverage],
80 match_method: CostPeriodMatching,
81 ) -> Vec<Self> {
82 let mut used_indices = if match_method == CostPeriodMatching::First {
83 Some(std::collections::HashSet::new())
84 } else {
85 None
86 };
87
88 periods
89 .iter()
90 .map(|period| {
91 let averages_for_period: Vec<PowerAverage> = averages
92 .iter()
93 .enumerate()
94 .filter_map(|(avg_idx, avg)| {
95 if period.matches(avg.timestamp) {
96 if let Some(ref mut used) = used_indices {
98 if used.contains(&avg_idx) {
99 return None;
100 }
101 used.insert(avg_idx);
102 }
103 Some(*avg)
104 } else {
105 None
106 }
107 })
108 .collect();
109
110 Self {
111 period: period.clone(),
112 peaks: PowerPeaks::new(calc_method, &averages_for_period),
113 }
114 })
115 .collect()
116 }
117}
118
119#[derive(Copy, Clone, Debug)]
121pub struct PartialPowerAverage {
122 power_average: PowerAverage,
123 duration_secs: u16,
124}
125
126impl PartialPowerAverage {
127 pub fn new(power_average: PowerAverage, duration_secs: u16) -> Self {
128 Self {
129 power_average,
130 duration_secs,
131 }
132 }
133
134 pub fn power_average(&self) -> PowerAverage {
135 self.power_average
136 }
137
138 pub fn duration_secs(&self) -> u16 {
139 self.duration_secs
140 }
141
142 fn start(&self) -> DateTime<Tz> {
143 self.power_average().timestamp()
144 }
145
146 fn end(&self) -> DateTime<Tz> {
147 self.power_average().timestamp() + TimeDelta::seconds(self.duration_secs().into())
148 }
149
150 pub fn covers(&self, ts: DateTime<Utc>, secs: u16) -> bool {
151 let ts_end = ts + TimeDelta::seconds(secs.into());
152 ts >= self.start() && ts_end <= self.end()
153 }
154
155 pub fn cover_percentage(&self, ts: DateTime<Utc>, num_secs: u32) -> u8 {
182 let ts_end = ts + TimeDelta::seconds(num_secs.into());
183
184 let self_start = self.start().with_timezone(&Utc);
186 let self_end = self.end().with_timezone(&Utc);
187
188 let overlap_start = ts.max(self_start);
190 let overlap_end = ts_end.min(self_end);
191
192 if overlap_start >= overlap_end {
194 return 0;
195 }
196
197 let overlap_secs = (overlap_end - overlap_start).num_seconds();
199
200 (overlap_secs as f64 / num_secs as f64 * 100.0) as u8
202 }
203}
204
205#[derive(Copy, Clone, Debug, PartialEq)]
207pub struct PowerAverage {
208 timestamp: DateTime<Tz>,
209 value: u32,
210}
211
212impl PowerAverage {
213 pub fn new<Dt: Into<DateTime<Tz>>>(timestamp: Dt, value: u32) -> Self {
214 Self {
215 timestamp: timestamp.into(),
216 value,
217 }
218 }
219
220 pub fn timestamp(&self) -> DateTime<Tz> {
221 self.timestamp
222 }
223
224 pub fn kw(&self) -> f64 {
225 self.value as f64 / 1000.
226 }
227}
228
229#[derive(Default, Clone, Debug)]
231pub struct PowerPeaks(Vec<PowerAverage>);
232
233impl PowerPeaks {
234 pub fn new(calc_method: TariffCalculationMethod, period_averages: &[PowerAverage]) -> Self {
235 let peaks: Vec<PowerAverage> = match calc_method {
236 crate::TariffCalculationMethod::AverageDays(n) => {
237 let mut daily_peaks: HashMap<chrono::NaiveDate, PowerAverage> = HashMap::new();
239
240 for power_average in period_averages {
242 let date = power_average.timestamp.date_naive();
243 daily_peaks
244 .entry(date)
245 .and_modify(|existing| {
246 if power_average.value > existing.value {
247 *existing = *power_average;
248 }
249 })
250 .or_insert(*power_average);
251 }
252
253 daily_peaks
255 .into_values()
256 .sorted_by_key(|power_average| power_average.value)
257 .rev()
258 .take(n as usize)
259 .collect()
260 }
261 crate::TariffCalculationMethod::AverageHours(n) => {
262 period_averages
264 .iter()
265 .sorted_by_key(|power_average| power_average.value)
266 .rev()
267 .take(n as usize)
268 .copied()
269 .collect()
270 }
271 };
272
273 Self(peaks)
274 }
275
276 pub fn values(&self) -> &[PowerAverage] {
277 &self.0
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::{Country, LoadType, Stockholm, months::Month};
285 use chrono::{Datelike, Timelike};
286
287 #[test]
288 fn average_hours_returns_n_highest_values() {
289 let averages = vec![
290 PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100),
291 PowerAverage::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 500),
292 PowerAverage::new(Stockholm.dt(2025, 1, 1, 2, 0, 0), 300),
293 PowerAverage::new(Stockholm.dt(2025, 1, 1, 3, 0, 0), 200),
294 PowerAverage::new(Stockholm.dt(2025, 1, 1, 4, 0, 0), 400),
295 ];
296
297 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageHours(3), &averages);
298
299 assert_eq!(peaks.values().len(), 3);
300 assert_eq!(peaks.values()[0].value, 500);
301 assert_eq!(peaks.values()[1].value, 400);
302 assert_eq!(peaks.values()[2].value, 300);
303 }
304
305 #[test]
306 fn average_hours_with_equal_values() {
307 let averages = vec![
308 PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 500),
309 PowerAverage::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 500),
310 PowerAverage::new(Stockholm.dt(2025, 1, 1, 2, 0, 0), 300),
311 ];
312
313 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageHours(2), &averages);
314
315 assert_eq!(peaks.values().len(), 2);
316 assert_eq!(peaks.values()[0].value, 500);
317 assert_eq!(peaks.values()[1].value, 500);
318 }
319
320 #[test]
321 fn average_hours_empty_input() {
322 let averages = vec![];
323
324 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageHours(3), &averages);
325
326 assert_eq!(peaks.values().len(), 0);
327 }
328
329 #[test]
330 fn average_hours_zero_n() {
331 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100)];
332
333 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageHours(0), &averages);
334
335 assert_eq!(peaks.values().len(), 0);
336 }
337
338 #[test]
339 fn average_hours_n_greater_than_available() {
340 let averages = vec![
341 PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100),
342 PowerAverage::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 200),
343 ];
344
345 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageHours(5), &averages);
346
347 assert_eq!(peaks.values().len(), 2);
348 assert_eq!(peaks.values()[0].value, 200);
349 assert_eq!(peaks.values()[1].value, 100);
350 }
351
352 #[test]
353 fn average_days_one_peak_per_day() {
354 let averages = vec![
355 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
357 PowerAverage::new(Stockholm.dt(2025, 1, 1, 11, 0, 0), 500),
358 PowerAverage::new(Stockholm.dt(2025, 1, 1, 12, 0, 0), 300),
359 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 200),
361 PowerAverage::new(Stockholm.dt(2025, 1, 2, 11, 0, 0), 600),
362 PowerAverage::new(Stockholm.dt(2025, 1, 3, 10, 0, 0), 150),
364 PowerAverage::new(Stockholm.dt(2025, 1, 3, 11, 0, 0), 250),
365 ];
366
367 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(2), &averages);
368
369 assert_eq!(peaks.values().len(), 2);
370 assert_eq!(peaks.values()[0].value, 600);
371 assert_eq!(peaks.values()[0].timestamp.day(), 2);
372 assert_eq!(peaks.values()[1].value, 500);
373 assert_eq!(peaks.values()[1].timestamp.day(), 1);
374 }
375
376 #[test]
377 fn average_days_ensures_different_days() {
378 let averages = vec![
379 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 500),
380 PowerAverage::new(Stockholm.dt(2025, 1, 1, 11, 0, 0), 450),
381 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 300),
382 ];
383
384 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(2), &averages);
385
386 assert_eq!(peaks.values().len(), 2);
387 let day1 = peaks.values()[0].timestamp.date_naive();
388 let day2 = peaks.values()[1].timestamp.date_naive();
389 assert_ne!(day1, day2);
390 }
391
392 #[test]
393 fn average_days_preserves_peak_hour_timestamp() {
394 let averages = vec![
395 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
396 PowerAverage::new(Stockholm.dt(2025, 1, 1, 14, 0, 0), 500),
397 PowerAverage::new(Stockholm.dt(2025, 1, 1, 20, 0, 0), 300),
398 ];
399
400 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(1), &averages);
401
402 assert_eq!(peaks.values().len(), 1);
403 assert_eq!(peaks.values()[0].timestamp.hour(), 14);
404 assert_eq!(peaks.values()[0].value, 500);
405 }
406
407 #[test]
408 fn average_days_empty_input() {
409 let averages = vec![];
410
411 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(3), &averages);
412
413 assert_eq!(peaks.values().len(), 0);
414 }
415
416 #[test]
417 fn average_days_zero_n() {
418 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100)];
419
420 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(0), &averages);
421
422 assert_eq!(peaks.values().len(), 0);
423 }
424
425 #[test]
426 fn average_days_n_greater_than_available_days() {
427 let averages = vec![
428 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
429 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 200),
430 ];
431
432 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(5), &averages);
433
434 assert_eq!(peaks.values().len(), 2);
435 }
436
437 #[test]
438 fn peak_periods_first_matching_splits_values() {
439 static PERIODS_ARRAY: [CostPeriod; 2] = [
440 CostPeriod::builder()
441 .load(LoadType::High)
442 .fixed_cost(10, 0)
443 .hours(6, 22)
444 .months(Month::November, Month::March)
445 .exclude_weekends()
446 .exclude_holidays(Country::SE)
447 .build(),
448 CostPeriod::builder()
449 .load(LoadType::Low)
450 .fixed_cost(5, 0)
451 .build(),
452 ];
453 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
454
455 let averages = vec![
457 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
459 PowerAverage::new(Stockholm.dt(2025, 1, 15, 23, 0, 0), 300),
461 PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400),
463 ];
464
465 let result = PowerTariffMatches::new(
466 TariffCalculationMethod::AverageHours(10),
467 periods,
468 &averages,
469 None,
470 );
471
472 assert_eq!(result.items().len(), 2);
473
474 assert_eq!(result.items()[0].peaks().values().len(), 2);
476 assert_eq!(result.items()[0].peaks().values()[0].value, 500);
477 assert_eq!(result.items()[0].peaks().values()[1].value, 400);
478
479 assert_eq!(result.items()[1].peaks().values().len(), 1);
481 assert_eq!(result.items()[1].peaks().values()[0].value, 300);
482 }
483
484 #[test]
485 fn peak_periods_all_matching_duplicates_values() {
486 static PERIODS_ARRAY: [CostPeriod; 2] = [
487 CostPeriod::builder()
488 .load(LoadType::High)
489 .fixed_cost(10, 0)
490 .hours(6, 22)
491 .months(Month::November, Month::March)
492 .exclude_weekends()
493 .exclude_holidays(Country::SE)
494 .build(),
495 CostPeriod::builder()
496 .load(LoadType::Low)
497 .fixed_cost(5, 0)
498 .build(),
499 ];
500 let periods = CostPeriods::new_all(&PERIODS_ARRAY);
501
502 let averages = vec![
504 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
505 PowerAverage::new(Stockholm.dt(2025, 1, 15, 23, 0, 0), 300),
506 ];
507
508 let result = PowerTariffMatches::new(
509 TariffCalculationMethod::AverageHours(10),
510 periods,
511 &averages,
512 None,
513 );
514
515 assert_eq!(result.items().len(), 2);
516
517 assert_eq!(result.items()[0].peaks().values().len(), 1);
519 assert_eq!(result.items()[0].peaks().values()[0].value, 500);
520
521 assert_eq!(result.items()[1].peaks().values().len(), 2);
523 assert_eq!(result.items()[1].peaks().values()[0].value, 500);
524 assert_eq!(result.items()[1].peaks().values()[1].value, 300);
525 }
526
527 #[test]
528 fn peak_periods_empty_averages() {
529 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
530 .load(LoadType::Low)
531 .fixed_cost(5, 0)
532 .build()];
533 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
534 let averages = vec![];
535
536 let result = PowerTariffMatches::new(
537 TariffCalculationMethod::AverageHours(3),
538 periods,
539 &averages,
540 None,
541 );
542
543 assert_eq!(result.items().len(), 1);
544 assert_eq!(result.items()[0].peaks().values().len(), 0);
545 }
546
547 #[test]
548 fn single_value_both_methods() {
549 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100)];
550
551 let hours = PowerPeaks::new(TariffCalculationMethod::AverageHours(3), &averages);
552 assert_eq!(hours.values().len(), 1);
553 assert_eq!(hours.values()[0].value, 100);
554
555 let days = PowerPeaks::new(TariffCalculationMethod::AverageDays(3), &averages);
556 assert_eq!(days.values().len(), 1);
557 assert_eq!(days.values()[0].value, 100);
558 }
559
560 #[test]
561 fn covers_percentage_complete_overlap() {
562 let partial = PartialPowerAverage::new(
564 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
565 3600,
566 );
567
568 let ts = Stockholm.dt(2025, 1, 1, 10, 15, 0).with_timezone(&Utc); let result = partial.cover_percentage(ts, 1800); assert_eq!(result, 100); }
574
575 #[test]
576 fn covers_percentage_partial_overlap_start() {
577 let partial = PartialPowerAverage::new(
579 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
580 3600,
581 );
582
583 let ts = Stockholm.dt(2025, 1, 1, 9, 30, 0).with_timezone(&Utc);
586 let result = partial.cover_percentage(ts, 3600); assert_eq!(result, 50); }
590
591 #[test]
592 fn covers_percentage_partial_overlap_end() {
593 let partial = PartialPowerAverage::new(
595 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
596 3600,
597 );
598
599 let ts = Stockholm.dt(2025, 1, 1, 10, 30, 0).with_timezone(&Utc);
602 let result = partial.cover_percentage(ts, 3600); assert_eq!(result, 50); }
606
607 #[test]
608 fn covers_percentage_no_overlap_before() {
609 let partial = PartialPowerAverage::new(
611 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
612 3600,
613 );
614
615 let ts = Stockholm.dt(2025, 1, 1, 8, 0, 0).with_timezone(&Utc);
617 let result = partial.cover_percentage(ts, 3600);
618
619 assert_eq!(result, 0);
620 }
621
622 #[test]
623 fn covers_percentage_no_overlap_after() {
624 let partial = PartialPowerAverage::new(
626 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
627 3600,
628 );
629
630 let ts = Stockholm.dt(2025, 1, 1, 12, 0, 0).with_timezone(&Utc);
632 let result = partial.cover_percentage(ts, 3600);
633
634 assert_eq!(result, 0);
635 }
636
637 #[test]
638 fn covers_percentage_no_overlap() {
639 let partial = PartialPowerAverage::new(
641 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
642 3600,
643 );
644
645 let ts = Stockholm.dt(2025, 1, 1, 9, 0, 0).with_timezone(&Utc);
648 let result = partial.cover_percentage(ts, 3600);
649
650 assert_eq!(result, 0);
651 }
652
653 #[test]
654 fn covers_percentage_half_overlap() {
655 let partial = PartialPowerAverage::new(
657 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
658 3600,
659 );
660
661 let ts = Stockholm.dt(2025, 1, 1, 9, 30, 0).with_timezone(&Utc);
663 let result = partial.cover_percentage(ts, 3600);
664
665 assert_eq!(result, 50);
666 }
667
668 #[test]
669 fn covers_percentage_query_contains_partial() {
670 let partial = PartialPowerAverage::new(
672 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
673 3600,
674 );
675
676 let ts = Stockholm.dt(2025, 1, 1, 9, 0, 0).with_timezone(&Utc);
679 let result = partial.cover_percentage(ts, 10800);
680
681 assert_eq!(result, 33); }
683
684 #[test]
685 fn covers_percentage_exact_match() {
686 let partial = PartialPowerAverage::new(
688 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
689 3600,
690 );
691
692 let ts = Stockholm.dt(2025, 1, 1, 10, 0, 0).with_timezone(&Utc);
694 let result = partial.cover_percentage(ts, 3600);
695
696 assert_eq!(result, 100);
697 }
698
699 #[test]
700 fn covers_percentage_adjacent_ranges_no_overlap() {
701 let partial = PartialPowerAverage::new(
703 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
704 3600,
705 );
706
707 let ts = Stockholm.dt(2025, 1, 1, 11, 0, 0).with_timezone(&Utc);
709 let result = partial.cover_percentage(ts, 3600);
710
711 assert_eq!(result, 0); }
713
714 #[test]
715 fn covers_percentage_very_small_overlap() {
716 let partial = PartialPowerAverage::new(
718 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
719 3600,
720 );
721
722 let ts = Stockholm.dt(2025, 1, 1, 10, 59, 59).with_timezone(&Utc);
724 let result = partial.cover_percentage(ts, 3600);
725
726 assert_eq!(result, 0);
728 }
729
730 #[test]
731 fn covers_percentage_short_duration() {
732 let partial = PartialPowerAverage::new(
734 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
735 60,
736 );
737
738 let ts = Stockholm.dt(2025, 1, 1, 10, 0, 30).with_timezone(&Utc);
740 let result = partial.cover_percentage(ts, 60);
741
742 assert_eq!(result, 50); }
744
745 mod map_periods_to_data_tests {
746 use super::*;
747 use crate::{LoadType, Stockholm, months::Month};
748
749 #[test]
750 fn map_periods_first_matching_no_overlap() {
751 static PERIODS_ARRAY: [CostPeriod; 2] = [
753 CostPeriod::builder()
754 .load(LoadType::High)
755 .fixed_cost(10, 0)
756 .hours(6, 12)
757 .build(),
758 CostPeriod::builder()
759 .load(LoadType::Low)
760 .fixed_cost(5, 0)
761 .hours(12, 22)
762 .build(),
763 ];
764 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
765
766 let averages = vec![
767 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500), PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400), ];
770
771 let result = PeriodPeakMatches::new(
772 TariffCalculationMethod::AverageHours(10),
773 &periods,
774 &averages,
775 CostPeriodMatching::First,
776 );
777
778 assert_eq!(result.len(), 2);
779 assert_eq!(result[0].peaks().values().len(), 1);
780 assert_eq!(result[0].peaks().values()[0].value, 500);
781 assert_eq!(result[1].peaks().values().len(), 1);
782 assert_eq!(result[1].peaks().values()[0].value, 400);
783 }
784
785 #[test]
786 fn map_periods_first_matching_overlapping_periods() {
787 static PERIODS_ARRAY: [CostPeriod; 2] = [
789 CostPeriod::builder()
790 .load(LoadType::High)
791 .fixed_cost(10, 0)
792 .hours(6, 18) .build(),
794 CostPeriod::builder()
795 .load(LoadType::Low)
796 .fixed_cost(5, 0)
797 .hours(12, 22) .build(),
799 ];
800 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
801
802 let averages = vec![
803 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500), PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400), PowerAverage::new(Stockholm.dt(2025, 1, 15, 20, 0, 0), 300), ];
807
808 let result = PeriodPeakMatches::new(
809 TariffCalculationMethod::AverageHours(10),
810 &periods,
811 &averages,
812 CostPeriodMatching::First,
813 );
814
815 assert_eq!(result.len(), 2);
816 assert_eq!(result[0].peaks().values().len(), 2);
818 assert_eq!(result[0].peaks().values()[0].value, 500);
819 assert_eq!(result[0].peaks().values()[1].value, 400);
820 assert_eq!(result[1].peaks().values().len(), 1);
822 assert_eq!(result[1].peaks().values()[0].value, 300);
823 }
824
825 #[test]
826 fn map_periods_all_matching_duplicates() {
827 static PERIODS_ARRAY: [CostPeriod; 2] = [
829 CostPeriod::builder()
830 .load(LoadType::High)
831 .fixed_cost(10, 0)
832 .hours(6, 18)
833 .build(),
834 CostPeriod::builder()
835 .load(LoadType::Base)
836 .fixed_cost(5, 0)
837 .build(),
838 ];
839 let periods = CostPeriods::new_all(&PERIODS_ARRAY);
840
841 let averages = vec![
842 PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400), ];
844
845 let result = PeriodPeakMatches::new(
846 TariffCalculationMethod::AverageHours(10),
847 &periods,
848 &averages,
849 CostPeriodMatching::All,
850 );
851
852 assert_eq!(result.len(), 2);
853 assert_eq!(result[0].peaks().values().len(), 1);
855 assert_eq!(result[0].peaks().values()[0].value, 400);
856 assert_eq!(result[1].peaks().values().len(), 1);
857 assert_eq!(result[1].peaks().values()[0].value, 400);
858 }
859
860 #[test]
861 fn map_periods_empty_averages() {
862 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
863 .load(LoadType::Low)
864 .fixed_cost(5, 0)
865 .build()];
866 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
867 let averages = vec![];
868
869 let result = PeriodPeakMatches::new(
870 TariffCalculationMethod::AverageHours(3),
871 &periods,
872 &averages,
873 CostPeriodMatching::First,
874 );
875
876 assert_eq!(result.len(), 1);
877 assert_eq!(result[0].peaks().values().len(), 0);
878 }
879
880 #[test]
881 fn map_periods_no_matching_averages() {
882 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
884 .load(LoadType::High)
885 .fixed_cost(10, 0)
886 .hours(6, 12)
887 .months(Month::June, Month::August) .build()];
889 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
890
891 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500)];
893
894 let result = PeriodPeakMatches::new(
895 TariffCalculationMethod::AverageHours(10),
896 &periods,
897 &averages,
898 CostPeriodMatching::First,
899 );
900
901 assert_eq!(result.len(), 1);
902 assert_eq!(result[0].peaks().values().len(), 0);
903 }
904
905 #[test]
906 fn map_periods_preserves_calculation_method() {
907 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
908 .load(LoadType::High)
909 .fixed_cost(10, 0)
910 .build()];
911 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
912
913 let averages = vec![
915 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 500),
916 PowerAverage::new(Stockholm.dt(2025, 1, 1, 14, 0, 0), 600),
917 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 300),
918 PowerAverage::new(Stockholm.dt(2025, 1, 2, 14, 0, 0), 400),
919 ];
920
921 let result_days = PeriodPeakMatches::new(
923 TariffCalculationMethod::AverageDays(1),
924 &periods,
925 &averages,
926 CostPeriodMatching::First,
927 );
928 assert_eq!(result_days[0].peaks().values().len(), 1);
929 assert_eq!(result_days[0].peaks().values()[0].value, 600); let result_hours = PeriodPeakMatches::new(
933 TariffCalculationMethod::AverageHours(2),
934 &periods,
935 &averages,
936 CostPeriodMatching::First,
937 );
938 assert_eq!(result_hours[0].peaks().values().len(), 2);
939 assert_eq!(result_hours[0].peaks().values()[0].value, 600);
940 assert_eq!(result_hours[0].peaks().values()[1].value, 500);
941 }
942
943 #[test]
944 fn map_periods_first_matching_multiple_periods() {
945 static PERIODS_ARRAY: [CostPeriod; 3] = [
947 CostPeriod::builder()
948 .load(LoadType::High)
949 .fixed_cost(10, 0)
950 .hours(6, 12)
951 .build(),
952 CostPeriod::builder()
953 .load(LoadType::Base)
954 .fixed_cost(7, 50)
955 .hours(12, 18)
956 .build(),
957 CostPeriod::builder()
958 .load(LoadType::Low)
959 .fixed_cost(5, 0)
960 .build(),
961 ];
962 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
963
964 let averages = vec![
965 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500), PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400), PowerAverage::new(Stockholm.dt(2025, 1, 15, 20, 0, 0), 300), PowerAverage::new(Stockholm.dt(2025, 1, 15, 3, 0, 0), 200), ];
970
971 let result = PeriodPeakMatches::new(
972 TariffCalculationMethod::AverageHours(10),
973 &periods,
974 &averages,
975 CostPeriodMatching::First,
976 );
977
978 assert_eq!(result.len(), 3);
979 assert_eq!(result[0].peaks().values().len(), 1); assert_eq!(result[0].peaks().values()[0].value, 500);
981 assert_eq!(result[1].peaks().values().len(), 1); assert_eq!(result[1].peaks().values()[0].value, 400);
983 assert_eq!(result[2].peaks().values().len(), 2); assert_eq!(result[2].peaks().values()[0].value, 300);
985 assert_eq!(result[2].peaks().values()[1].value, 200);
986 }
987
988 #[test]
989 fn map_periods_all_matching_catch_all_period() {
990 static PERIODS_ARRAY: [CostPeriod; 2] = [
992 CostPeriod::builder()
993 .load(LoadType::High)
994 .fixed_cost(10, 0)
995 .hours(6, 12)
996 .build(),
997 CostPeriod::builder()
998 .load(LoadType::Low)
999 .fixed_cost(5, 0)
1000 .build(), ];
1002 let periods = CostPeriods::new_all(&PERIODS_ARRAY);
1003
1004 let averages = vec![
1005 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500), PowerAverage::new(Stockholm.dt(2025, 1, 15, 20, 0, 0), 300), ];
1008
1009 let result = PeriodPeakMatches::new(
1010 TariffCalculationMethod::AverageHours(10),
1011 &periods,
1012 &averages,
1013 CostPeriodMatching::All,
1014 );
1015
1016 assert_eq!(result.len(), 2);
1017 assert_eq!(result[0].peaks().values().len(), 1);
1019 assert_eq!(result[0].peaks().values()[0].value, 500);
1020 assert_eq!(result[1].peaks().values().len(), 2);
1022 assert_eq!(result[1].peaks().values()[0].value, 500);
1023 assert_eq!(result[1].peaks().values()[1].value, 300);
1024 }
1025
1026 #[test]
1027 fn map_periods_order_preservation() {
1028 static PERIODS_ARRAY: [CostPeriod; 2] = [
1030 CostPeriod::builder()
1031 .load(LoadType::High)
1032 .fixed_cost(10, 0)
1033 .hours(6, 12)
1034 .build(),
1035 CostPeriod::builder()
1036 .load(LoadType::Low)
1037 .fixed_cost(5, 0)
1038 .hours(12, 18)
1039 .build(),
1040 ];
1041 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
1042
1043 let averages = vec![
1044 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
1045 PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400),
1046 ];
1047
1048 let result = PeriodPeakMatches::new(
1049 TariffCalculationMethod::AverageHours(10),
1050 &periods,
1051 &averages,
1052 CostPeriodMatching::First,
1053 );
1054
1055 assert_eq!(result.len(), 2);
1056 assert_eq!(result[0].peaks().values()[0].value, 500); assert_eq!(result[1].peaks().values()[0].value, 400); }
1060 }
1061}