grid_tariffs/
peaks.rs

1use std::collections::HashMap;
2
3use chrono::DateTime;
4use chrono_tz::Tz;
5
6use crate::{CostPeriod, CostPeriodMatching, CostPeriods, 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::{Datelike, 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    #[test]
151    fn average_hours_returns_n_highest_values() {
152        let period = create_simple_period().clone();
153        let averages = vec![
154            AverageDemand {
155                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
156                value: 100,
157            },
158            AverageDemand {
159                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(),
160                value: 500,
161            },
162            AverageDemand {
163                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 2, 0, 0).unwrap(),
164                value: 300,
165            },
166            AverageDemand {
167                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 3, 0, 0).unwrap(),
168                value: 200,
169            },
170            AverageDemand {
171                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 4, 0, 0).unwrap(),
172                value: 400,
173            },
174        ];
175
176        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(3), period, averages);
177
178        assert_eq!(peaks.values().len(), 3);
179        assert_eq!(peaks.values()[0].value, 500);
180        assert_eq!(peaks.values()[1].value, 400);
181        assert_eq!(peaks.values()[2].value, 300);
182    }
183
184    #[test]
185    fn average_hours_with_equal_values() {
186        let period = create_simple_period().clone();
187        let averages = vec![
188            AverageDemand {
189                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
190                value: 500,
191            },
192            AverageDemand {
193                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(),
194                value: 500,
195            },
196            AverageDemand {
197                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 2, 0, 0).unwrap(),
198                value: 300,
199            },
200        ];
201
202        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(2), period, averages);
203
204        assert_eq!(peaks.values().len(), 2);
205        assert_eq!(peaks.values()[0].value, 500);
206        assert_eq!(peaks.values()[1].value, 500);
207    }
208
209    #[test]
210    fn average_hours_empty_input() {
211        let period = create_simple_period().clone();
212        let averages = vec![];
213
214        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(3), period, averages);
215
216        assert_eq!(peaks.values().len(), 0);
217    }
218
219    #[test]
220    fn average_hours_zero_n() {
221        let period = create_simple_period().clone();
222        let averages = vec![AverageDemand {
223            timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
224            value: 100,
225        }];
226
227        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(0), period, averages);
228
229        assert_eq!(peaks.values().len(), 0);
230    }
231
232    #[test]
233    fn average_hours_n_greater_than_available() {
234        let period = create_simple_period().clone();
235        let averages = vec![
236            AverageDemand {
237                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
238                value: 100,
239            },
240            AverageDemand {
241                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 1, 0, 0).unwrap(),
242                value: 200,
243            },
244        ];
245
246        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(5), period, averages);
247
248        assert_eq!(peaks.values().len(), 2);
249        assert_eq!(peaks.values()[0].value, 200);
250        assert_eq!(peaks.values()[1].value, 100);
251    }
252
253    #[test]
254    fn average_days_one_peak_per_day() {
255        let period = create_simple_period().clone();
256        let averages = vec![
257            // Day 1: peak at 500
258            AverageDemand {
259                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
260                value: 100,
261            },
262            AverageDemand {
263                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 11, 0, 0).unwrap(),
264                value: 500,
265            },
266            AverageDemand {
267                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap(),
268                value: 300,
269            },
270            // Day 2: peak at 600
271            AverageDemand {
272                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap(),
273                value: 200,
274            },
275            AverageDemand {
276                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 11, 0, 0).unwrap(),
277                value: 600,
278            },
279            // Day 3: peak at 250
280            AverageDemand {
281                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 3, 10, 0, 0).unwrap(),
282                value: 150,
283            },
284            AverageDemand {
285                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 3, 11, 0, 0).unwrap(),
286                value: 250,
287            },
288        ];
289
290        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(2), period, averages);
291
292        assert_eq!(peaks.values().len(), 2);
293        assert_eq!(peaks.values()[0].value, 600);
294        assert_eq!(peaks.values()[0].timestamp.day(), 2);
295        assert_eq!(peaks.values()[1].value, 500);
296        assert_eq!(peaks.values()[1].timestamp.day(), 1);
297    }
298
299    #[test]
300    fn average_days_ensures_different_days() {
301        let period = create_simple_period().clone();
302        let averages = vec![
303            AverageDemand {
304                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
305                value: 500,
306            },
307            AverageDemand {
308                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 11, 0, 0).unwrap(),
309                value: 450,
310            },
311            AverageDemand {
312                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap(),
313                value: 300,
314            },
315        ];
316
317        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(2), period, averages);
318
319        assert_eq!(peaks.values().len(), 2);
320        let day1 = peaks.values()[0].timestamp.date_naive();
321        let day2 = peaks.values()[1].timestamp.date_naive();
322        assert_ne!(day1, day2);
323    }
324
325    #[test]
326    fn average_days_preserves_peak_hour_timestamp() {
327        let period = create_simple_period().clone();
328        let averages = vec![
329            AverageDemand {
330                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
331                value: 100,
332            },
333            AverageDemand {
334                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap(),
335                value: 500,
336            },
337            AverageDemand {
338                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 20, 0, 0).unwrap(),
339                value: 300,
340            },
341        ];
342
343        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(1), period, averages);
344
345        assert_eq!(peaks.values().len(), 1);
346        assert_eq!(peaks.values()[0].timestamp.hour(), 14);
347        assert_eq!(peaks.values()[0].value, 500);
348    }
349
350    #[test]
351    fn average_days_empty_input() {
352        let period = create_simple_period().clone();
353        let averages = vec![];
354
355        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(3), period, averages);
356
357        assert_eq!(peaks.values().len(), 0);
358    }
359
360    #[test]
361    fn average_days_zero_n() {
362        let period = create_simple_period().clone();
363        let averages = vec![AverageDemand {
364            timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap(),
365            value: 100,
366        }];
367
368        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(0), period, averages);
369
370        assert_eq!(peaks.values().len(), 0);
371    }
372
373    #[test]
374    fn average_days_n_greater_than_available_days() {
375        let period = create_simple_period().clone();
376        let averages = vec![
377            AverageDemand {
378                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
379                value: 100,
380            },
381            AverageDemand {
382                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap(),
383                value: 200,
384            },
385        ];
386
387        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(5), period, averages);
388
389        assert_eq!(peaks.values().len(), 2);
390    }
391
392    #[test]
393    fn peak_periods_first_matching_splits_values() {
394        static PERIODS_ARRAY: [CostPeriod; 2] = [
395            CostPeriod::builder()
396                .load(LoadType::High)
397                .fixed_cost(10, 0)
398                .hours(6, 22)
399                .months(Month::November, Month::March)
400                .exclude_weekends()
401                .exclude_holidays(Country::SE)
402                .build(),
403            CostPeriod::builder()
404                .load(LoadType::Low)
405                .fixed_cost(5, 0)
406                .build(),
407        ];
408        let periods = CostPeriods::new_first(&PERIODS_ARRAY);
409
410        // January 15, 2025 is a Wednesday
411        let averages = vec![
412            // Matches high: winter weekday 10:00
413            AverageDemand {
414                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap(),
415                value: 500,
416            },
417            // Doesn't match high (hour 23): goes to low
418            AverageDemand {
419                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap(),
420                value: 300,
421            },
422            // Matches high: winter weekday 14:00
423            AverageDemand {
424                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap(),
425                value: 400,
426            },
427        ];
428
429        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(10), periods, averages);
430
431        assert_eq!(result.items().len(), 2);
432
433        // High period gets 2 values (10:00, 14:00)
434        assert_eq!(result.items()[0].peaks().values().len(), 2);
435        assert_eq!(result.items()[0].peaks().values()[0].value, 500);
436        assert_eq!(result.items()[0].peaks().values()[1].value, 400);
437
438        // Low period gets 1 value (23:00)
439        assert_eq!(result.items()[1].peaks().values().len(), 1);
440        assert_eq!(result.items()[1].peaks().values()[0].value, 300);
441    }
442
443    #[test]
444    fn peak_periods_all_matching_duplicates_values() {
445        static PERIODS_ARRAY: [CostPeriod; 2] = [
446            CostPeriod::builder()
447                .load(LoadType::High)
448                .fixed_cost(10, 0)
449                .hours(6, 22)
450                .months(Month::November, Month::March)
451                .exclude_weekends()
452                .exclude_holidays(Country::SE)
453                .build(),
454            CostPeriod::builder()
455                .load(LoadType::Low)
456                .fixed_cost(5, 0)
457                .build(),
458        ];
459        let periods = CostPeriods::new_all(&PERIODS_ARRAY);
460
461        // January 15, 2025 is a Wednesday
462        let averages = vec![
463            AverageDemand {
464                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap(),
465                value: 500,
466            },
467            AverageDemand {
468                timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap(),
469                value: 300,
470            },
471        ];
472
473        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(10), periods, averages);
474
475        assert_eq!(result.items().len(), 2);
476
477        // High period gets 1 value (only 10:00 matches criteria)
478        assert_eq!(result.items()[0].peaks().values().len(), 1);
479        assert_eq!(result.items()[0].peaks().values()[0].value, 500);
480
481        // Low period gets both (no restrictions)
482        assert_eq!(result.items()[1].peaks().values().len(), 2);
483        assert_eq!(result.items()[1].peaks().values()[0].value, 500);
484        assert_eq!(result.items()[1].peaks().values()[1].value, 300);
485    }
486
487    #[test]
488    fn peak_periods_empty_averages() {
489        static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
490            .load(LoadType::Low)
491            .fixed_cost(5, 0)
492            .build()];
493        let periods = CostPeriods::new_first(&PERIODS_ARRAY);
494        let averages = vec![];
495
496        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(3), periods, averages);
497
498        assert_eq!(result.items().len(), 1);
499        assert_eq!(result.items()[0].peaks().values().len(), 0);
500    }
501
502    #[test]
503    fn single_value_both_methods() {
504        let period = create_simple_period().clone();
505        let averages = vec![AverageDemand {
506            timestamp: Stockholm.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
507            value: 100,
508        }];
509
510        let hours = PeakDemands::new(
511            TariffCalculationMethod::AverageHours(3),
512            period.clone(),
513            averages.clone(),
514        );
515        assert_eq!(hours.values().len(), 1);
516        assert_eq!(hours.values()[0].value, 100);
517
518        let days = PeakDemands::new(TariffCalculationMethod::AverageDays(3), period, averages);
519        assert_eq!(days.values().len(), 1);
520        assert_eq!(days.values()[0].value, 100);
521    }
522}