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    items: Vec<PeriodDemand>,
11}
12
13impl PeakPeriods {
14    pub fn new(
15        calc_method: TariffCalculationMethod,
16        periods: CostPeriods,
17        mut averages: Vec<AverageDemand>,
18    ) -> Self {
19        let mut periods_averages_map = HashMap::new();
20
21        for (p_idx, period) in periods.iter().enumerate() {
22            let mut averages_for_period = vec![];
23
24            for a_idx in (0..averages.len()).rev() {
25                if period.matches(averages[a_idx].timestamp) {
26                    averages_for_period.push(averages[a_idx].clone());
27
28                    if periods.match_method() == CostPeriodMatching::First {
29                        averages.remove(a_idx);
30                    }
31                }
32            }
33
34            periods_averages_map.insert(p_idx, averages_for_period);
35        }
36
37        let items = periods
38            .clone()
39            .iter()
40            .enumerate()
41            .map(|(p_idx, period)| PeriodDemand {
42                period: period.clone(),
43                peaks: PeakDemands::new(calc_method, periods_averages_map.remove(&p_idx).unwrap()),
44            })
45            .collect();
46
47        Self { items }
48    }
49
50    pub fn items(&self) -> &[PeriodDemand] {
51        &self.items
52    }
53}
54
55#[derive(Clone, Debug)]
56pub struct PeriodDemand {
57    period: CostPeriod,
58    peaks: PeakDemands,
59}
60
61impl PeriodDemand {
62    pub fn period(&self) -> &CostPeriod {
63        &self.period
64    }
65    pub fn peaks(&self) -> &PeakDemands {
66        &self.peaks
67    }
68}
69
70#[derive(Clone, Debug, PartialEq)]
71pub struct AverageDemand {
72    timestamp: DateTime<Tz>,
73    value: u32,
74}
75
76impl AverageDemand {
77    pub fn new<Dt: Into<DateTime<Tz>>>(timestamp: Dt, value: u32) -> Self {
78        Self {
79            timestamp: timestamp.into(),
80            value,
81        }
82    }
83
84    pub fn timestamp(&self) -> DateTime<Tz> {
85        self.timestamp
86    }
87
88    pub fn kw(&self) -> f64 {
89        self.value as f64 / 1000.
90    }
91}
92
93#[derive(Default, Clone, Debug)]
94pub struct PeakDemands(Vec<AverageDemand>);
95
96impl PeakDemands {
97    pub fn new(
98        calc_method: TariffCalculationMethod,
99        mut period_averages: Vec<AverageDemand>,
100    ) -> Self {
101        let peak_demands: Vec<AverageDemand> = match calc_method {
102            crate::TariffCalculationMethod::AverageDays(n) => {
103                // For AverageDays: get the highest hour from each of the top n days
104                let mut daily_peaks: HashMap<chrono::NaiveDate, AverageDemand> = HashMap::new();
105
106                // Group by day and keep only the highest value for each day
107                for demand in period_averages.clone() {
108                    let date = demand.timestamp.date_naive();
109                    daily_peaks
110                        .entry(date)
111                        .and_modify(|existing| {
112                            if demand.value > existing.value {
113                                *existing = demand.clone();
114                            }
115                        })
116                        .or_insert(demand);
117                }
118
119                // Convert to vector and sort by value descending
120                let mut daily_peaks_vec: Vec<AverageDemand> = daily_peaks.into_values().collect();
121                daily_peaks_vec.sort_by(|a, b| b.value.cmp(&a.value));
122
123                // Take the top n days
124                daily_peaks_vec.into_iter().take(n as usize).collect()
125            }
126            crate::TariffCalculationMethod::AverageHours(n) => {
127                // For AverageHours: get the top n hours by value
128                period_averages.sort_by(|a, b| b.value.cmp(&a.value));
129                period_averages.into_iter().take(n as usize).collect()
130            }
131        };
132
133        Self(peak_demands)
134    }
135
136    pub fn values(&self) -> &[AverageDemand] {
137        &self.0
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::{Country, Stockholm, costs::LoadType, months::Month};
145    use chrono::{Datelike, Timelike};
146
147    #[test]
148    fn average_hours_returns_n_highest_values() {
149        let averages = vec![
150            AverageDemand::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100),
151            AverageDemand::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 500),
152            AverageDemand::new(Stockholm.dt(2025, 1, 1, 2, 0, 0), 300),
153            AverageDemand::new(Stockholm.dt(2025, 1, 1, 3, 0, 0), 200),
154            AverageDemand::new(Stockholm.dt(2025, 1, 1, 4, 0, 0), 400),
155        ];
156
157        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(3), averages);
158
159        assert_eq!(peaks.values().len(), 3);
160        assert_eq!(peaks.values()[0].value, 500);
161        assert_eq!(peaks.values()[1].value, 400);
162        assert_eq!(peaks.values()[2].value, 300);
163    }
164
165    #[test]
166    fn average_hours_with_equal_values() {
167        let averages = vec![
168            AverageDemand::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 500),
169            AverageDemand::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 500),
170            AverageDemand::new(Stockholm.dt(2025, 1, 1, 2, 0, 0), 300),
171        ];
172
173        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(2), averages);
174
175        assert_eq!(peaks.values().len(), 2);
176        assert_eq!(peaks.values()[0].value, 500);
177        assert_eq!(peaks.values()[1].value, 500);
178    }
179
180    #[test]
181    fn average_hours_empty_input() {
182        let averages = vec![];
183
184        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(3), averages);
185
186        assert_eq!(peaks.values().len(), 0);
187    }
188
189    #[test]
190    fn average_hours_zero_n() {
191        let averages = vec![AverageDemand::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100)];
192
193        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(0), averages);
194
195        assert_eq!(peaks.values().len(), 0);
196    }
197
198    #[test]
199    fn average_hours_n_greater_than_available() {
200        let averages = vec![
201            AverageDemand::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100),
202            AverageDemand::new(Stockholm.dt(2025, 1, 1, 1, 0, 0), 200),
203        ];
204
205        let peaks = PeakDemands::new(TariffCalculationMethod::AverageHours(5), averages);
206
207        assert_eq!(peaks.values().len(), 2);
208        assert_eq!(peaks.values()[0].value, 200);
209        assert_eq!(peaks.values()[1].value, 100);
210    }
211
212    #[test]
213    fn average_days_one_peak_per_day() {
214        let averages = vec![
215            // Day 1: peak at 500
216            AverageDemand::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
217            AverageDemand::new(Stockholm.dt(2025, 1, 1, 11, 0, 0), 500),
218            AverageDemand::new(Stockholm.dt(2025, 1, 1, 12, 0, 0), 300),
219            // Day 2: peak at 600
220            AverageDemand::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 200),
221            AverageDemand::new(Stockholm.dt(2025, 1, 2, 11, 0, 0), 600),
222            // Day 3: peak at 250
223            AverageDemand::new(Stockholm.dt(2025, 1, 3, 10, 0, 0), 150),
224            AverageDemand::new(Stockholm.dt(2025, 1, 3, 11, 0, 0), 250),
225        ];
226
227        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(2), averages);
228
229        assert_eq!(peaks.values().len(), 2);
230        assert_eq!(peaks.values()[0].value, 600);
231        assert_eq!(peaks.values()[0].timestamp.day(), 2);
232        assert_eq!(peaks.values()[1].value, 500);
233        assert_eq!(peaks.values()[1].timestamp.day(), 1);
234    }
235
236    #[test]
237    fn average_days_ensures_different_days() {
238        let averages = vec![
239            AverageDemand::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 500),
240            AverageDemand::new(Stockholm.dt(2025, 1, 1, 11, 0, 0), 450),
241            AverageDemand::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 300),
242        ];
243
244        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(2), averages);
245
246        assert_eq!(peaks.values().len(), 2);
247        let day1 = peaks.values()[0].timestamp.date_naive();
248        let day2 = peaks.values()[1].timestamp.date_naive();
249        assert_ne!(day1, day2);
250    }
251
252    #[test]
253    fn average_days_preserves_peak_hour_timestamp() {
254        let averages = vec![
255            AverageDemand::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
256            AverageDemand::new(Stockholm.dt(2025, 1, 1, 14, 0, 0), 500),
257            AverageDemand::new(Stockholm.dt(2025, 1, 1, 20, 0, 0), 300),
258        ];
259
260        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(1), averages);
261
262        assert_eq!(peaks.values().len(), 1);
263        assert_eq!(peaks.values()[0].timestamp.hour(), 14);
264        assert_eq!(peaks.values()[0].value, 500);
265    }
266
267    #[test]
268    fn average_days_empty_input() {
269        let averages = vec![];
270
271        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(3), averages);
272
273        assert_eq!(peaks.values().len(), 0);
274    }
275
276    #[test]
277    fn average_days_zero_n() {
278        let averages = vec![AverageDemand::new(Stockholm.dt(2025, 1, 1, 0, 0, 0), 100)];
279
280        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(0), averages);
281
282        assert_eq!(peaks.values().len(), 0);
283    }
284
285    #[test]
286    fn average_days_n_greater_than_available_days() {
287        let averages = vec![
288            AverageDemand::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100),
289            AverageDemand::new(Stockholm.dt(2025, 1, 2, 10, 0, 0), 200),
290        ];
291
292        let peaks = PeakDemands::new(TariffCalculationMethod::AverageDays(5), averages);
293
294        assert_eq!(peaks.values().len(), 2);
295    }
296
297    #[test]
298    fn peak_periods_first_matching_splits_values() {
299        static PERIODS_ARRAY: [CostPeriod; 2] = [
300            CostPeriod::builder()
301                .load(LoadType::High)
302                .fixed_cost(10, 0)
303                .hours(6, 22)
304                .months(Month::November, Month::March)
305                .exclude_weekends()
306                .exclude_holidays(Country::SE)
307                .build(),
308            CostPeriod::builder()
309                .load(LoadType::Low)
310                .fixed_cost(5, 0)
311                .build(),
312        ];
313        let periods = CostPeriods::new_first(&PERIODS_ARRAY);
314
315        // January 15, 2025 is a Wednesday
316        let averages = vec![
317            // Matches high: winter weekday 10:00
318            AverageDemand::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
319            // Doesn't match high (hour 23): goes to low
320            AverageDemand::new(Stockholm.dt(2025, 1, 15, 23, 0, 0), 300),
321            // Matches high: winter weekday 14:00
322            AverageDemand::new(Stockholm.dt(2025, 1, 15, 14, 0, 0), 400),
323        ];
324
325        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(10), periods, averages);
326
327        assert_eq!(result.items().len(), 2);
328
329        // High period gets 2 values (10:00, 14:00)
330        assert_eq!(result.items()[0].peaks().values().len(), 2);
331        assert_eq!(result.items()[0].peaks().values()[0].value, 500);
332        assert_eq!(result.items()[0].peaks().values()[1].value, 400);
333
334        // Low period gets 1 value (23:00)
335        assert_eq!(result.items()[1].peaks().values().len(), 1);
336        assert_eq!(result.items()[1].peaks().values()[0].value, 300);
337    }
338
339    #[test]
340    fn peak_periods_all_matching_duplicates_values() {
341        static PERIODS_ARRAY: [CostPeriod; 2] = [
342            CostPeriod::builder()
343                .load(LoadType::High)
344                .fixed_cost(10, 0)
345                .hours(6, 22)
346                .months(Month::November, Month::March)
347                .exclude_weekends()
348                .exclude_holidays(Country::SE)
349                .build(),
350            CostPeriod::builder()
351                .load(LoadType::Low)
352                .fixed_cost(5, 0)
353                .build(),
354        ];
355        let periods = CostPeriods::new_all(&PERIODS_ARRAY);
356
357        // January 15, 2025 is a Wednesday
358        let averages = vec![
359            AverageDemand::new(Stockholm.dt(2025, 1, 15, 10, 0, 0), 500),
360            AverageDemand::new(Stockholm.dt(2025, 1, 15, 23, 0, 0), 300),
361        ];
362
363        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(10), periods, averages);
364
365        assert_eq!(result.items().len(), 2);
366
367        // High period gets 1 value (only 10:00 matches criteria)
368        assert_eq!(result.items()[0].peaks().values().len(), 1);
369        assert_eq!(result.items()[0].peaks().values()[0].value, 500);
370
371        // Low period gets both (no restrictions)
372        assert_eq!(result.items()[1].peaks().values().len(), 2);
373        assert_eq!(result.items()[1].peaks().values()[0].value, 500);
374        assert_eq!(result.items()[1].peaks().values()[1].value, 300);
375    }
376
377    #[test]
378    fn peak_periods_empty_averages() {
379        static PERIODS_ARRAY: [CostPeriod; 1] = [CostPeriod::builder()
380            .load(LoadType::Low)
381            .fixed_cost(5, 0)
382            .build()];
383        let periods = CostPeriods::new_first(&PERIODS_ARRAY);
384        let averages = vec![];
385
386        let result = PeakPeriods::new(TariffCalculationMethod::AverageHours(3), periods, averages);
387
388        assert_eq!(result.items().len(), 1);
389        assert_eq!(result.items()[0].peaks().values().len(), 0);
390    }
391
392    #[test]
393    fn single_value_both_methods() {
394        let averages = vec![AverageDemand::new(Stockholm.dt(2025, 1, 1, 10, 0, 0), 100)];
395
396        let hours = PeakDemands::new(TariffCalculationMethod::AverageHours(3), averages.clone());
397        assert_eq!(hours.values().len(), 1);
398        assert_eq!(hours.values()[0].value, 100);
399
400        let days = PeakDemands::new(TariffCalculationMethod::AverageDays(3), averages);
401        assert_eq!(days.values().len(), 1);
402        assert_eq!(days.values()[0].value, 100);
403    }
404}