grid_tariffs/
peaks.rs

1use std::collections::HashMap;
2
3use chrono::{DateTime, Datelike};
4use chrono_tz::Tz;
5
6use crate::{CostPeriod, CostPeriodMatching, CostPeriods, PowerTariff, TariffCalculationMethod};
7
8#[derive(Clone, Debug)]
9pub struct PeakPeriods {
10    calc_method: TariffCalculationMethod,
11    matching_method: CostPeriodMatching,
12    items: Vec<PeriodDemand>,
13}
14
15impl PeakPeriods {
16    pub fn new(
17        calc_method: TariffCalculationMethod,
18        periods: CostPeriods,
19        mut averages: Vec<AverageDemand>,
20    ) -> Self {
21        let mut periods_averages_map = HashMap::new();
22
23        for (p_idx, period) in periods.iter().enumerate() {
24            let mut averages_for_period = vec![];
25
26            for a_idx in (0..averages.len()).rev() {
27                if period.matches(averages[a_idx].timestamp) {
28                    averages_for_period.push(averages[a_idx].clone());
29
30                    if periods.match_method() == CostPeriodMatching::First {
31                        averages.remove(a_idx);
32                    }
33                }
34            }
35
36            periods_averages_map.insert(p_idx, averages_for_period);
37        }
38
39        let items = periods
40            .clone()
41            .iter()
42            .enumerate()
43            .map(|(p_idx, period)| PeriodDemand {
44                period: period.clone(),
45                peaks: PeakDemands::new(
46                    calc_method,
47                    period.clone(),
48                    periods_averages_map.remove(&p_idx).unwrap(),
49                ),
50            })
51            .collect();
52
53        Self {
54            calc_method,
55            matching_method: periods.match_method(),
56            items,
57        }
58    }
59
60    pub fn items(&self) -> &[PeriodDemand] {
61        &self.items
62    }
63}
64
65#[derive(Clone, Debug)]
66pub struct PeriodDemand {
67    period: CostPeriod,
68    peaks: PeakDemands,
69}
70
71impl PeriodDemand {
72    pub fn period(&self) -> &CostPeriod {
73        &self.period
74    }
75    pub fn peaks(&self) -> &PeakDemands {
76        &self.peaks
77    }
78}
79
80#[derive(Clone, Debug, PartialEq)]
81pub struct AverageDemand {
82    pub timestamp: DateTime<Tz>,
83    pub value: u32,
84}
85
86#[derive(Default, Clone, Debug)]
87pub struct PeakDemands(Vec<AverageDemand>);
88
89impl PeakDemands {
90    pub fn new(
91        calc_method: TariffCalculationMethod,
92        period: CostPeriod,
93        mut period_averages: Vec<AverageDemand>,
94    ) -> Self {
95        let peak_demands: Vec<AverageDemand> = match calc_method {
96            crate::TariffCalculationMethod::AverageDays(n) => {
97                // For AverageDays: get the highest hour from each of the top n days
98                let mut daily_peaks: HashMap<chrono::NaiveDate, AverageDemand> = HashMap::new();
99
100                // Group by day and keep only the highest value for each day
101                for demand in period_averages.clone() {
102                    let date = demand.timestamp.date_naive();
103                    daily_peaks
104                        .entry(date)
105                        .and_modify(|existing| {
106                            if demand.value > existing.value {
107                                *existing = demand.clone();
108                            }
109                        })
110                        .or_insert(demand);
111                }
112
113                // Convert to vector and sort by value descending
114                let mut daily_peaks_vec: Vec<AverageDemand> = daily_peaks.into_values().collect();
115                daily_peaks_vec.sort_by(|a, b| b.value.cmp(&a.value));
116
117                // Take the top n days
118                daily_peaks_vec.into_iter().take(n as usize).collect()
119            }
120            crate::TariffCalculationMethod::AverageHours(n) => {
121                // For AverageHours: get the top n hours by value
122                period_averages.sort_by(|a, b| b.value.cmp(&a.value));
123                period_averages.into_iter().take(n as usize).collect()
124            }
125        };
126
127        Self(peak_demands)
128    }
129
130    pub fn values(&self) -> &[AverageDemand] {
131        &self.0
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::{Country, costs::LoadType, defs::Month};
139    use chrono::{TimeZone, Timelike};
140    use chrono_tz::Europe::Stockholm;
141
142    fn create_simple_period() -> &'static CostPeriod {
143        const PERIOD: CostPeriod = CostPeriod::builder()
144            .load(LoadType::Low)
145            .fixed_cost(5, 0)
146            .build();
147        &PERIOD
148    }
149
150    fn create_high_load_period() -> &'static CostPeriod {
151        const PERIOD: CostPeriod = CostPeriod::builder()
152            .load(LoadType::High)
153            .fixed_cost(10, 0)
154            .hours(6, 22)
155            .months(Month::November, Month::March)
156            .exclude_weekends()
157            .exclude_holidays(Country::SE)
158            .build();
159        &PERIOD
160    }
161
162    #[test]
163    fn average_hours_returns_n_highest_values() {
164        let period = create_simple_period().clone();
165        let averages = vec![
166            AverageDemand {
167                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
168                value: 100,
169            },
170            AverageDemand {
171                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(),
172                value: 500,
173            },
174            AverageDemand {
175                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 2, 0, 0).unwrap(),
176                value: 300,
177            },
178            AverageDemand {
179                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 3, 0, 0).unwrap(),
180                value: 200,
181            },
182            AverageDemand {
183                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 4, 0, 0).unwrap(),
184                value: 400,
185            },
186        ];
187
188        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(3), period, averages);
189
190        assert_eq!(peaks.values().len(), 3);
191        assert_eq!(peaks.values()[0].value, 500);
192        assert_eq!(peaks.values()[1].value, 400);
193        assert_eq!(peaks.values()[2].value, 300);
194    }
195
196    #[test]
197    fn average_hours_with_equal_values() {
198        let period = create_simple_period().clone();
199        let averages = vec![
200            AverageDemand {
201                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
202                value: 500,
203            },
204            AverageDemand {
205                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(),
206                value: 500,
207            },
208            AverageDemand {
209                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 2, 0, 0).unwrap(),
210                value: 300,
211            },
212        ];
213
214        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(2), period, averages);
215
216        assert_eq!(peaks.values().len(), 2);
217        assert_eq!(peaks.values()[0].value, 500);
218        assert_eq!(peaks.values()[1].value, 500);
219    }
220
221    #[test]
222    fn average_hours_empty_input() {
223        let period = create_simple_period().clone();
224        let averages = vec![];
225
226        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(3), period, averages);
227
228        assert_eq!(peaks.values().len(), 0);
229    }
230
231    #[test]
232    fn average_hours_zero_n() {
233        let period = create_simple_period().clone();
234        let averages = vec![AverageDemand {
235            timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
236            value: 100,
237        }];
238
239        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(0), period, averages);
240
241        assert_eq!(peaks.values().len(), 0);
242    }
243
244    #[test]
245    fn average_hours_n_greater_than_available() {
246        let period = create_simple_period().clone();
247        let averages = vec![
248            AverageDemand {
249                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
250                value: 100,
251            },
252            AverageDemand {
253                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(),
254                value: 200,
255            },
256        ];
257
258        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(5), period, averages);
259
260        assert_eq!(peaks.values().len(), 2);
261        assert_eq!(peaks.values()[0].value, 200);
262        assert_eq!(peaks.values()[1].value, 100);
263    }
264
265    #[test]
266    fn average_days_one_peak_per_day() {
267        let period = create_simple_period().clone();
268        let averages = vec![
269            // Day 1: peak at 500
270            AverageDemand {
271                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
272                value: 100,
273            },
274            AverageDemand {
275                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 11, 0, 0).unwrap(),
276                value: 500,
277            },
278            AverageDemand {
279                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap(),
280                value: 300,
281            },
282            // Day 2: peak at 600
283            AverageDemand {
284                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap(),
285                value: 200,
286            },
287            AverageDemand {
288                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 11, 0, 0).unwrap(),
289                value: 600,
290            },
291            // Day 3: peak at 250
292            AverageDemand {
293                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 3, 10, 0, 0).unwrap(),
294                value: 150,
295            },
296            AverageDemand {
297                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 3, 11, 0, 0).unwrap(),
298                value: 250,
299            },
300        ];
301
302        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(2), period, averages);
303
304        assert_eq!(peaks.values().len(), 2);
305        assert_eq!(peaks.values()[0].value, 600);
306        assert_eq!(peaks.values()[0].timestamp.day(), 2);
307        assert_eq!(peaks.values()[1].value, 500);
308        assert_eq!(peaks.values()[1].timestamp.day(), 1);
309    }
310
311    #[test]
312    fn average_days_ensures_different_days() {
313        let period = create_simple_period().clone();
314        let averages = vec![
315            AverageDemand {
316                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
317                value: 500,
318            },
319            AverageDemand {
320                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 11, 0, 0).unwrap(),
321                value: 450,
322            },
323            AverageDemand {
324                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap(),
325                value: 300,
326            },
327        ];
328
329        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(2), period, averages);
330
331        assert_eq!(peaks.values().len(), 2);
332        let day1 = peaks.values()[0].timestamp.date_naive();
333        let day2 = peaks.values()[1].timestamp.date_naive();
334        assert_ne!(day1, day2);
335    }
336
337    #[test]
338    fn average_days_preserves_peak_hour_timestamp() {
339        let period = create_simple_period().clone();
340        let averages = vec![
341            AverageDemand {
342                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
343                value: 100,
344            },
345            AverageDemand {
346                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap(),
347                value: 500,
348            },
349            AverageDemand {
350                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 20, 0, 0).unwrap(),
351                value: 300,
352            },
353        ];
354
355        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(1), period, averages);
356
357        assert_eq!(peaks.values().len(), 1);
358        assert_eq!(peaks.values()[0].timestamp.hour(), 14);
359        assert_eq!(peaks.values()[0].value, 500);
360    }
361
362    #[test]
363    fn average_days_empty_input() {
364        let period = create_simple_period().clone();
365        let averages = vec![];
366
367        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(3), period, averages);
368
369        assert_eq!(peaks.values().len(), 0);
370    }
371
372    #[test]
373    fn average_days_zero_n() {
374        let period = create_simple_period().clone();
375        let averages = vec![AverageDemand {
376            timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
377            value: 100,
378        }];
379
380        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(0), period, averages);
381
382        assert_eq!(peaks.values().len(), 0);
383    }
384
385    #[test]
386    fn average_days_n_greater_than_available_days() {
387        let period = create_simple_period().clone();
388        let averages = vec![
389            AverageDemand {
390                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
391                value: 100,
392            },
393            AverageDemand {
394                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap(),
395                value: 200,
396            },
397        ];
398
399        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(5), period, averages);
400
401        assert_eq!(peaks.values().len(), 2);
402    }
403
404    #[test]
405    fn peak_periods_first_matching_splits_values() {
406        static PERIODS_ARRAY: [CostPeriod; 2] = [
407            CostPeriod::builder()
408                .load(LoadType::High)
409                .fixed_cost(10, 0)
410                .hours(6, 22)
411                .months(Month::November, Month::March)
412                .exclude_weekends()
413                .exclude_holidays(Country::SE)
414                .build(),
415            CostPeriod::builder()
416                .load(LoadType::Low)
417                .fixed_cost(5, 0)
418                .build(),
419        ];
420        let periods = CostPeriods::new_first(&PERIODS_ARRAY);
421
422        // January 15, 2025 is a Wednesday
423        let averages = vec![
424            // Matches high: winter weekday 10:00
425            AverageDemand {
426                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap(),
427                value: 500,
428            },
429            // Doesn't match high (hour 23): goes to low
430            AverageDemand {
431                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap(),
432                value: 300,
433            },
434            // Matches high: winter weekday 14:00
435            AverageDemand {
436                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap(),
437                value: 400,
438            },
439        ];
440
441        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(10), periods, averages);
442
443        assert_eq!(result.items().len(), 2);
444
445        // High period gets 2 values (10:00, 14:00)
446        assert_eq!(result.items()[0].peaks().values().len(), 2);
447        assert_eq!(result.items()[0].peaks().values()[0].value, 500);
448        assert_eq!(result.items()[0].peaks().values()[1].value, 400);
449
450        // Low period gets 1 value (23:00)
451        assert_eq!(result.items()[1].peaks().values().len(), 1);
452        assert_eq!(result.items()[1].peaks().values()[0].value, 300);
453    }
454
455    #[test]
456    fn peak_periods_all_matching_duplicates_values() {
457        static PERIODS_ARRAY: [CostPeriod; 2] = [
458            CostPeriod::builder()
459                .load(LoadType::High)
460                .fixed_cost(10, 0)
461                .hours(6, 22)
462                .months(Month::November, Month::March)
463                .exclude_weekends()
464                .exclude_holidays(Country::SE)
465                .build(),
466            CostPeriod::builder()
467                .load(LoadType::Low)
468                .fixed_cost(5, 0)
469                .build(),
470        ];
471        let periods = CostPeriods::new_all(&PERIODS_ARRAY);
472
473        // January 15, 2025 is a Wednesday
474        let averages = vec![
475            AverageDemand {
476                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap(),
477                value: 500,
478            },
479            AverageDemand {
480                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap(),
481                value: 300,
482            },
483        ];
484
485        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(10), periods, averages);
486
487        assert_eq!(result.items().len(), 2);
488
489        // High period gets 1 value (only 10:00 matches criteria)
490        assert_eq!(result.items()[0].peaks().values().len(), 1);
491        assert_eq!(result.items()[0].peaks().values()[0].value, 500);
492
493        // Low period gets both (no restrictions)
494        assert_eq!(result.items()[1].peaks().values().len(), 2);
495        assert_eq!(result.items()[1].peaks().values()[0].value, 500);
496        assert_eq!(result.items()[1].peaks().values()[1].value, 300);
497    }
498
499    #[test]
500    fn peak_periods_empty_averages() {
501        static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
502            .load(LoadType::Low)
503            .fixed_cost(5, 0)
504            .build()];
505        let periods = CostPeriods::new_first(&PERIODS_ARRAY);
506        let averages = vec![];
507
508        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(3), periods, averages);
509
510        assert_eq!(result.items().len(), 1);
511        assert_eq!(result.items()[0].peaks().values().len(), 0);
512    }
513
514    #[test]
515    fn single_value_both_methods() {
516        let period = create_simple_period().clone();
517        let averages = vec![AverageDemand {
518            timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
519            value: 100,
520        }];
521
522        let hours = PeakDemands::new(
523            TariffCalculationMethod::AverageHours(3),
524            period.clone(),
525            averages.clone(),
526        );
527        assert_eq!(hours.values().len(), 1);
528        assert_eq!(hours.values()[0].value, 100);
529
530        let days = PeakDemands::new(TariffCalculationMethod::AverageDays(3), period, averages);
531        assert_eq!(days.values().len(), 1);
532        assert_eq!(days.values()[0].value, 100);
533    }
534}