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