1#[derive(Debug, Clone, PartialEq)]
3pub enum ElectricityRate {
4 Fixed {
6 rate: f64,
8 },
9 Tiered {
11 tiers: Vec<RateTier>,
13 },
14}
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct RateTier {
19 pub name: String,
21 pub rate: f64,
23 pub hour_ranges: Vec<HourRange>,
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub struct HourRange {
30 pub from: u8,
32 pub till: u8,
34 pub weekday_type: WeekdayType,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq)]
40pub enum WeekdayType {
41 Weekday,
43 Weekend,
45}
46
47impl ElectricityRate {
48 pub fn fixed(rate: f64) -> Self {
50 Self::Fixed { rate }
51 }
52
53 pub fn tiered(tiers: Vec<RateTier>) -> Self {
55 Self::Tiered { tiers }
56 }
57
58 pub fn to_weekly_hourly_rates(&self) -> Vec<f64> {
62 let mut weekly_rates = Vec::with_capacity(168);
63
64 for day in 0..7 {
66 let weekday_type = if day < 5 {
67 WeekdayType::Weekday
68 } else {
69 WeekdayType::Weekend
70 };
71
72 for hour in 0..24 {
73 let rate = self.get_rate_for_hour(hour, weekday_type);
74 weekly_rates.push(rate);
75 }
76 }
77
78 weekly_rates
79 }
80
81 pub fn to_yearly_hourly_rates(&self) -> Vec<f64> {
85 let mut yearly_rates = Vec::with_capacity(8760);
86
87 for day_of_year in 0..365 {
89 let weekday_type = self.get_weekday_type_for_day_of_year(day_of_year);
90
91 for hour in 0..24 {
92 let rate = self.get_rate_for_hour(hour, weekday_type);
93 yearly_rates.push(rate);
94 }
95 }
96
97 yearly_rates
98 }
99
100 fn get_rate_for_hour(&self, hour: u8, weekday_type: WeekdayType) -> f64 {
102 match self {
103 ElectricityRate::Fixed { rate } => *rate,
104 ElectricityRate::Tiered { tiers } => {
105 for tier in tiers {
107 if tier.matches_hour(hour, weekday_type) {
108 return tier.rate;
109 }
110 }
111 0.0
113 }
114 }
115 }
116
117 fn get_weekday_type_for_day_of_year(&self, day_of_year: u16) -> WeekdayType {
120 let day_of_week = day_of_year % 7;
123 if day_of_week < 5 {
124 WeekdayType::Weekday
125 } else {
126 WeekdayType::Weekend
127 }
128 }
129
130 pub fn is_valid(&self) -> bool {
133 match self {
134 ElectricityRate::Fixed { .. } => {
135 true
137 }
138 ElectricityRate::Tiered { tiers } => {
139 self.validate_weekday_coverage(tiers) && self.validate_weekend_coverage(tiers)
141 }
142 }
143 }
144
145 fn validate_weekday_coverage(&self, tiers: &[RateTier]) -> bool {
147 let mut covered_hours = [false; 24];
148
149 for tier in tiers {
150 for hour_range in &tier.hour_ranges {
151 if hour_range.weekday_type == WeekdayType::Weekday {
152 if !self.mark_hours_covered(&mut covered_hours, hour_range) {
153 return false; }
155 }
156 }
157 }
158
159 covered_hours.iter().all(|&covered| covered)
161 }
162
163 fn validate_weekend_coverage(&self, tiers: &[RateTier]) -> bool {
165 let mut covered_hours = [false; 24];
166
167 for tier in tiers {
168 for hour_range in &tier.hour_ranges {
169 if hour_range.weekday_type == WeekdayType::Weekend {
170 if !self.mark_hours_covered(&mut covered_hours, hour_range) {
171 return false; }
173 }
174 }
175 }
176
177 covered_hours.iter().all(|&covered| covered)
179 }
180
181 fn mark_hours_covered(&self, covered_hours: &mut [bool; 24], hour_range: &HourRange) -> bool {
183 if hour_range.from > hour_range.till {
184 for hour in hour_range.from..24 {
186 if covered_hours[hour as usize] {
187 return false; }
189 covered_hours[hour as usize] = true;
190 }
191 for hour in 0..hour_range.till {
192 if covered_hours[hour as usize] {
193 return false; }
195 covered_hours[hour as usize] = true;
196 }
197 } else {
198 for hour in hour_range.from..hour_range.till {
200 if covered_hours[hour as usize] {
201 return false; }
203 covered_hours[hour as usize] = true;
204 }
205 }
206 true
207 }
208}
209
210impl RateTier {
211 pub fn new(name: String, rate: f64, hour_ranges: Vec<HourRange>) -> Self {
213 Self {
214 name,
215 rate,
216 hour_ranges,
217 }
218 }
219
220 pub fn matches_hour(&self, hour: u8, weekday_type: WeekdayType) -> bool {
222 self.hour_ranges
223 .iter()
224 .any(|range| range.matches_hour(hour, weekday_type))
225 }
226}
227
228impl HourRange {
229 pub fn new(from: u8, till: u8, weekday_type: WeekdayType) -> Self {
231 Self {
232 from,
233 till,
234 weekday_type,
235 }
236 }
237
238 pub fn matches_hour(&self, hour: u8, weekday_type: WeekdayType) -> bool {
240 if self.weekday_type != weekday_type {
242 return false;
243 }
244
245 if self.from > self.till {
247 hour >= self.from || hour < self.till
249 } else {
250 hour >= self.from && hour < self.till
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_fixed_rate() {
262 let rate = ElectricityRate::fixed(0.12);
263 match rate {
264 ElectricityRate::Fixed { rate: r } => assert_eq!(r, 0.12),
265 _ => panic!("Expected Fixed rate"),
266 }
267 }
268
269 #[test]
270 fn test_tiered_rate() {
271 let peak_tier = RateTier::new(
272 "Peak".to_string(),
273 0.25,
274 vec![
275 HourRange::new(9, 17, WeekdayType::Weekday),
276 HourRange::new(10, 16, WeekdayType::Weekend),
277 ],
278 );
279
280 let off_peak_tier = RateTier::new(
281 "Off-Peak".to_string(),
282 0.08,
283 vec![
284 HourRange::new(17, 9, WeekdayType::Weekday),
285 HourRange::new(16, 10, WeekdayType::Weekend),
286 ],
287 );
288
289 let rate = ElectricityRate::tiered(vec![peak_tier, off_peak_tier]);
290 match rate {
291 ElectricityRate::Tiered { tiers } => assert_eq!(tiers.len(), 2),
292 _ => panic!("Expected Tiered rate"),
293 }
294 }
295
296 #[test]
297 fn test_fixed_rate_weekly_conversion() {
298 let rate = ElectricityRate::fixed(0.15);
299 let weekly_rates = rate.to_weekly_hourly_rates();
300
301 assert_eq!(weekly_rates.len(), 168);
303
304 for &rate_value in &weekly_rates {
306 assert_eq!(rate_value, 0.15);
307 }
308 }
309
310 #[test]
311 fn test_fixed_rate_yearly_conversion() {
312 let rate = ElectricityRate::fixed(0.15);
313 let yearly_rates = rate.to_yearly_hourly_rates();
314
315 assert_eq!(yearly_rates.len(), 8760);
317
318 for &rate_value in &yearly_rates {
320 assert_eq!(rate_value, 0.15);
321 }
322 }
323
324 #[test]
325 fn test_tiered_rate_weekly_conversion() {
326 let peak_tier = RateTier::new(
327 "Peak".to_string(),
328 0.25,
329 vec![HourRange::new(9, 17, WeekdayType::Weekday)],
330 );
331
332 let off_peak_tier = RateTier::new(
333 "Off-Peak".to_string(),
334 0.08,
335 vec![
336 HourRange::new(17, 9, WeekdayType::Weekday),
337 HourRange::new(0, 24, WeekdayType::Weekend),
338 ],
339 );
340
341 let rate = ElectricityRate::tiered(vec![peak_tier, off_peak_tier]);
342 let weekly_rates = rate.to_weekly_hourly_rates();
343
344 assert_eq!(weekly_rates.len(), 168);
346
347 for day in 0..5 {
349 for hour in 0..24 {
350 let index = day * 24 + hour;
351 if hour >= 9 && hour < 17 {
352 assert_eq!(weekly_rates[index], 0.25);
354 } else {
355 assert_eq!(weekly_rates[index], 0.08);
357 }
358 }
359 }
360
361 for day in 5..7 {
363 for hour in 0..24 {
364 let index = day * 24 + hour;
365 assert_eq!(weekly_rates[index], 0.08);
367 }
368 }
369 }
370
371 #[test]
372 fn test_hour_range_matching() {
373 let range = HourRange::new(9, 17, WeekdayType::Weekday);
375
376 for hour in 9..17 {
378 assert!(range.matches_hour(hour, WeekdayType::Weekday));
379 }
380
381 for hour in 0..9 {
383 assert!(!range.matches_hour(hour, WeekdayType::Weekday));
384 }
385 for hour in 17..24 {
386 assert!(!range.matches_hour(hour, WeekdayType::Weekday));
387 }
388
389 assert!(!range.matches_hour(10, WeekdayType::Weekend));
391 }
392
393 #[test]
394 fn test_wrapping_hour_range() {
395 let range = HourRange::new(22, 6, WeekdayType::Weekday);
397
398 for hour in 22..24 {
400 assert!(range.matches_hour(hour, WeekdayType::Weekday));
401 }
402 for hour in 0..6 {
403 assert!(range.matches_hour(hour, WeekdayType::Weekday));
404 }
405
406 for hour in 6..22 {
408 assert!(!range.matches_hour(hour, WeekdayType::Weekday));
409 }
410 }
411
412 #[test]
413 fn test_weekday_type_determination() {
414 let rate = ElectricityRate::fixed(0.1);
415
416 let weekday_type = rate.get_weekday_type_for_day_of_year(0);
419 assert_eq!(weekday_type, WeekdayType::Weekday);
420
421 let weekday_type = rate.get_weekday_type_for_day_of_year(4);
423 assert_eq!(weekday_type, WeekdayType::Weekday);
424
425 let weekday_type = rate.get_weekday_type_for_day_of_year(5);
427 assert_eq!(weekday_type, WeekdayType::Weekend);
428
429 let weekday_type = rate.get_weekday_type_for_day_of_year(6);
431 assert_eq!(weekday_type, WeekdayType::Weekend);
432
433 let weekday_type = rate.get_weekday_type_for_day_of_year(7);
435 assert_eq!(weekday_type, WeekdayType::Weekday);
436 }
437
438 #[test]
439 fn test_fixed_rate_is_valid() {
440 let rate = ElectricityRate::fixed(0.12);
441 assert!(rate.is_valid());
442 }
443
444 #[test]
445 fn test_valid_tiered_rate() {
446 let peak_tier = RateTier::new(
448 "Peak".to_string(),
449 0.25,
450 vec![HourRange::new(9, 17, WeekdayType::Weekday)],
451 );
452
453 let off_peak_tier = RateTier::new(
454 "Off-Peak".to_string(),
455 0.08,
456 vec![
457 HourRange::new(17, 9, WeekdayType::Weekday), HourRange::new(0, 24, WeekdayType::Weekend), ],
460 );
461
462 let rate = ElectricityRate::tiered(vec![peak_tier, off_peak_tier]);
463 assert!(rate.is_valid());
464 }
465
466 #[test]
467 fn test_invalid_tiered_rate_missing_weekday_hours() {
468 let peak_tier = RateTier::new(
470 "Peak".to_string(),
471 0.25,
472 vec![HourRange::new(9, 17, WeekdayType::Weekday)], );
474
475 let rate = ElectricityRate::tiered(vec![peak_tier]);
476 assert!(!rate.is_valid()); }
478
479 #[test]
480 fn test_invalid_tiered_rate_missing_weekend_hours() {
481 let peak_tier = RateTier::new(
483 "Peak".to_string(),
484 0.25,
485 vec![HourRange::new(9, 17, WeekdayType::Weekend)], );
487
488 let rate = ElectricityRate::tiered(vec![peak_tier]);
489 assert!(!rate.is_valid()); }
491
492 #[test]
493 fn test_invalid_tiered_rate_overlapping_hours() {
494 let peak_tier = RateTier::new(
496 "Peak".to_string(),
497 0.25,
498 vec![HourRange::new(9, 17, WeekdayType::Weekday)],
499 );
500
501 let off_peak_tier = RateTier::new(
502 "Off-Peak".to_string(),
503 0.08,
504 vec![HourRange::new(15, 20, WeekdayType::Weekday)], );
506
507 let rate = ElectricityRate::tiered(vec![peak_tier, off_peak_tier]);
508 assert!(!rate.is_valid()); }
510
511 #[test]
512 fn test_valid_tiered_rate_with_wrapping_ranges() {
513 let peak_tier = RateTier::new(
515 "Peak".to_string(),
516 0.25,
517 vec![
518 HourRange::new(9, 17, WeekdayType::Weekday),
519 HourRange::new(10, 16, WeekdayType::Weekend),
520 ],
521 );
522
523 let off_peak_tier = RateTier::new(
524 "Off-Peak".to_string(),
525 0.08,
526 vec![
527 HourRange::new(17, 9, WeekdayType::Weekday), HourRange::new(16, 10, WeekdayType::Weekend), ],
530 );
531
532 let rate = ElectricityRate::tiered(vec![peak_tier, off_peak_tier]);
533 assert!(rate.is_valid());
534 }
535
536 #[test]
537 fn test_invalid_tiered_rate_wrapping_overlap() {
538 let peak_tier = RateTier::new(
540 "Peak".to_string(),
541 0.25,
542 vec![HourRange::new(22, 6, WeekdayType::Weekday)], );
544
545 let off_peak_tier = RateTier::new(
546 "Off-Peak".to_string(),
547 0.08,
548 vec![HourRange::new(4, 8, WeekdayType::Weekday)], );
550
551 let rate = ElectricityRate::tiered(vec![peak_tier, off_peak_tier]);
552 assert!(!rate.is_valid()); }
554
555 #[test]
556 fn test_valid_tiered_rate_separate_weekday_weekend() {
557 let weekday_peak = RateTier::new(
559 "Weekday Peak".to_string(),
560 0.25,
561 vec![HourRange::new(9, 17, WeekdayType::Weekday)],
562 );
563
564 let weekday_off_peak = RateTier::new(
565 "Weekday Off-Peak".to_string(),
566 0.08,
567 vec![HourRange::new(17, 9, WeekdayType::Weekday)],
568 );
569
570 let weekend_rate = RateTier::new(
571 "Weekend Rate".to_string(),
572 0.12,
573 vec![HourRange::new(0, 24, WeekdayType::Weekend)],
574 );
575
576 let rate = ElectricityRate::tiered(vec![weekday_peak, weekday_off_peak, weekend_rate]);
577 assert!(rate.is_valid());
578 }
579}