1use chrono::{DateTime, TimeDelta, Utc};
2use chrono_tz::Tz;
3use itertools::Itertools;
4use std::{collections::HashMap, marker::PhantomData};
5
6use crate::{
7 CostPeriod, CostPeriodMatching, CostPeriods, TariffCalculationMethod,
8 power_tariffs::PowerDivide,
9};
10
11#[derive(Clone, Debug)]
13pub struct PowerTariffMatches {
14 calc_method: TariffCalculationMethod,
15 cost_period_matching: CostPeriodMatching,
16 power_divide: Option<PowerDivide>,
17 items: Vec<PeriodPeakMatches>,
18 current_power_average: Option<PartialPowerAverage>,
19}
20
21impl PowerTariffMatches {
22 pub fn new(
23 calc_method: TariffCalculationMethod,
24 power_divide: Option<PowerDivide>,
25 periods: CostPeriods,
26 averages: &[PowerAverage<Virtual>],
27 current_power_average: Option<PartialPowerAverage>,
28 ) -> Self {
29 let cost_period_matching = periods.match_method();
30 let items = PeriodPeakMatches::new(calc_method, &periods, averages, cost_period_matching);
31
32 Self {
33 calc_method,
34 cost_period_matching,
35 power_divide,
36 items,
37 current_power_average,
38 }
39 }
40
41 pub fn new_dummy() -> Self {
42 Self::new(
43 TariffCalculationMethod::AverageDays(99),
44 None,
45 CostPeriods::new_first(&[]),
46 &[],
47 None,
48 )
49 }
50
51 pub fn calc_method(&self) -> TariffCalculationMethod {
52 self.calc_method
53 }
54
55 pub fn cost_period_matching(&self) -> CostPeriodMatching {
56 self.cost_period_matching
57 }
58
59 pub fn items(&self) -> &[PeriodPeakMatches] {
60 &self.items
61 }
62
63 pub fn current_power_average(&self) -> Option<PartialPowerAverage> {
64 self.current_power_average
65 }
66
67 pub fn power_divide(&self) -> Option<PowerDivide> {
68 self.power_divide
69 }
70}
71
72#[derive(Clone, Debug)]
74pub struct PeriodPeakMatches {
75 period: CostPeriod,
76 peaks: PowerPeaks,
77}
78
79impl PeriodPeakMatches {
80 pub fn period(&self) -> &CostPeriod {
81 &self.period
82 }
83 pub fn peaks(&self) -> &PowerPeaks {
84 &self.peaks
85 }
86
87 fn new(
88 calc_method: TariffCalculationMethod,
89 periods: &CostPeriods,
90 averages: &[PowerAverage<Virtual>],
91 match_method: CostPeriodMatching,
92 ) -> Vec<Self> {
93 let mut used_indices = if match_method == CostPeriodMatching::First {
94 Some(std::collections::HashSet::new())
95 } else {
96 None
97 };
98
99 periods
100 .iter()
101 .map(|period| {
102 let averages_for_period: Vec<PowerAverage<Virtual>> = averages
103 .iter()
104 .enumerate()
105 .filter_map(|(avg_idx, avg)| {
106 if period.matches(avg.timestamp) {
107 if let Some(ref mut used) = used_indices {
109 if used.contains(&avg_idx) {
110 return None;
111 }
112 used.insert(avg_idx);
113 }
114 Some(*avg)
115 } else {
116 None
117 }
118 })
119 .collect();
120
121 Self {
122 period: period.clone(),
123 peaks: PowerPeaks::new(calc_method, &averages_for_period),
124 }
125 })
126 .collect()
127 }
128}
129
130#[derive(Copy, Clone, Debug)]
132pub struct PartialPowerAverage {
133 power_average: PowerAverage<Actual>,
134 duration_secs: u16,
135}
136
137impl PartialPowerAverage {
138 pub fn new(power_average: PowerAverage<Actual>, duration_secs: u16) -> Self {
139 Self {
140 power_average,
141 duration_secs,
142 }
143 }
144
145 pub fn power_average(&self) -> PowerAverage<Actual> {
146 self.power_average
147 }
148
149 pub fn duration_secs(&self) -> u16 {
150 self.duration_secs
151 }
152
153 fn start(&self) -> DateTime<Tz> {
154 self.power_average().timestamp()
155 }
156
157 fn end(&self) -> DateTime<Tz> {
158 self.power_average().timestamp() + TimeDelta::seconds(self.duration_secs().into())
159 }
160
161 pub fn covers(&self, ts: DateTime<Utc>, secs: u16) -> bool {
162 let ts_end = ts + TimeDelta::seconds(secs.into());
163 ts >= self.start() && ts_end <= self.end()
164 }
165
166 pub fn cover_percentage(&self, ts: DateTime<Utc>, num_secs: u32) -> u8 {
193 let ts_end = ts + TimeDelta::seconds(num_secs.into());
194
195 let self_start = self.start().with_timezone(&Utc);
197 let self_end = self.end().with_timezone(&Utc);
198
199 let overlap_start = ts.max(self_start);
201 let overlap_end = ts_end.min(self_end);
202
203 if overlap_start >= overlap_end {
205 return 0;
206 }
207
208 let overlap_secs = (overlap_end - overlap_start).num_seconds();
210
211 (overlap_secs as f64 / num_secs as f64 * 100.0) as u8
213 }
214}
215
216pub trait PowerAverageType {}
217
218#[derive(Copy, Clone, Debug, PartialEq)]
219pub struct Virtual;
220impl PowerAverageType for Virtual {}
221
222#[derive(Copy, Clone, Debug, PartialEq)]
223pub struct Actual;
224impl PowerAverageType for Actual {}
225
226#[derive(Copy, Clone, Debug, PartialEq)]
228pub struct PowerAverage<T: PowerAverageType> {
229 timestamp: DateTime<Tz>,
230 value: u32,
231 _type: PhantomData<T>,
232}
233
234impl<T: PowerAverageType> PowerAverage<T> {
235 pub fn timestamp(&self) -> DateTime<Tz> {
236 self.timestamp
237 }
238
239 pub fn kw(&self) -> f64 {
240 self.value as f64 / 1000.
241 }
242}
243
244impl PowerAverage<Actual> {
245 pub fn new<Dt: Into<DateTime<Tz>>>(timestamp: Dt, value: u32) -> Self {
246 Self {
247 timestamp: timestamp.into(),
248 value,
249 _type: PhantomData,
250 }
251 }
252
253 pub fn into_virtual(self, power_divide: Option<PowerDivide>) -> PowerAverage<Virtual> {
254 let adjusted_value = if let Some(pd) = power_divide {
255 (self.value as f64 * pd.multiplier(self.timestamp)) as u32
256 } else {
257 self.value
258 };
259
260 PowerAverage {
261 timestamp: self.timestamp,
262 value: adjusted_value,
263 _type: PhantomData,
264 }
265 }
266}
267
268#[derive(Default, Clone, Debug)]
270pub struct PowerPeaks(Vec<PowerAverage<Virtual>>);
271
272impl PowerPeaks {
273 pub fn new(
274 calc_method: TariffCalculationMethod,
275 period_averages: &[PowerAverage<Virtual>],
276 ) -> Self {
277 let peaks: Vec<PowerAverage<Virtual>> = match calc_method {
278 crate::TariffCalculationMethod::AverageDays(n) => {
279 let mut daily_peaks: HashMap<chrono::NaiveDate, PowerAverage<Virtual>> =
281 HashMap::new();
282
283 for power_average in period_averages {
285 let date = power_average.timestamp.date_naive();
286 daily_peaks
287 .entry(date)
288 .and_modify(|existing| {
289 if power_average.value > existing.value {
290 *existing = *power_average;
291 }
292 })
293 .or_insert(*power_average);
294 }
295
296 daily_peaks
298 .into_values()
299 .sorted_by_key(|power_average| power_average.value)
300 .rev()
301 .take(n as usize)
302 .collect()
303 }
304 crate::TariffCalculationMethod::AverageHours(n) => {
305 period_averages
307 .iter()
308 .sorted_by_key(|power_average| power_average.value)
309 .rev()
310 .take(n as usize)
311 .copied()
312 .collect()
313 }
314 };
315
316 Self(peaks)
317 }
318
319 pub fn values(&self) -> &[PowerAverage<Virtual>] {
320 &self.0
321 }
322
323 pub fn min(&self) -> Option<PowerAverage<Virtual>> {
324 self.values().last().copied()
325 }
326
327 pub fn max(&self) -> Option<PowerAverage<Virtual>> {
328 self.values().first().copied()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::{Country, LoadType, Stockholm, months::Month};
336 use chrono::{Datelike, Timelike};
337
338 #[test]
339 fn average_hours_returns_n_highest_values() {
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), 500),
343 PowerAverage::new(Stockholm.dt(2025, 1, 1, 2, 0, 0), 300),
344 PowerAverage::new(Stockholm.dt(2025, 1, 1, 3, 0, 0), 200),
345 PowerAverage::new(Stockholm.dt(2025, 1, 1, 4, 0, 0), 400),
346 ];
347
348 let peaks = PowerPeaks::new(
349 TariffCalculationMethod::AverageHours(3),
350 &averages
351 .iter()
352 .map(|a| a.into_virtual(None))
353 .collect::<Vec<_>>(),
354 );
355
356 assert_eq!(peaks.values().len(), 3);
357 assert_eq!(peaks.values()[0].value, 500);
358 assert_eq!(peaks.values()[1].value, 400);
359 assert_eq!(peaks.values()[2].value, 300);
360 }
361
362 #[test]
363 fn average_hours_with_equal_values() {
364 let averages = vec![
365 PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 500),
366 PowerAverage::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 500),
367 PowerAverage::new(Stockholm.dt(2025, 1, 1, 2, 0, 0), 300),
368 ];
369
370 let peaks = PowerPeaks::new(
371 TariffCalculationMethod::AverageHours(2),
372 &averages
373 .iter()
374 .map(|a| a.into_virtual(None))
375 .collect::<Vec<_>>(),
376 );
377
378 assert_eq!(peaks.values().len(), 2);
379 assert_eq!(peaks.values()[0].value, 500);
380 assert_eq!(peaks.values()[1].value, 500);
381 }
382
383 #[test]
384 fn average_hours_empty_input() {
385 let averages = vec![];
386
387 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageHours(3), &averages);
388
389 assert_eq!(peaks.values().len(), 0);
390 }
391
392 #[test]
393 fn average_hours_zero_n() {
394 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100)];
395
396 let peaks = PowerPeaks::new(
397 TariffCalculationMethod::AverageHours(0),
398 &averages
399 .iter()
400 .map(|a| a.into_virtual(None))
401 .collect::<Vec<_>>(),
402 );
403
404 assert_eq!(peaks.values().len(), 0);
405 }
406
407 #[test]
408 fn average_hours_n_greater_than_available() {
409 let averages = vec![
410 PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100),
411 PowerAverage::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 200),
412 ];
413
414 let peaks = PowerPeaks::new(
415 TariffCalculationMethod::AverageHours(5),
416 &averages
417 .iter()
418 .map(|a| a.into_virtual(None))
419 .collect::<Vec<_>>(),
420 );
421
422 assert_eq!(peaks.values().len(), 2);
423 assert_eq!(peaks.values()[0].value, 200);
424 assert_eq!(peaks.values()[1].value, 100);
425 }
426
427 #[test]
428 fn average_days_one_peak_per_day() {
429 let averages = vec![
430 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
432 PowerAverage::new(Stockholm.dt(2025, 1, 1, 11, 0, 0), 500),
433 PowerAverage::new(Stockholm.dt(2025, 1, 1, 12, 0, 0), 300),
434 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 200),
436 PowerAverage::new(Stockholm.dt(2025, 1, 2, 11, 0, 0), 600),
437 PowerAverage::new(Stockholm.dt(2025, 1, 3, 10, 0, 0), 150),
439 PowerAverage::new(Stockholm.dt(2025, 1, 3, 11, 0, 0), 250),
440 ];
441
442 let peaks = PowerPeaks::new(
443 TariffCalculationMethod::AverageDays(2),
444 &averages
445 .iter()
446 .map(|a| a.into_virtual(None))
447 .collect::<Vec<_>>(),
448 );
449
450 assert_eq!(peaks.values().len(), 2);
451 assert_eq!(peaks.values()[0].value, 600);
452 assert_eq!(peaks.values()[0].timestamp.day(), 2);
453 assert_eq!(peaks.values()[1].value, 500);
454 assert_eq!(peaks.values()[1].timestamp.day(), 1);
455 }
456
457 #[test]
458 fn average_days_ensures_different_days() {
459 let averages = vec![
460 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 500),
461 PowerAverage::new(Stockholm.dt(2025, 1, 1, 11, 0, 0), 450),
462 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 300),
463 ];
464
465 let peaks = PowerPeaks::new(
466 TariffCalculationMethod::AverageDays(2),
467 &averages
468 .iter()
469 .map(|a| a.into_virtual(None))
470 .collect::<Vec<_>>(),
471 );
472
473 assert_eq!(peaks.values().len(), 2);
474 let day1 = peaks.values()[0].timestamp.date_naive();
475 let day2 = peaks.values()[1].timestamp.date_naive();
476 assert_ne!(day1, day2);
477 }
478
479 #[test]
480 fn average_days_preserves_peak_hour_timestamp() {
481 let averages = vec![
482 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
483 PowerAverage::new(Stockholm.dt(2025, 1, 1, 14, 0, 0), 500),
484 PowerAverage::new(Stockholm.dt(2025, 1, 1, 20, 0, 0), 300),
485 ];
486
487 let peaks = PowerPeaks::new(
488 TariffCalculationMethod::AverageDays(1),
489 &averages
490 .iter()
491 .map(|a| a.into_virtual(None))
492 .collect::<Vec<_>>(),
493 );
494
495 assert_eq!(peaks.values().len(), 1);
496 assert_eq!(peaks.values()[0].timestamp.hour(), 14);
497 assert_eq!(peaks.values()[0].value, 500);
498 }
499
500 #[test]
501 fn average_days_empty_input() {
502 let averages = vec![];
503
504 let peaks = PowerPeaks::new(TariffCalculationMethod::AverageDays(3), &averages);
505
506 assert_eq!(peaks.values().len(), 0);
507 }
508
509 #[test]
510 fn average_days_zero_n() {
511 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100)];
512
513 let peaks = PowerPeaks::new(
514 TariffCalculationMethod::AverageDays(0),
515 &averages
516 .iter()
517 .map(|a| a.into_virtual(None))
518 .collect::<Vec<_>>(),
519 );
520
521 assert_eq!(peaks.values().len(), 0);
522 }
523
524 #[test]
525 fn average_days_n_greater_than_available_days() {
526 let averages = vec![
527 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
528 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 200),
529 ];
530
531 let peaks = PowerPeaks::new(
532 TariffCalculationMethod::AverageDays(5),
533 &averages
534 .iter()
535 .map(|a| a.into_virtual(None))
536 .collect::<Vec<_>>(),
537 );
538
539 assert_eq!(peaks.values().len(), 2);
540 }
541
542 #[test]
543 fn peak_periods_first_matching_splits_values() {
544 static PERIODS_ARRAY: [CostPeriod; 2] = [
545 CostPeriod::builder()
546 .load(LoadType::High)
547 .fixed_cost(10, 0)
548 .hours(6, 22)
549 .months(Month::November, Month::March)
550 .exclude_weekends()
551 .exclude_holidays(Country::SE)
552 .build(),
553 CostPeriod::builder()
554 .load(LoadType::Low)
555 .fixed_cost(5, 0)
556 .build(),
557 ];
558 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
559
560 let averages = vec![
562 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
564 PowerAverage::new(Stockholm.dt(2025, 1, 15, 23, 0, 0), 300),
566 PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400),
568 ];
569
570 let result = PowerTariffMatches::new(
571 TariffCalculationMethod::AverageHours(10),
572 None,
573 periods,
574 &averages
575 .iter()
576 .map(|a| a.into_virtual(None))
577 .collect::<Vec<_>>(),
578 None,
579 );
580
581 assert_eq!(result.items().len(), 2);
582
583 assert_eq!(result.items()[0].peaks().values().len(), 2);
585 assert_eq!(result.items()[0].peaks().values()[0].value, 500);
586 assert_eq!(result.items()[0].peaks().values()[1].value, 400);
587
588 assert_eq!(result.items()[1].peaks().values().len(), 1);
590 assert_eq!(result.items()[1].peaks().values()[0].value, 300);
591 }
592
593 #[test]
594 fn peak_periods_all_matching_duplicates_values() {
595 static PERIODS_ARRAY: [CostPeriod; 2] = [
596 CostPeriod::builder()
597 .load(LoadType::High)
598 .fixed_cost(10, 0)
599 .hours(6, 22)
600 .months(Month::November, Month::March)
601 .exclude_weekends()
602 .exclude_holidays(Country::SE)
603 .build(),
604 CostPeriod::builder()
605 .load(LoadType::Low)
606 .fixed_cost(5, 0)
607 .build(),
608 ];
609 let periods = CostPeriods::new_all(&PERIODS_ARRAY);
610
611 let averages = vec![
613 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
614 PowerAverage::new(Stockholm.dt(2025, 1, 15, 23, 0, 0), 300),
615 ];
616
617 let result = PowerTariffMatches::new(
618 TariffCalculationMethod::AverageHours(10),
619 None,
620 periods,
621 &averages
622 .iter()
623 .map(|a| a.into_virtual(None))
624 .collect::<Vec<_>>(),
625 None,
626 );
627
628 assert_eq!(result.items().len(), 2);
629
630 assert_eq!(result.items()[0].peaks().values().len(), 1);
632 assert_eq!(result.items()[0].peaks().values()[0].value, 500);
633
634 assert_eq!(result.items()[1].peaks().values().len(), 2);
636 assert_eq!(result.items()[1].peaks().values()[0].value, 500);
637 assert_eq!(result.items()[1].peaks().values()[1].value, 300);
638 }
639
640 #[test]
641 fn peak_periods_empty_averages() {
642 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
643 .load(LoadType::Low)
644 .fixed_cost(5, 0)
645 .build()];
646 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
647 let averages = vec![];
648
649 let result = PowerTariffMatches::new(
650 TariffCalculationMethod::AverageHours(3),
651 None,
652 periods,
653 &averages,
654 None,
655 );
656
657 assert_eq!(result.items().len(), 1);
658 assert_eq!(result.items()[0].peaks().values().len(), 0);
659 }
660
661 #[test]
662 fn single_value_both_methods() {
663 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100)];
664
665 let hours = PowerPeaks::new(
666 TariffCalculationMethod::AverageHours(3),
667 &averages
668 .iter()
669 .map(|a| a.into_virtual(None))
670 .collect::<Vec<_>>(),
671 );
672 assert_eq!(hours.values().len(), 1);
673 assert_eq!(hours.values()[0].value, 100);
674
675 let days = PowerPeaks::new(
676 TariffCalculationMethod::AverageDays(3),
677 &averages
678 .iter()
679 .map(|a| a.into_virtual(None))
680 .collect::<Vec<_>>(),
681 );
682 assert_eq!(days.values().len(), 1);
683 assert_eq!(days.values()[0].value, 100);
684 }
685
686 #[test]
687 fn covers_percentage_complete_overlap() {
688 let partial = PartialPowerAverage::new(
690 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
691 3600,
692 );
693
694 let ts = Stockholm.dt(2025, 1, 1, 10, 15, 0).with_timezone(&Utc); let result = partial.cover_percentage(ts, 1800); assert_eq!(result, 100); }
700
701 #[test]
702 fn covers_percentage_partial_overlap_start() {
703 let partial = PartialPowerAverage::new(
705 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
706 3600,
707 );
708
709 let ts = Stockholm.dt(2025, 1, 1, 9, 30, 0).with_timezone(&Utc);
712 let result = partial.cover_percentage(ts, 3600); assert_eq!(result, 50); }
716
717 #[test]
718 fn covers_percentage_partial_overlap_end() {
719 let partial = PartialPowerAverage::new(
721 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
722 3600,
723 );
724
725 let ts = Stockholm.dt(2025, 1, 1, 10, 30, 0).with_timezone(&Utc);
728 let result = partial.cover_percentage(ts, 3600); assert_eq!(result, 50); }
732
733 #[test]
734 fn covers_percentage_no_overlap_before() {
735 let partial = PartialPowerAverage::new(
737 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
738 3600,
739 );
740
741 let ts = Stockholm.dt(2025, 1, 1, 8, 0, 0).with_timezone(&Utc);
743 let result = partial.cover_percentage(ts, 3600);
744
745 assert_eq!(result, 0);
746 }
747
748 #[test]
749 fn covers_percentage_no_overlap_after() {
750 let partial = PartialPowerAverage::new(
752 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
753 3600,
754 );
755
756 let ts = Stockholm.dt(2025, 1, 1, 12, 0, 0).with_timezone(&Utc);
758 let result = partial.cover_percentage(ts, 3600);
759
760 assert_eq!(result, 0);
761 }
762
763 #[test]
764 fn covers_percentage_no_overlap() {
765 let partial = PartialPowerAverage::new(
767 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
768 3600,
769 );
770
771 let ts = Stockholm.dt(2025, 1, 1, 9, 0, 0).with_timezone(&Utc);
774 let result = partial.cover_percentage(ts, 3600);
775
776 assert_eq!(result, 0);
777 }
778
779 #[test]
780 fn covers_percentage_half_overlap() {
781 let partial = PartialPowerAverage::new(
783 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
784 3600,
785 );
786
787 let ts = Stockholm.dt(2025, 1, 1, 9, 30, 0).with_timezone(&Utc);
789 let result = partial.cover_percentage(ts, 3600);
790
791 assert_eq!(result, 50);
792 }
793
794 #[test]
795 fn covers_percentage_query_contains_partial() {
796 let partial = PartialPowerAverage::new(
798 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
799 3600,
800 );
801
802 let ts = Stockholm.dt(2025, 1, 1, 9, 0, 0).with_timezone(&Utc);
805 let result = partial.cover_percentage(ts, 10800);
806
807 assert_eq!(result, 33); }
809
810 #[test]
811 fn covers_percentage_exact_match() {
812 let partial = PartialPowerAverage::new(
814 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
815 3600,
816 );
817
818 let ts = Stockholm.dt(2025, 1, 1, 10, 0, 0).with_timezone(&Utc);
820 let result = partial.cover_percentage(ts, 3600);
821
822 assert_eq!(result, 100);
823 }
824
825 #[test]
826 fn covers_percentage_adjacent_ranges_no_overlap() {
827 let partial = PartialPowerAverage::new(
829 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
830 3600,
831 );
832
833 let ts = Stockholm.dt(2025, 1, 1, 11, 0, 0).with_timezone(&Utc);
835 let result = partial.cover_percentage(ts, 3600);
836
837 assert_eq!(result, 0); }
839
840 #[test]
841 fn covers_percentage_very_small_overlap() {
842 let partial = PartialPowerAverage::new(
844 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
845 3600,
846 );
847
848 let ts = Stockholm.dt(2025, 1, 1, 10, 59, 59).with_timezone(&Utc);
850 let result = partial.cover_percentage(ts, 3600);
851
852 assert_eq!(result, 0);
854 }
855
856 #[test]
857 fn covers_percentage_short_duration() {
858 let partial = PartialPowerAverage::new(
860 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
861 60,
862 );
863
864 let ts = Stockholm.dt(2025, 1, 1, 10, 0, 30).with_timezone(&Utc);
866 let result = partial.cover_percentage(ts, 60);
867
868 assert_eq!(result, 50); }
870
871 mod map_periods_to_data_tests {
872 use super::*;
873 use crate::{LoadType, Stockholm, months::Month};
874
875 #[test]
876 fn map_periods_first_matching_no_overlap() {
877 static PERIODS_ARRAY: [CostPeriod; 2] = [
879 CostPeriod::builder()
880 .load(LoadType::High)
881 .fixed_cost(10, 0)
882 .hours(6, 12)
883 .build(),
884 CostPeriod::builder()
885 .load(LoadType::Low)
886 .fixed_cost(5, 0)
887 .hours(12, 22)
888 .build(),
889 ];
890 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
891
892 let averages = vec![
893 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500), PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400), ];
896
897 let result = PeriodPeakMatches::new(
898 TariffCalculationMethod::AverageHours(10),
899 &periods,
900 &averages
901 .iter()
902 .map(|a| a.into_virtual(None))
903 .collect::<Vec<_>>(),
904 CostPeriodMatching::First,
905 );
906
907 assert_eq!(result.len(), 2);
908 assert_eq!(result[0].peaks().values().len(), 1);
909 assert_eq!(result[0].peaks().values()[0].value, 500);
910 assert_eq!(result[1].peaks().values().len(), 1);
911 assert_eq!(result[1].peaks().values()[0].value, 400);
912 }
913
914 #[test]
915 fn map_periods_first_matching_overlapping_periods() {
916 static PERIODS_ARRAY: [CostPeriod; 2] = [
918 CostPeriod::builder()
919 .load(LoadType::High)
920 .fixed_cost(10, 0)
921 .hours(6, 18) .build(),
923 CostPeriod::builder()
924 .load(LoadType::Low)
925 .fixed_cost(5, 0)
926 .hours(12, 22) .build(),
928 ];
929 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
930
931 let averages = vec![
932 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), ];
936
937 let result = PeriodPeakMatches::new(
938 TariffCalculationMethod::AverageHours(10),
939 &periods,
940 &averages
941 .iter()
942 .map(|a| a.into_virtual(None))
943 .collect::<Vec<_>>(),
944 CostPeriodMatching::First,
945 );
946
947 assert_eq!(result.len(), 2);
948 assert_eq!(result[0].peaks().values().len(), 2);
950 assert_eq!(result[0].peaks().values()[0].value, 500);
951 assert_eq!(result[0].peaks().values()[1].value, 400);
952 assert_eq!(result[1].peaks().values().len(), 1);
954 assert_eq!(result[1].peaks().values()[0].value, 300);
955 }
956
957 #[test]
958 fn map_periods_all_matching_duplicates() {
959 static PERIODS_ARRAY: [CostPeriod; 2] = [
961 CostPeriod::builder()
962 .load(LoadType::High)
963 .fixed_cost(10, 0)
964 .hours(6, 18)
965 .build(),
966 CostPeriod::builder()
967 .load(LoadType::Base)
968 .fixed_cost(5, 0)
969 .build(),
970 ];
971 let periods = CostPeriods::new_all(&PERIODS_ARRAY);
972
973 let averages = vec![
974 PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400), ];
976
977 let result = PeriodPeakMatches::new(
978 TariffCalculationMethod::AverageHours(10),
979 &periods,
980 &averages
981 .iter()
982 .map(|a| a.into_virtual(None))
983 .collect::<Vec<_>>(),
984 CostPeriodMatching::All,
985 );
986
987 assert_eq!(result.len(), 2);
988 assert_eq!(result[0].peaks().values().len(), 1);
990 assert_eq!(result[0].peaks().values()[0].value, 400);
991 assert_eq!(result[1].peaks().values().len(), 1);
992 assert_eq!(result[1].peaks().values()[0].value, 400);
993 }
994
995 #[test]
996 fn map_periods_empty_averages() {
997 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
998 .load(LoadType::Low)
999 .fixed_cost(5, 0)
1000 .build()];
1001 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
1002 let averages = vec![];
1003
1004 let result = PeriodPeakMatches::new(
1005 TariffCalculationMethod::AverageHours(3),
1006 &periods,
1007 &averages,
1008 CostPeriodMatching::First,
1009 );
1010
1011 assert_eq!(result.len(), 1);
1012 assert_eq!(result[0].peaks().values().len(), 0);
1013 }
1014
1015 #[test]
1016 fn map_periods_no_matching_averages() {
1017 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
1019 .load(LoadType::High)
1020 .fixed_cost(10, 0)
1021 .hours(6, 12)
1022 .months(Month::June, Month::August) .build()];
1024 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
1025
1026 let averages = vec![PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500)];
1028
1029 let result = PeriodPeakMatches::new(
1030 TariffCalculationMethod::AverageHours(10),
1031 &periods,
1032 &averages
1033 .iter()
1034 .map(|a| a.into_virtual(None))
1035 .collect::<Vec<_>>(),
1036 CostPeriodMatching::First,
1037 );
1038
1039 assert_eq!(result.len(), 1);
1040 assert_eq!(result[0].peaks().values().len(), 0);
1041 }
1042
1043 #[test]
1044 fn map_periods_preserves_calculation_method() {
1045 static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
1046 .load(LoadType::High)
1047 .fixed_cost(10, 0)
1048 .build()];
1049 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
1050
1051 let averages = vec![
1053 PowerAverage::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 500),
1054 PowerAverage::new(Stockholm.dt(2025, 1, 1, 14, 0, 0), 600),
1055 PowerAverage::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 300),
1056 PowerAverage::new(Stockholm.dt(2025, 1, 2, 14, 0, 0), 400),
1057 ];
1058
1059 let result_days = PeriodPeakMatches::new(
1061 TariffCalculationMethod::AverageDays(1),
1062 &periods,
1063 &averages
1064 .iter()
1065 .map(|a| a.into_virtual(None))
1066 .collect::<Vec<_>>(),
1067 CostPeriodMatching::First,
1068 );
1069 assert_eq!(result_days[0].peaks().values().len(), 1);
1070 assert_eq!(result_days[0].peaks().values()[0].value, 600); let result_hours = PeriodPeakMatches::new(
1074 TariffCalculationMethod::AverageHours(2),
1075 &periods,
1076 &averages
1077 .iter()
1078 .map(|a| a.into_virtual(None))
1079 .collect::<Vec<_>>(),
1080 CostPeriodMatching::First,
1081 );
1082 assert_eq!(result_hours[0].peaks().values().len(), 2);
1083 assert_eq!(result_hours[0].peaks().values()[0].value, 600);
1084 assert_eq!(result_hours[0].peaks().values()[1].value, 500);
1085 }
1086
1087 #[test]
1088 fn map_periods_first_matching_multiple_periods() {
1089 static PERIODS_ARRAY: [CostPeriod; 3] = [
1091 CostPeriod::builder()
1092 .load(LoadType::High)
1093 .fixed_cost(10, 0)
1094 .hours(6, 12)
1095 .build(),
1096 CostPeriod::builder()
1097 .load(LoadType::Base)
1098 .fixed_cost(7, 50)
1099 .hours(12, 18)
1100 .build(),
1101 CostPeriod::builder()
1102 .load(LoadType::Low)
1103 .fixed_cost(5, 0)
1104 .build(),
1105 ];
1106 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
1107
1108 let averages = vec![
1109 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), ];
1114
1115 let result = PeriodPeakMatches::new(
1116 TariffCalculationMethod::AverageHours(10),
1117 &periods,
1118 &averages
1119 .iter()
1120 .map(|a| a.into_virtual(None))
1121 .collect::<Vec<_>>(),
1122 CostPeriodMatching::First,
1123 );
1124
1125 assert_eq!(result.len(), 3);
1126 assert_eq!(result[0].peaks().values().len(), 1); assert_eq!(result[0].peaks().values()[0].value, 500);
1128 assert_eq!(result[1].peaks().values().len(), 1); assert_eq!(result[1].peaks().values()[0].value, 400);
1130 assert_eq!(result[2].peaks().values().len(), 2); assert_eq!(result[2].peaks().values()[0].value, 300);
1132 assert_eq!(result[2].peaks().values()[1].value, 200);
1133 }
1134
1135 #[test]
1136 fn map_periods_all_matching_catch_all_period() {
1137 static PERIODS_ARRAY: [CostPeriod; 2] = [
1139 CostPeriod::builder()
1140 .load(LoadType::High)
1141 .fixed_cost(10, 0)
1142 .hours(6, 12)
1143 .build(),
1144 CostPeriod::builder()
1145 .load(LoadType::Low)
1146 .fixed_cost(5, 0)
1147 .build(), ];
1149 let periods = CostPeriods::new_all(&PERIODS_ARRAY);
1150
1151 let averages = vec![
1152 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500), PowerAverage::new(Stockholm.dt(2025, 1, 15, 20, 0, 0), 300), ];
1155
1156 let result = PeriodPeakMatches::new(
1157 TariffCalculationMethod::AverageHours(10),
1158 &periods,
1159 &averages
1160 .iter()
1161 .map(|a| a.into_virtual(None))
1162 .collect::<Vec<_>>(),
1163 CostPeriodMatching::All,
1164 );
1165
1166 assert_eq!(result.len(), 2);
1167 assert_eq!(result[0].peaks().values().len(), 1);
1169 assert_eq!(result[0].peaks().values()[0].value, 500);
1170 assert_eq!(result[1].peaks().values().len(), 2);
1172 assert_eq!(result[1].peaks().values()[0].value, 500);
1173 assert_eq!(result[1].peaks().values()[1].value, 300);
1174 }
1175
1176 #[test]
1177 fn map_periods_order_preservation() {
1178 static PERIODS_ARRAY: [CostPeriod; 2] = [
1180 CostPeriod::builder()
1181 .load(LoadType::High)
1182 .fixed_cost(10, 0)
1183 .hours(6, 12)
1184 .build(),
1185 CostPeriod::builder()
1186 .load(LoadType::Low)
1187 .fixed_cost(5, 0)
1188 .hours(12, 18)
1189 .build(),
1190 ];
1191 let periods = CostPeriods::new_first(&PERIODS_ARRAY);
1192
1193 let averages = vec![
1194 PowerAverage::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
1195 PowerAverage::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400),
1196 ];
1197
1198 let result = PeriodPeakMatches::new(
1199 TariffCalculationMethod::AverageHours(10),
1200 &periods,
1201 &averages
1202 .iter()
1203 .map(|a| a.into_virtual(None))
1204 .collect::<Vec<_>>(),
1205 CostPeriodMatching::First,
1206 );
1207
1208 assert_eq!(result.len(), 2);
1209 assert_eq!(result[0].peaks().values()[0].value, 500); assert_eq!(result[1].peaks().values()[0].value, 400); }
1213 }
1214}