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 let mut daily_peaks: HashMap<chrono::NaiveDate, AverageDemand> = HashMap::new();
99
100 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 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 daily_peaks_vec.into_iter().take(n as usize).collect()
119 }
120 crate::TariffCalculationMethod::AverageHours(n) => {
121 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 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 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 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 let averages = vec![
424 AverageDemand {
426 timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap(),
427 value: 500,
428 },
429 AverageDemand {
431 timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap(),
432 value: 300,
433 },
434 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 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 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 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 assert_eq!(result.items()[0].peaks().values().len(), 1);
491 assert_eq!(result.items()[0].peaks().values()[0].value, 500);
492
493 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}