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 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::{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 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 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 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 let averages = vec![
412 AverageDemand {
414 timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap(),
415 value: 500,
416 },
417 AverageDemand {
419 timestamp: Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap(),
420 value: 300,
421 },
422 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 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 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 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 assert_eq!(result.items()[0].peaks().values().len(), 1);
479 assert_eq!(result.items()[0].peaks().values()[0].value, 500);
480
481 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}