1use std::slice::Iter;
2
3use chrono::{DateTime, Datelike};
4use chrono_tz::Tz;
5use serde::Serialize;
6
7use crate::{
8 Country, Language, Money,
9 defs::{Hours, Month, Months},
10 helpers,
11};
12
13#[derive(Debug, Clone, Copy, Serialize)]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15pub enum Cost {
16 None,
17 Unverified,
19 Fixed(Money),
20 Fuses(&'static [(u16, Money)]),
21 FusesYearlyConsumption(&'static [(u16, Option<u32>, Money)]),
23 FuseRange(&'static [(u16, u16, Money)]),
24}
25
26impl Cost {
27 pub const fn is_unverified(&self) -> bool {
28 matches!(self, Self::Unverified)
29 }
30
31 pub(super) const fn fuses(values: &'static [(u16, Money)]) -> Self {
32 Self::Fuses(values)
33 }
34
35 pub(super) const fn fuse_range(ranges: &'static [(u16, u16, Money)]) -> Self {
36 Self::FuseRange(ranges)
37 }
38
39 pub(super) const fn fuses_with_yearly_consumption(
40 values: &'static [(u16, Option<u32>, Money)],
41 ) -> Cost {
42 Self::FusesYearlyConsumption(values)
43 }
44
45 pub(super) const fn fixed(int: i64, fract: u8) -> Self {
46 Self::Fixed(Money::new(int, fract))
47 }
48
49 pub(super) const fn fixed_yearly(int: i64, fract: u8) -> Self {
50 Self::Fixed(Money::new(int, fract).divide_by(12))
51 }
52
53 pub(super) const fn fixed_subunit(subunit: f64) -> Self {
54 Self::Fixed(Money::new_subunit(subunit))
55 }
56
57 pub(super) const fn divide_by(&self, by: i64) -> Self {
58 match self {
59 Self::None => Self::None,
60 Self::Unverified => Self::Unverified,
61 Self::Fixed(money) => Self::Fixed(money.divide_by(by)),
62 Self::Fuses(_) => panic!(".divide_by() is unsupported on Cost::Fuses"),
63 Self::FusesYearlyConsumption(_) => {
64 panic!(".divide_by() is unsupported on Cost::FuseRangeYearlyConsumption")
65 }
66 Self::FuseRange(_) => panic!(".divide_by() is unsupported on Cost::FuseRange"),
67 }
68 }
69
70 pub const fn cost_for(&self, fuse_size: u16, yearly_consumption: u32) -> Option<Money> {
71 match *self {
72 Cost::None => None,
73 Cost::Unverified => None,
74 Cost::Fixed(money) => Some(money),
75 Cost::Fuses(values) => {
76 let mut i = 0;
77 while i < values.len() {
78 let (fsize, money) = values[i];
79 if fuse_size == fsize {
80 return Some(money);
81 }
82 i += 1;
83 }
84 None
85 }
86 Cost::FusesYearlyConsumption(values) => {
87 let mut i = 0;
88 while i < values.len() {
89 let (fsize, max_consumption, money) = values[i];
90 if fsize == fuse_size {
91 if let Some(max_consumption) = max_consumption {
92 if yearly_consumption <= max_consumption {
93 return Some(money);
94 }
95 } else {
96 return Some(money);
97 }
98 }
99 i += 1;
100 }
101 None
102 }
103 Cost::FuseRange(ranges) => {
104 let mut i = 0;
105 while i < ranges.len() {
106 let (min, max, money) = ranges[i];
107 if fuse_size >= min && fuse_size <= max {
108 return Some(money);
109 }
110 i += 1;
111 }
112 None
113 }
114 }
115 }
116
117 pub(crate) const fn add_vat(&self, country: Country) -> Cost {
118 match self {
119 Cost::None => Cost::None,
120 Cost::Unverified => Cost::Unverified,
121 Cost::Fixed(money) => Cost::Fixed(money.add_vat(country)),
122 Cost::Fuses(_) => todo!(),
123 Cost::FusesYearlyConsumption(_) => todo!(),
124 Cost::FuseRange(_) => todo!(),
125 }
126 }
127
128 pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
129 match self {
130 Cost::FusesYearlyConsumption(items) => items
131 .iter()
132 .filter(|(fsize, _, _)| *fsize == fuse_size)
133 .any(|(_, yearly_consumption, _)| yearly_consumption.is_some()),
134 _ => false,
135 }
136 }
137}
138
139#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
140#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
141pub enum CostPeriodMatching {
142 First,
143 All,
144}
145
146#[derive(Debug, Clone, Copy, Serialize)]
147#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
148pub struct CostPeriods {
149 match_method: CostPeriodMatching,
150 periods: &'static [CostPeriod],
151}
152
153impl CostPeriods {
154 pub(super) const fn new_first(periods: &'static [CostPeriod]) -> Self {
155 Self {
156 match_method: CostPeriodMatching::First,
157 periods,
158 }
159 }
160
161 pub fn match_method(&self) -> CostPeriodMatching {
162 self.match_method
163 }
164
165 pub(super) const fn new_all(periods: &'static [CostPeriod]) -> Self {
166 Self {
167 match_method: CostPeriodMatching::All,
168 periods,
169 }
170 }
171
172 pub fn iter(&self) -> Iter<'_, CostPeriod> {
173 self.periods.iter()
174 }
175
176 pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
177 self.periods
178 .iter()
179 .any(|cp| cp.is_yearly_consumption_based(fuse_size))
180 }
181
182 pub fn matching_periods(&self, timestamp: DateTime<Tz>) -> Vec<&CostPeriod> {
183 let mut ret = vec![];
184 for period in self.periods {
185 if period.matches(timestamp) {
186 ret.push(period);
187 if self.match_method == CostPeriodMatching::First {
188 break;
189 }
190 }
191 }
192 ret
193 }
194}
195
196#[derive(Debug, Clone, Serialize)]
198#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
199pub struct CostPeriodsSimple {
200 periods: Vec<CostPeriodSimple>,
201}
202
203impl CostPeriodsSimple {
204 pub(crate) fn new(
205 periods: CostPeriods,
206 fuse_size: u16,
207 yearly_consumption: u32,
208 language: Language,
209 ) -> Self {
210 Self {
211 periods: periods
212 .periods
213 .iter()
214 .flat_map(|period| {
215 CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
216 })
217 .collect(),
218 }
219 }
220}
221
222#[derive(Debug, Clone, Serialize)]
223#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
224pub struct CostPeriod {
225 cost: Cost,
226 load: LoadType,
227 #[serde(serialize_with = "helpers::skip_nones")]
228 include: [Option<Include>; 2],
229 #[serde(serialize_with = "helpers::skip_nones")]
230 exclude: [Option<Exclude>; 2],
231 divide_kw_by: u8,
233}
234
235#[derive(Debug, Clone, Serialize)]
237#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
238pub(super) struct CostPeriodSimple {
239 cost: Money,
240 load: LoadType,
241 include: Vec<Include>,
242 exclude: Vec<Exclude>,
243 divide_kw_by: u8,
245 info: String,
246}
247
248impl CostPeriodSimple {
249 fn new(
250 period: &CostPeriod,
251 fuse_size: u16,
252 yearly_consumption: u32,
253 language: Language,
254 ) -> Option<Self> {
255 let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
256 Some(
257 Self {
258 cost,
259 load: period.load,
260 include: period.include.into_iter().flatten().collect(),
261 exclude: period.exclude.into_iter().flatten().collect(),
262 divide_kw_by: period.divide_kw_by,
263 info: Default::default(),
264 }
265 .add_info(language),
266 )
267 }
268
269 fn add_info(mut self, language: Language) -> Self {
270 let mut infos = Vec::new();
271 for include in &self.include {
272 infos.push(include.translate(language));
273 }
274 for exclude in &self.exclude {
275 infos.push(exclude.translate(language).into());
276 }
277 self.info = infos.join(", ");
278 self
279 }
280}
281
282impl CostPeriod {
283 pub(super) const fn builder() -> CostPeriodBuilder {
284 CostPeriodBuilder::new()
285 }
286
287 pub const fn cost(&self) -> Cost {
288 self.cost
289 }
290
291 pub const fn load(&self) -> LoadType {
292 self.load
293 }
294
295 pub const fn power_multiplier(&self) -> f64 {
296 1. / self.divide_kw_by as f64
297 }
298
299 pub fn matches(&self, timestamp: DateTime<Tz>) -> bool {
300 for include in self.include_period_types() {
301 if !include.matches(timestamp) {
302 return false;
303 }
304 }
305
306 for exclude in self.exclude_period_types() {
307 if exclude.matches(timestamp) {
308 return false;
309 }
310 }
311 true
312 }
313
314 fn include_period_types(&self) -> Vec<Include> {
315 self.include.iter().flatten().copied().collect()
316 }
317
318 fn exclude_period_types(&self) -> Vec<Exclude> {
319 self.exclude.iter().flatten().copied().collect()
320 }
321
322 fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
323 self.cost.is_yearly_consumption_based(fuse_size)
324 }
325}
326
327#[derive(Debug, Clone, Copy, Serialize)]
328#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
329pub enum LoadType {
330 Base,
332 Low,
334 High,
336}
337
338pub(super) use LoadType::*;
339
340#[derive(Clone)]
341pub(super) struct CostPeriodBuilder {
342 cost: Cost,
343 load: Option<LoadType>,
344 include: [Option<Include>; 2],
345 exclude: [Option<Exclude>; 2],
346 divide_kw_by: u8,
348}
349
350impl CostPeriodBuilder {
351 pub(super) const fn new() -> Self {
352 Self {
353 cost: Cost::None,
354 load: None,
355 include: [None; 2],
356 exclude: [None; 2],
357 divide_kw_by: 1,
358 }
359 }
360
361 pub(super) const fn build(self) -> CostPeriod {
362 CostPeriod {
363 cost: self.cost,
364 load: self.load.expect("`load` must be specified"),
365 include: self.include,
366 exclude: self.exclude,
367 divide_kw_by: self.divide_kw_by,
368 }
369 }
370
371 pub(super) const fn cost(mut self, cost: Cost) -> Self {
372 self.cost = cost;
373 self
374 }
375
376 pub(super) const fn load(mut self, load: LoadType) -> Self {
377 self.load = Some(load);
378 self
379 }
380
381 pub(super) const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
382 self.cost = Cost::fixed(int, fract);
383 self
384 }
385
386 pub(super) const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
387 self.cost = Cost::fixed_subunit(subunit);
388 self
389 }
390
391 pub(super) const fn include(mut self, period_type: Include) -> Self {
392 let mut i = 0;
393 while i < self.include.len() {
394 if self.include[i].is_some() {
395 i += 1;
396 } else {
397 self.include[i] = Some(period_type);
398 return self;
399 }
400 }
401 panic!("Too many includes");
402 }
403
404 pub(super) const fn months(self, from: Month, to: Month) -> Self {
405 self.include(Include::Months(Months::new(from, to)))
406 }
407
408 pub(super) const fn month(self, month: Month) -> Self {
409 self.include(Include::Month(month))
410 }
411
412 pub(super) const fn hours(self, from: u8, to_inclusive: u8) -> Self {
413 self.include(Include::Hours(Hours::new(from, to_inclusive)))
414 }
415
416 pub(super) const fn exclude(mut self, period_type: Exclude) -> Self {
417 let mut i = 0;
418 while i < self.exclude.len() {
419 if self.exclude[i].is_some() {
420 i += 1;
421 } else {
422 self.exclude[i] = Some(period_type);
423 return self;
424 }
425 }
426 panic!("Too many excludes");
427 }
428
429 pub(super) const fn exclude_holidays(self, country: Country) -> Self {
430 self.exclude(Exclude::Holidays(country))
431 }
432
433 pub(super) const fn exclude_weekends(self) -> Self {
434 self.exclude(Exclude::Weekends)
435 }
436}
437
438#[derive(Debug, Clone, Copy, Serialize)]
439#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
440pub(super) enum Include {
441 Months(Months),
442 Month(Month),
443 Hours(Hours),
444}
445
446impl Include {
447 fn translate(&self, language: Language) -> String {
448 match self {
449 Include::Months(months) => months.translate(language),
450 Include::Month(month) => month.translate(language).into(),
451 Include::Hours(hours) => hours.translate(language),
452 }
453 }
454
455 fn matches(&self, timestamp: DateTime<Tz>) -> bool {
456 match self {
457 Include::Months(months) => months.matches(timestamp),
458 Include::Month(month) => month.matches(timestamp),
459 Include::Hours(hours) => hours.matches(timestamp),
460 }
461 }
462}
463
464#[derive(Debug, Clone, Copy, Serialize)]
465#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
466pub(super) enum Exclude {
467 Weekends,
468 Holidays(Country),
469}
470
471impl Exclude {
472 pub(super) fn translate(&self, language: Language) -> &'static str {
473 match language {
474 Language::En => match self {
475 Exclude::Weekends => "Weekends",
476 Exclude::Holidays(country) => match country {
477 Country::SE => "Swedish holidays",
478 },
479 },
480 Language::Sv => match self {
481 Exclude::Weekends => "Helg",
482 Exclude::Holidays(country) => match country {
483 Country::SE => "Svenska helgdagar",
484 },
485 },
486 }
487 }
488
489 fn matches(&self, timestamp: DateTime<Tz>) -> bool {
490 match self {
491 Exclude::Weekends => (6..=7).contains(×tamp.weekday().number_from_monday()),
492 Exclude::Holidays(country) => country.is_holiday(timestamp.date_naive()),
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::defs::{Hours, Month, Months};
501 use crate::money::Money;
502 use chrono::TimeZone;
503 use chrono_tz::Europe::Stockholm;
504
505 #[test]
506 fn cost_for_none() {
507 const NONE_COST: Cost = Cost::None;
508 assert_eq!(NONE_COST.cost_for(16, 0), None);
509 assert_eq!(NONE_COST.cost_for(25, 5000), None);
510 }
511
512 #[test]
513 fn cost_for_unverified() {
514 const UNVERIFIED_COST: Cost = Cost::Unverified;
515 assert_eq!(UNVERIFIED_COST.cost_for(16, 0), None);
516 assert_eq!(UNVERIFIED_COST.cost_for(25, 5000), None);
517 }
518
519 #[test]
520 fn cost_for_fixed() {
521 const FIXED_COST: Cost = Cost::Fixed(Money::new(100, 50));
522 assert_eq!(FIXED_COST.cost_for(16, 0), Some(Money::new(100, 50)));
524 assert_eq!(FIXED_COST.cost_for(25, 5000), Some(Money::new(100, 50)));
525 assert_eq!(FIXED_COST.cost_for(63, 10000), Some(Money::new(100, 50)));
526 }
527
528 #[test]
529 fn cost_for_fuses_exact_match() {
530 const FUSES_COST: Cost = Cost::fuses(&[
531 (16, Money::new(50, 0)),
532 (25, Money::new(75, 0)),
533 (35, Money::new(100, 0)),
534 (50, Money::new(150, 0)),
535 ]);
536
537 assert_eq!(FUSES_COST.cost_for(16, 0), Some(Money::new(50, 0)));
539 assert_eq!(FUSES_COST.cost_for(25, 0), Some(Money::new(75, 0)));
540 assert_eq!(FUSES_COST.cost_for(35, 0), Some(Money::new(100, 0)));
541 assert_eq!(FUSES_COST.cost_for(50, 0), Some(Money::new(150, 0)));
542
543 assert_eq!(FUSES_COST.cost_for(25, 500000), Some(Money::new(75, 0)));
545 }
546
547 #[test]
548 fn cost_for_fuses_no_match() {
549 const FUSES_COST: Cost = Cost::fuses(&[(16, Money::new(50, 0)), (25, Money::new(75, 0))]);
550
551 assert_eq!(FUSES_COST.cost_for(20, 0), None);
553 assert_eq!(FUSES_COST.cost_for(63, 0), None);
554 }
555
556 #[test]
557 fn cost_for_fuses_yearly_consumption_with_limit() {
558 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
559 (16, Some(5000), Money::new(50, 0)),
560 (16, None, Money::new(75, 0)),
561 (25, Some(10000), Money::new(100, 0)),
562 (25, None, Money::new(125, 0)),
563 ]);
564
565 assert_eq!(
567 FUSES_WITH_CONSUMPTION.cost_for(16, 3000),
568 Some(Money::new(50, 0))
569 );
570
571 assert_eq!(
573 FUSES_WITH_CONSUMPTION.cost_for(16, 5000),
574 Some(Money::new(50, 0))
575 );
576
577 assert_eq!(
579 FUSES_WITH_CONSUMPTION.cost_for(16, 6000),
580 Some(Money::new(75, 0))
581 );
582
583 assert_eq!(
585 FUSES_WITH_CONSUMPTION.cost_for(16, 20000),
586 Some(Money::new(75, 0))
587 );
588
589 assert_eq!(
591 FUSES_WITH_CONSUMPTION.cost_for(25, 10000),
592 Some(Money::new(100, 0))
593 );
594
595 assert_eq!(
597 FUSES_WITH_CONSUMPTION.cost_for(25, 15000),
598 Some(Money::new(125, 0))
599 );
600
601 assert_eq!(
603 FUSES_WITH_CONSUMPTION.cost_for(25, 5000),
604 Some(Money::new(100, 0))
605 );
606 }
607
608 #[test]
609 fn cost_for_fuses_yearly_consumption_no_limit() {
610 const FUSES_NO_LIMIT: Cost = Cost::fuses_with_yearly_consumption(&[
611 (16, None, Money::new(50, 0)),
612 (25, None, Money::new(75, 0)),
613 ]);
614
615 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 0), Some(Money::new(50, 0)));
617 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 1000), Some(Money::new(50, 0)));
618 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 50000), Some(Money::new(50, 0)));
619 assert_eq!(FUSES_NO_LIMIT.cost_for(25, 100000), Some(Money::new(75, 0)));
620 }
621
622 #[test]
623 fn cost_for_fuses_yearly_consumption_no_fuse_match() {
624 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
625 (16, Some(5000), Money::new(50, 0)),
626 (25, Some(10000), Money::new(100, 0)),
627 ]);
628
629 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(35, 5000), None);
631 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(50, 10000), None);
632 }
633
634 #[test]
635 fn cost_for_fuses_yearly_consumption_max_limit_no_fallback() {
636 const FUSES_ONLY_LIMITS: Cost = Cost::fuses_with_yearly_consumption(&[
637 (16, Some(5000), Money::new(50, 0)),
638 (25, Some(10000), Money::new(100, 0)),
639 ]);
640
641 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 0), Some(Money::new(50, 0)));
643 assert_eq!(
644 FUSES_ONLY_LIMITS.cost_for(16, 3000),
645 Some(Money::new(50, 0))
646 );
647 assert_eq!(
648 FUSES_ONLY_LIMITS.cost_for(16, 4999),
649 Some(Money::new(50, 0))
650 );
651 assert_eq!(
652 FUSES_ONLY_LIMITS.cost_for(16, 5000),
653 Some(Money::new(50, 0))
654 );
655 assert_eq!(
656 FUSES_ONLY_LIMITS.cost_for(25, 9999),
657 Some(Money::new(100, 0))
658 );
659 assert_eq!(
660 FUSES_ONLY_LIMITS.cost_for(25, 10000),
661 Some(Money::new(100, 0))
662 );
663
664 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 5001), None);
666 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 10000), None);
667 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 10001), None);
668 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 20000), None);
669 }
670
671 #[test]
672 fn cost_for_fuse_range_within_range() {
673 const FUSE_BASED: Cost = Cost::fuse_range(&[
674 (16, 35, Money::new(54, 0)),
675 (35, u16::MAX, Money::new(108, 50)),
676 ]);
677
678 assert_eq!(FUSE_BASED.cost_for(10, 0), None);
680 assert_eq!(FUSE_BASED.cost_for(15, 0), None);
681
682 assert_eq!(FUSE_BASED.cost_for(16, 0), Some(Money::new(54, 0)));
684 assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
685 assert_eq!(FUSE_BASED.cost_for(35, 0), Some(Money::new(54, 0)));
686
687 assert_eq!(FUSE_BASED.cost_for(36, 0), Some(Money::new(108, 50)));
689 assert_eq!(FUSE_BASED.cost_for(50, 0), Some(Money::new(108, 50)));
690 assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
691 assert_eq!(FUSE_BASED.cost_for(u16::MAX, 0), Some(Money::new(108, 50)));
692 }
693
694 #[test]
695 fn cost_for_fuse_range_multiple_ranges() {
696 const MULTI_RANGE: Cost = Cost::fuse_range(&[
697 (1, 15, Money::new(20, 0)),
698 (16, 35, Money::new(50, 0)),
699 (36, 63, Money::new(100, 0)),
700 (64, u16::MAX, Money::new(200, 0)),
701 ]);
702
703 assert_eq!(MULTI_RANGE.cost_for(10, 0), Some(Money::new(20, 0)));
705 assert_eq!(MULTI_RANGE.cost_for(25, 0), Some(Money::new(50, 0)));
706 assert_eq!(MULTI_RANGE.cost_for(50, 0), Some(Money::new(100, 0)));
707 assert_eq!(MULTI_RANGE.cost_for(100, 0), Some(Money::new(200, 0)));
708
709 assert_eq!(MULTI_RANGE.cost_for(25, 10000), Some(Money::new(50, 0)));
711 }
712
713 #[test]
714 fn include_matches_hours() {
715 let include = Include::Hours(Hours::new(6, 22));
716 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
717 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
718
719 assert!(include.matches(timestamp_match));
720 assert!(!include.matches(timestamp_no_match));
721 }
722
723 #[test]
724 fn include_matches_month() {
725 let include = Include::Month(Month::June);
726 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 6, 15, 12, 0, 0).unwrap();
727 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 7, 15, 12, 0, 0).unwrap();
728
729 assert!(include.matches(timestamp_match));
730 assert!(!include.matches(timestamp_no_match));
731 }
732
733 #[test]
734 fn include_matches_months() {
735 let include = Include::Months(Months::new(Month::November, Month::March));
736 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
737 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 7, 15, 12, 0, 0).unwrap();
738
739 assert!(include.matches(timestamp_match));
740 assert!(!include.matches(timestamp_no_match));
741 }
742
743 #[test]
744 fn exclude_matches_weekends_saturday() {
745 let exclude = Exclude::Weekends;
746 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 4, 12, 0, 0).unwrap();
748 assert!(exclude.matches(timestamp));
749 }
750
751 #[test]
752 fn exclude_matches_weekends_sunday() {
753 let exclude = Exclude::Weekends;
754 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
756 assert!(exclude.matches(timestamp));
757 }
758
759 #[test]
760 fn exclude_does_not_match_weekday() {
761 let exclude = Exclude::Weekends;
762 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 6, 12, 0, 0).unwrap();
764 assert!(!exclude.matches(timestamp));
765 }
766
767 #[test]
768 fn exclude_matches_swedish_new_year() {
769 let exclude = Exclude::Holidays(Country::SE);
770 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
772 assert!(exclude.matches(timestamp));
773 }
774
775 #[test]
776 fn exclude_does_not_match_non_holiday() {
777 let exclude = Exclude::Holidays(Country::SE);
778 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 2, 12, 0, 0).unwrap();
780 assert!(!exclude.matches(timestamp));
781 }
782
783 #[test]
784 fn cost_period_matches_with_single_include() {
785 let period = CostPeriod::builder()
786 .load(LoadType::High)
787 .fixed_cost(10, 0)
788 .hours(6, 22)
789 .build();
790
791 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
792 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
793
794 assert!(period.matches(timestamp_match));
795 assert!(!period.matches(timestamp_no_match));
796 }
797
798 #[test]
799 fn cost_period_matches_with_multiple_includes() {
800 let period = CostPeriod::builder()
801 .load(LoadType::High)
802 .fixed_cost(10, 0)
803 .hours(6, 22)
804 .months(Month::November, Month::March)
805 .build();
806
807 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
809 let timestamp_wrong_hours = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
811 let timestamp_wrong_months = Stockholm.with_ymd_and_hms(2025, 7, 15, 14, 0, 0).unwrap();
813
814 assert!(period.matches(timestamp_match));
815 assert!(!period.matches(timestamp_wrong_hours));
816 assert!(!period.matches(timestamp_wrong_months));
817 }
818
819 #[test]
820 fn cost_period_matches_with_exclude_weekends() {
821 let period = CostPeriod::builder()
822 .load(LoadType::High)
823 .fixed_cost(10, 0)
824 .hours(6, 22)
825 .exclude_weekends()
826 .build();
827
828 println!("Excludes: {:?}", period.exclude_period_types());
829 println!("Includes: {:?}", period.include_period_types());
830
831 let timestamp_weekday = Stockholm.with_ymd_and_hms(2025, 1, 6, 14, 0, 0).unwrap();
833 let timestamp_saturday = Stockholm.with_ymd_and_hms(2025, 1, 4, 14, 0, 0).unwrap();
835
836 assert!(period.matches(timestamp_weekday));
837 assert!(!period.matches(timestamp_saturday));
838 }
839
840 #[test]
841 fn cost_period_matches_with_exclude_holidays() {
842 let period = CostPeriod::builder()
843 .load(LoadType::High)
844 .fixed_cost(10, 0)
845 .hours(6, 22)
846 .exclude_holidays(Country::SE)
847 .build();
848
849 let timestamp_regular = Stockholm.with_ymd_and_hms(2025, 1, 2, 14, 0, 0).unwrap();
851 let timestamp_holiday = Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap();
853
854 assert!(period.matches(timestamp_regular));
855 assert!(!period.matches(timestamp_holiday));
856 }
857
858 #[test]
859 fn cost_period_matches_complex_scenario() {
860 let period = CostPeriod::builder()
862 .load(LoadType::High)
863 .fixed_cost(10, 0)
864 .months(Month::November, Month::March)
865 .hours(6, 22)
866 .exclude_weekends()
867 .exclude_holidays(Country::SE)
868 .build();
869
870 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
872
873 let timestamp_wrong_hours = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
875
876 let timestamp_weekend = Stockholm.with_ymd_and_hms(2025, 1, 4, 14, 0, 0).unwrap();
878
879 let timestamp_holiday = Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap();
881
882 let timestamp_summer = Stockholm.with_ymd_and_hms(2025, 7, 15, 14, 0, 0).unwrap();
884
885 assert!(period.matches(timestamp_match));
886 assert!(!period.matches(timestamp_wrong_hours));
887 assert!(!period.matches(timestamp_weekend));
888 assert!(!period.matches(timestamp_holiday));
889 assert!(!period.matches(timestamp_summer));
890 }
891
892 #[test]
893 fn cost_period_matches_base_load() {
894 let period = CostPeriod::builder()
896 .load(LoadType::Base)
897 .fixed_cost(5, 0)
898 .build();
899
900 let timestamp1 = Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
902 let timestamp2 = Stockholm.with_ymd_and_hms(2025, 7, 15, 23, 59, 59).unwrap();
903 let timestamp3 = Stockholm.with_ymd_and_hms(2025, 1, 4, 12, 0, 0).unwrap();
904
905 assert!(period.matches(timestamp1));
906 assert!(period.matches(timestamp2));
907 assert!(period.matches(timestamp3));
908 }
909
910 #[test]
911 fn include_matches_hours_wraparound() {
912 let include = Include::Hours(Hours::new(22, 5));
914
915 let timestamp_evening = Stockholm.with_ymd_and_hms(2025, 1, 15, 22, 0, 0).unwrap();
917 assert!(include.matches(timestamp_evening));
918
919 let timestamp_midnight = Stockholm.with_ymd_and_hms(2025, 1, 15, 0, 0, 0).unwrap();
921 assert!(include.matches(timestamp_midnight));
922
923 let timestamp_morning = Stockholm.with_ymd_and_hms(2025, 1, 15, 5, 30, 0).unwrap();
925 assert!(include.matches(timestamp_morning));
926
927 let timestamp_day = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
929 assert!(!include.matches(timestamp_day));
930
931 let timestamp_after = Stockholm.with_ymd_and_hms(2025, 1, 15, 6, 0, 0).unwrap();
933 assert!(!include.matches(timestamp_after));
934
935 let timestamp_before = Stockholm.with_ymd_and_hms(2025, 1, 15, 21, 59, 59).unwrap();
937 assert!(!include.matches(timestamp_before));
938 }
939
940 #[test]
941 fn include_matches_months_wraparound() {
942 let include = Include::Months(Months::new(Month::November, Month::March));
944
945 let timestamp_nov = Stockholm.with_ymd_and_hms(2025, 11, 15, 12, 0, 0).unwrap();
947 assert!(include.matches(timestamp_nov));
948
949 let timestamp_dec = Stockholm.with_ymd_and_hms(2025, 12, 15, 12, 0, 0).unwrap();
951 assert!(include.matches(timestamp_dec));
952
953 let timestamp_jan = Stockholm.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
955 assert!(include.matches(timestamp_jan));
956
957 let timestamp_mar = Stockholm.with_ymd_and_hms(2025, 3, 15, 12, 0, 0).unwrap();
959 assert!(include.matches(timestamp_mar));
960
961 let timestamp_jul = Stockholm.with_ymd_and_hms(2025, 7, 15, 12, 0, 0).unwrap();
963 assert!(!include.matches(timestamp_jul));
964
965 let timestamp_oct = Stockholm
967 .with_ymd_and_hms(2025, 10, 31, 23, 59, 59)
968 .unwrap();
969 assert!(!include.matches(timestamp_oct));
970
971 let timestamp_apr = Stockholm.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
973 assert!(!include.matches(timestamp_apr));
974 }
975
976 #[test]
977 fn cost_period_matches_hours_wraparound() {
978 let period = CostPeriod::builder()
980 .load(LoadType::Low)
981 .fixed_cost(5, 0)
982 .hours(22, 5)
983 .build();
984
985 let timestamp_match_evening = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
986 let timestamp_match_morning = Stockholm.with_ymd_and_hms(2025, 1, 15, 3, 0, 0).unwrap();
987 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
988
989 assert!(period.matches(timestamp_match_evening));
990 assert!(period.matches(timestamp_match_morning));
991 assert!(!period.matches(timestamp_no_match));
992 }
993
994 #[test]
995 fn cost_period_matches_with_both_excludes() {
996 let period = CostPeriod::builder()
997 .load(LoadType::High)
998 .fixed_cost(10, 0)
999 .hours(6, 22)
1000 .exclude_weekends()
1001 .exclude_holidays(Country::SE)
1002 .build();
1003
1004 let weekday = Stockholm.with_ymd_and_hms(2025, 1, 2, 14, 0, 0).unwrap();
1006 assert!(period.matches(weekday));
1007
1008 let saturday = Stockholm.with_ymd_and_hms(2025, 1, 4, 14, 0, 0).unwrap();
1010 assert!(!period.matches(saturday));
1011
1012 let holiday = Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap();
1014 assert!(!period.matches(holiday));
1015
1016 let wrong_hours = Stockholm.with_ymd_and_hms(2025, 1, 2, 23, 0, 0).unwrap();
1018 assert!(!period.matches(wrong_hours));
1019 }
1020
1021 #[test]
1022 fn exclude_matches_friday_is_not_weekend() {
1023 let exclude = Exclude::Weekends;
1024 let friday = Stockholm.with_ymd_and_hms(2025, 1, 3, 12, 0, 0).unwrap();
1026 assert!(!exclude.matches(friday));
1027 }
1028
1029 #[test]
1030 fn exclude_matches_monday_is_not_weekend() {
1031 let exclude = Exclude::Weekends;
1032 let monday = Stockholm.with_ymd_and_hms(2025, 1, 6, 12, 0, 0).unwrap();
1034 assert!(!exclude.matches(monday));
1035 }
1036
1037 #[test]
1038 fn exclude_matches_holiday_midsummer() {
1039 let exclude = Exclude::Holidays(Country::SE);
1040 let midsummer = Stockholm.with_ymd_and_hms(2025, 6, 21, 12, 0, 0).unwrap();
1042 assert!(exclude.matches(midsummer));
1043 }
1044
1045 #[test]
1046 fn cost_period_matches_month_and_hours() {
1047 let period = CostPeriod::builder()
1049 .load(LoadType::Low)
1050 .fixed_cost(5, 0)
1051 .month(Month::June)
1052 .hours(22, 5)
1053 .build();
1054
1055 let match_june_night = Stockholm.with_ymd_and_hms(2025, 6, 15, 23, 0, 0).unwrap();
1057 assert!(period.matches(match_june_night));
1058
1059 let june_day = Stockholm.with_ymd_and_hms(2025, 6, 15, 14, 0, 0).unwrap();
1061 assert!(!period.matches(june_day));
1062
1063 let july_night = Stockholm.with_ymd_and_hms(2025, 7, 15, 23, 0, 0).unwrap();
1065 assert!(!period.matches(july_night));
1066 }
1067
1068 #[test]
1069 fn cost_period_matches_months_and_hours_with_exclude() {
1070 let period = CostPeriod::builder()
1072 .load(LoadType::High)
1073 .fixed_cost(15, 0)
1074 .months(Month::November, Month::March)
1075 .hours(6, 22)
1076 .exclude_weekends()
1077 .exclude_holidays(Country::SE)
1078 .build();
1079
1080 let perfect = Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap();
1082 assert!(period.matches(perfect));
1083
1084 let first_hour = Stockholm.with_ymd_and_hms(2025, 1, 15, 6, 0, 0).unwrap();
1086 assert!(period.matches(first_hour));
1087
1088 let last_hour = Stockholm.with_ymd_and_hms(2025, 1, 15, 22, 59, 59).unwrap();
1090 assert!(period.matches(last_hour));
1091
1092 let too_early = Stockholm.with_ymd_and_hms(2025, 1, 15, 5, 59, 59).unwrap();
1094 assert!(!period.matches(too_early));
1095
1096 let too_late = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
1098 assert!(!period.matches(too_late));
1099
1100 let summer = Stockholm.with_ymd_and_hms(2025, 7, 15, 10, 0, 0).unwrap();
1102 assert!(!period.matches(summer));
1103
1104 let weekend = Stockholm.with_ymd_and_hms(2025, 1, 4, 10, 0, 0).unwrap();
1106 assert!(!period.matches(weekend));
1107 }
1108
1109 #[test]
1110 fn cost_period_matches_base_with_restrictions() {
1111 let period = CostPeriod::builder()
1113 .load(LoadType::Base)
1114 .fixed_cost(3, 0)
1115 .hours(0, 5)
1116 .build();
1117
1118 let match_night = Stockholm.with_ymd_and_hms(2025, 1, 15, 3, 0, 0).unwrap();
1120 assert!(period.matches(match_night));
1121
1122 let no_match_day = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
1124 assert!(!period.matches(no_match_day));
1125 }
1126
1127 #[test]
1128 fn cost_period_matches_single_month() {
1129 let period = CostPeriod::builder()
1130 .load(LoadType::High)
1131 .fixed_cost(10, 0)
1132 .month(Month::December)
1133 .build();
1134
1135 let dec_first = Stockholm.with_ymd_and_hms(2025, 12, 1, 0, 0, 0).unwrap();
1137 assert!(period.matches(dec_first));
1138
1139 let dec_last = Stockholm
1141 .with_ymd_and_hms(2025, 12, 31, 23, 59, 59)
1142 .unwrap();
1143 assert!(period.matches(dec_last));
1144
1145 let nov = Stockholm.with_ymd_and_hms(2025, 11, 30, 12, 0, 0).unwrap();
1147 assert!(!period.matches(nov));
1148
1149 let jan = Stockholm.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
1151 assert!(!period.matches(jan));
1152 }
1153
1154 #[test]
1155 fn cost_period_matches_all_hours() {
1156 let period = CostPeriod::builder()
1158 .load(LoadType::Low)
1159 .fixed_cost(5, 0)
1160 .hours(0, 23)
1161 .build();
1162
1163 let midnight = Stockholm.with_ymd_and_hms(2025, 1, 15, 0, 0, 0).unwrap();
1164 let noon = Stockholm.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
1165 let almost_midnight = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 59, 59).unwrap();
1166
1167 assert!(period.matches(midnight));
1168 assert!(period.matches(noon));
1169 assert!(period.matches(almost_midnight));
1170 }
1171
1172 #[test]
1173 fn cost_period_matches_edge_of_month_range() {
1174 let period = CostPeriod::builder()
1176 .load(LoadType::Low)
1177 .fixed_cost(5, 0)
1178 .months(Month::May, Month::September)
1179 .build();
1180
1181 let may_start = Stockholm.with_ymd_and_hms(2025, 5, 1, 0, 0, 0).unwrap();
1183 assert!(period.matches(may_start));
1184
1185 let april_end = Stockholm.with_ymd_and_hms(2025, 4, 30, 23, 59, 59).unwrap();
1187 assert!(!period.matches(april_end));
1188
1189 let sept_end = Stockholm.with_ymd_and_hms(2025, 9, 30, 23, 59, 59).unwrap();
1191 assert!(period.matches(sept_end));
1192
1193 let oct_start = Stockholm.with_ymd_and_hms(2025, 10, 1, 0, 0, 0).unwrap();
1195 assert!(!period.matches(oct_start));
1196 }
1197
1198 #[test]
1199 fn include_matches_month_boundary() {
1200 let include = Include::Month(Month::February);
1202
1203 let feb_start = Stockholm.with_ymd_and_hms(2025, 2, 1, 0, 0, 0).unwrap();
1205 assert!(include.matches(feb_start));
1206
1207 let feb_end = Stockholm.with_ymd_and_hms(2025, 2, 28, 23, 59, 59).unwrap();
1209 assert!(include.matches(feb_end));
1210
1211 let jan_end = Stockholm.with_ymd_and_hms(2025, 1, 31, 23, 59, 59).unwrap();
1213 assert!(!include.matches(jan_end));
1214
1215 let mar_start = Stockholm.with_ymd_and_hms(2025, 3, 1, 0, 0, 0).unwrap();
1217 assert!(!include.matches(mar_start));
1218 }
1219
1220 #[test]
1221 fn include_matches_hours_exact_boundaries() {
1222 let include = Include::Hours(Hours::new(6, 22));
1223
1224 let start = Stockholm.with_ymd_and_hms(2025, 1, 15, 6, 0, 0).unwrap();
1226 assert!(include.matches(start));
1227
1228 let end = Stockholm.with_ymd_and_hms(2025, 1, 15, 22, 59, 59).unwrap();
1230 assert!(include.matches(end));
1231
1232 let before = Stockholm.with_ymd_and_hms(2025, 1, 15, 5, 59, 59).unwrap();
1234 assert!(!include.matches(before));
1235
1236 let after = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
1238 assert!(!include.matches(after));
1239 }
1240}