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
186#[derive(Debug, Clone, Serialize)]
188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
189pub struct CostPeriodsSimple {
190 periods: Vec<CostPeriodSimple>,
191}
192
193impl CostPeriodsSimple {
194 pub(crate) fn new(
195 periods: CostPeriods,
196 fuse_size: u16,
197 yearly_consumption: u32,
198 language: Language,
199 ) -> Self {
200 Self {
201 periods: periods
202 .periods
203 .iter()
204 .flat_map(|period| {
205 CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
206 })
207 .collect(),
208 }
209 }
210}
211
212#[derive(Debug, Clone, Serialize)]
213#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
214pub struct CostPeriod {
215 cost: Cost,
216 load: LoadType,
217 #[serde(serialize_with = "helpers::skip_nones")]
218 include: [Option<Include>; 2],
219 #[serde(serialize_with = "helpers::skip_nones")]
220 exclude: [Option<Exclude>; 2],
221 divide_kw_by: u8,
223}
224
225#[derive(Debug, Clone, Serialize)]
227#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
228pub(super) struct CostPeriodSimple {
229 cost: Money,
230 load: LoadType,
231 include: Vec<Include>,
232 exclude: Vec<Exclude>,
233 divide_kw_by: u8,
235 info: String,
236}
237
238impl CostPeriodSimple {
239 fn new(
240 period: &CostPeriod,
241 fuse_size: u16,
242 yearly_consumption: u32,
243 language: Language,
244 ) -> Option<Self> {
245 let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
246 Some(
247 Self {
248 cost,
249 load: period.load,
250 include: period.include.into_iter().flatten().collect(),
251 exclude: period.exclude.into_iter().flatten().collect(),
252 divide_kw_by: period.divide_kw_by,
253 info: Default::default(),
254 }
255 .add_info(language),
256 )
257 }
258
259 fn add_info(mut self, language: Language) -> Self {
260 let mut infos = Vec::new();
261 for include in &self.include {
262 infos.push(include.translate(language));
263 }
264 for exclude in &self.exclude {
265 infos.push(exclude.translate(language).into());
266 }
267 self.info = infos.join(", ");
268 self
269 }
270}
271
272impl CostPeriod {
273 pub(super) const fn builder() -> CostPeriodBuilder {
274 CostPeriodBuilder::new()
275 }
276
277 pub const fn cost(&self) -> Cost {
278 self.cost
279 }
280
281 pub const fn load(&self) -> LoadType {
282 self.load
283 }
284
285 pub const fn power_multiplier(&self) -> f64 {
286 1. / self.divide_kw_by as f64
287 }
288
289 pub fn matches(&self, timestamp: DateTime<Tz>) -> bool {
290 for include in self.include_period_types() {
291 if !include.matches(timestamp) {
292 return false;
293 }
294 }
295
296 for exclude in self.exclude_period_types() {
297 if exclude.matches(timestamp) {
298 return false;
299 }
300 }
301 true
302 }
303
304 fn include_period_types(&self) -> Vec<Include> {
305 self.include.iter().flatten().copied().collect()
306 }
307
308 fn exclude_period_types(&self) -> Vec<Exclude> {
309 self.exclude.iter().flatten().copied().collect()
310 }
311
312 fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
313 self.cost.is_yearly_consumption_based(fuse_size)
314 }
315}
316
317#[derive(Debug, Clone, Copy, Serialize)]
318#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
319pub enum LoadType {
320 Base,
322 Low,
324 High,
326}
327
328pub(super) use LoadType::*;
329
330#[derive(Clone)]
331pub(super) struct CostPeriodBuilder {
332 cost: Cost,
333 load: Option<LoadType>,
334 include: [Option<Include>; 2],
335 exclude: [Option<Exclude>; 2],
336 divide_kw_by: u8,
338}
339
340impl CostPeriodBuilder {
341 pub(super) const fn new() -> Self {
342 Self {
343 cost: Cost::None,
344 load: None,
345 include: [None; 2],
346 exclude: [None; 2],
347 divide_kw_by: 1,
348 }
349 }
350
351 pub(super) const fn build(self) -> CostPeriod {
352 CostPeriod {
353 cost: self.cost,
354 load: self.load.expect("`load` must be specified"),
355 include: self.include,
356 exclude: self.exclude,
357 divide_kw_by: self.divide_kw_by,
358 }
359 }
360
361 pub(super) const fn cost(mut self, cost: Cost) -> Self {
362 self.cost = cost;
363 self
364 }
365
366 pub(super) const fn load(mut self, load: LoadType) -> Self {
367 self.load = Some(load);
368 self
369 }
370
371 pub(super) const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
372 self.cost = Cost::fixed(int, fract);
373 self
374 }
375
376 pub(super) const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
377 self.cost = Cost::fixed_subunit(subunit);
378 self
379 }
380
381 pub(super) const fn include(mut self, period_type: Include) -> Self {
382 let mut i = 0;
383 while i < self.include.len() {
384 if self.include[i].is_some() {
385 i += 1;
386 } else {
387 self.include[i] = Some(period_type);
388 return self;
389 }
390 }
391 panic!("Too many includes");
392 }
393
394 pub(super) const fn months(self, from: Month, to: Month) -> Self {
395 self.include(Include::Months(Months::new(from, to)))
396 }
397
398 pub(super) const fn month(self, month: Month) -> Self {
399 self.include(Include::Month(month))
400 }
401
402 pub(super) const fn hours(self, from: u8, to_inclusive: u8) -> Self {
403 self.include(Include::Hours(Hours::new(from, to_inclusive)))
404 }
405
406 pub(super) const fn exclude(mut self, period_type: Exclude) -> Self {
407 let mut i = 0;
408 while i < self.exclude.len() {
409 if self.exclude[i].is_some() {
410 i += 1;
411 } else {
412 self.exclude[i] = Some(period_type);
413 return self;
414 }
415 }
416 panic!("Too many excludes");
417 }
418
419 pub(super) const fn exclude_holidays(self, country: Country) -> Self {
420 self.exclude(Exclude::Holidays(country))
421 }
422
423 pub(super) const fn exclude_weekends(self) -> Self {
424 self.exclude(Exclude::Weekends)
425 }
426
427 pub(super) const fn divide_kw_by(mut self, value: u8) -> Self {
428 self.divide_kw_by = value;
429 self
430 }
431}
432
433#[derive(Debug, Clone, Copy, Serialize)]
434#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
435pub(super) enum Include {
436 Months(Months),
437 Month(Month),
438 Hours(Hours),
439}
440
441impl Include {
442 fn translate(&self, language: Language) -> String {
443 match self {
444 Include::Months(months) => months.translate(language),
445 Include::Month(month) => month.translate(language).into(),
446 Include::Hours(hours) => hours.translate(language),
447 }
448 }
449
450 fn matches(&self, timestamp: DateTime<Tz>) -> bool {
451 match self {
452 Include::Months(months) => months.matches(timestamp),
453 Include::Month(month) => month.matches(timestamp),
454 Include::Hours(hours) => hours.matches(timestamp),
455 }
456 }
457}
458
459#[derive(Debug, Clone, Copy, Serialize)]
460#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
461pub(super) enum Exclude {
462 Weekends,
463 Holidays(Country),
464}
465
466impl Exclude {
467 pub(super) fn translate(&self, language: Language) -> &'static str {
468 match language {
469 Language::En => match self {
470 Exclude::Weekends => "Weekends",
471 Exclude::Holidays(country) => match country {
472 Country::SE => "Swedish holidays",
473 },
474 },
475 Language::Sv => match self {
476 Exclude::Weekends => "Helg",
477 Exclude::Holidays(country) => match country {
478 Country::SE => "Svenska helgdagar",
479 },
480 },
481 }
482 }
483
484 fn matches(&self, timestamp: DateTime<Tz>) -> bool {
485 match self {
486 Exclude::Weekends => (6..=7).contains(×tamp.weekday().number_from_monday()),
487 Exclude::Holidays(country) => country.is_holiday(timestamp.date_naive()),
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::defs::{Hours, Month, Months};
496 use crate::money::Money;
497 use chrono::TimeZone;
498 use chrono_tz::Europe::Stockholm;
499
500 #[test]
501 fn cost_for_none() {
502 const NONE_COST: Cost = Cost::None;
503 assert_eq!(NONE_COST.cost_for(16, 0), None);
504 assert_eq!(NONE_COST.cost_for(25, 5000), None);
505 }
506
507 #[test]
508 fn cost_for_unverified() {
509 const UNVERIFIED_COST: Cost = Cost::Unverified;
510 assert_eq!(UNVERIFIED_COST.cost_for(16, 0), None);
511 assert_eq!(UNVERIFIED_COST.cost_for(25, 5000), None);
512 }
513
514 #[test]
515 fn cost_for_fixed() {
516 const FIXED_COST: Cost = Cost::Fixed(Money::new(100, 50));
517 assert_eq!(FIXED_COST.cost_for(16, 0), Some(Money::new(100, 50)));
519 assert_eq!(FIXED_COST.cost_for(25, 5000), Some(Money::new(100, 50)));
520 assert_eq!(FIXED_COST.cost_for(63, 10000), Some(Money::new(100, 50)));
521 }
522
523 #[test]
524 fn cost_for_fuses_exact_match() {
525 const FUSES_COST: Cost = Cost::fuses(&[
526 (16, Money::new(50, 0)),
527 (25, Money::new(75, 0)),
528 (35, Money::new(100, 0)),
529 (50, Money::new(150, 0)),
530 ]);
531
532 assert_eq!(FUSES_COST.cost_for(16, 0), Some(Money::new(50, 0)));
534 assert_eq!(FUSES_COST.cost_for(25, 0), Some(Money::new(75, 0)));
535 assert_eq!(FUSES_COST.cost_for(35, 0), Some(Money::new(100, 0)));
536 assert_eq!(FUSES_COST.cost_for(50, 0), Some(Money::new(150, 0)));
537
538 assert_eq!(FUSES_COST.cost_for(25, 500000), Some(Money::new(75, 0)));
540 }
541
542 #[test]
543 fn cost_for_fuses_no_match() {
544 const FUSES_COST: Cost = Cost::fuses(&[(16, Money::new(50, 0)), (25, Money::new(75, 0))]);
545
546 assert_eq!(FUSES_COST.cost_for(20, 0), None);
548 assert_eq!(FUSES_COST.cost_for(63, 0), None);
549 }
550
551 #[test]
552 fn cost_for_fuses_yearly_consumption_with_limit() {
553 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
554 (16, Some(5000), Money::new(50, 0)),
555 (16, None, Money::new(75, 0)),
556 (25, Some(10000), Money::new(100, 0)),
557 (25, None, Money::new(125, 0)),
558 ]);
559
560 assert_eq!(
562 FUSES_WITH_CONSUMPTION.cost_for(16, 3000),
563 Some(Money::new(50, 0))
564 );
565
566 assert_eq!(
568 FUSES_WITH_CONSUMPTION.cost_for(16, 5000),
569 Some(Money::new(50, 0))
570 );
571
572 assert_eq!(
574 FUSES_WITH_CONSUMPTION.cost_for(16, 6000),
575 Some(Money::new(75, 0))
576 );
577
578 assert_eq!(
580 FUSES_WITH_CONSUMPTION.cost_for(16, 20000),
581 Some(Money::new(75, 0))
582 );
583
584 assert_eq!(
586 FUSES_WITH_CONSUMPTION.cost_for(25, 10000),
587 Some(Money::new(100, 0))
588 );
589
590 assert_eq!(
592 FUSES_WITH_CONSUMPTION.cost_for(25, 15000),
593 Some(Money::new(125, 0))
594 );
595
596 assert_eq!(
598 FUSES_WITH_CONSUMPTION.cost_for(25, 5000),
599 Some(Money::new(100, 0))
600 );
601 }
602
603 #[test]
604 fn cost_for_fuses_yearly_consumption_no_limit() {
605 const FUSES_NO_LIMIT: Cost = Cost::fuses_with_yearly_consumption(&[
606 (16, None, Money::new(50, 0)),
607 (25, None, Money::new(75, 0)),
608 ]);
609
610 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 0), Some(Money::new(50, 0)));
612 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 1000), Some(Money::new(50, 0)));
613 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 50000), Some(Money::new(50, 0)));
614 assert_eq!(FUSES_NO_LIMIT.cost_for(25, 100000), Some(Money::new(75, 0)));
615 }
616
617 #[test]
618 fn cost_for_fuses_yearly_consumption_no_fuse_match() {
619 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
620 (16, Some(5000), Money::new(50, 0)),
621 (25, Some(10000), Money::new(100, 0)),
622 ]);
623
624 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(35, 5000), None);
626 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(50, 10000), None);
627 }
628
629 #[test]
630 fn cost_for_fuses_yearly_consumption_max_limit_no_fallback() {
631 const FUSES_ONLY_LIMITS: Cost = Cost::fuses_with_yearly_consumption(&[
632 (16, Some(5000), Money::new(50, 0)),
633 (25, Some(10000), Money::new(100, 0)),
634 ]);
635
636 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 0), Some(Money::new(50, 0)));
638 assert_eq!(
639 FUSES_ONLY_LIMITS.cost_for(16, 3000),
640 Some(Money::new(50, 0))
641 );
642 assert_eq!(
643 FUSES_ONLY_LIMITS.cost_for(16, 4999),
644 Some(Money::new(50, 0))
645 );
646 assert_eq!(
647 FUSES_ONLY_LIMITS.cost_for(16, 5000),
648 Some(Money::new(50, 0))
649 );
650 assert_eq!(
651 FUSES_ONLY_LIMITS.cost_for(25, 9999),
652 Some(Money::new(100, 0))
653 );
654 assert_eq!(
655 FUSES_ONLY_LIMITS.cost_for(25, 10000),
656 Some(Money::new(100, 0))
657 );
658
659 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 5001), None);
661 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 10000), None);
662 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 10001), None);
663 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 20000), None);
664 }
665
666 #[test]
667 fn cost_for_fuse_range_within_range() {
668 const FUSE_BASED: Cost = Cost::fuse_range(&[
669 (16, 35, Money::new(54, 0)),
670 (35, u16::MAX, Money::new(108, 50)),
671 ]);
672
673 assert_eq!(FUSE_BASED.cost_for(10, 0), None);
675 assert_eq!(FUSE_BASED.cost_for(15, 0), None);
676
677 assert_eq!(FUSE_BASED.cost_for(16, 0), Some(Money::new(54, 0)));
679 assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
680 assert_eq!(FUSE_BASED.cost_for(35, 0), Some(Money::new(54, 0)));
681
682 assert_eq!(FUSE_BASED.cost_for(36, 0), Some(Money::new(108, 50)));
684 assert_eq!(FUSE_BASED.cost_for(50, 0), Some(Money::new(108, 50)));
685 assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
686 assert_eq!(FUSE_BASED.cost_for(u16::MAX, 0), Some(Money::new(108, 50)));
687 }
688
689 #[test]
690 fn cost_for_fuse_range_multiple_ranges() {
691 const MULTI_RANGE: Cost = Cost::fuse_range(&[
692 (1, 15, Money::new(20, 0)),
693 (16, 35, Money::new(50, 0)),
694 (36, 63, Money::new(100, 0)),
695 (64, u16::MAX, Money::new(200, 0)),
696 ]);
697
698 assert_eq!(MULTI_RANGE.cost_for(10, 0), Some(Money::new(20, 0)));
700 assert_eq!(MULTI_RANGE.cost_for(25, 0), Some(Money::new(50, 0)));
701 assert_eq!(MULTI_RANGE.cost_for(50, 0), Some(Money::new(100, 0)));
702 assert_eq!(MULTI_RANGE.cost_for(100, 0), Some(Money::new(200, 0)));
703
704 assert_eq!(MULTI_RANGE.cost_for(25, 10000), Some(Money::new(50, 0)));
706 }
707
708 #[test]
709 fn include_matches_hours() {
710 let include = Include::Hours(Hours::new(6, 22));
711 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
712 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
713
714 assert!(include.matches(timestamp_match));
715 assert!(!include.matches(timestamp_no_match));
716 }
717
718 #[test]
719 fn include_matches_month() {
720 let include = Include::Month(Month::June);
721 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 6, 15, 12, 0, 0).unwrap();
722 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 7, 15, 12, 0, 0).unwrap();
723
724 assert!(include.matches(timestamp_match));
725 assert!(!include.matches(timestamp_no_match));
726 }
727
728 #[test]
729 fn include_matches_months() {
730 let include = Include::Months(Months::new(Month::November, Month::March));
731 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
732 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 7, 15, 12, 0, 0).unwrap();
733
734 assert!(include.matches(timestamp_match));
735 assert!(!include.matches(timestamp_no_match));
736 }
737
738 #[test]
739 fn exclude_matches_weekends_saturday() {
740 let exclude = Exclude::Weekends;
741 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 4, 12, 0, 0).unwrap();
743 assert!(exclude.matches(timestamp));
744 }
745
746 #[test]
747 fn exclude_matches_weekends_sunday() {
748 let exclude = Exclude::Weekends;
749 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 5, 12, 0, 0).unwrap();
751 assert!(exclude.matches(timestamp));
752 }
753
754 #[test]
755 fn exclude_does_not_match_weekday() {
756 let exclude = Exclude::Weekends;
757 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 6, 12, 0, 0).unwrap();
759 assert!(!exclude.matches(timestamp));
760 }
761
762 #[test]
763 fn exclude_matches_swedish_new_year() {
764 let exclude = Exclude::Holidays(Country::SE);
765 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
767 assert!(exclude.matches(timestamp));
768 }
769
770 #[test]
771 fn exclude_does_not_match_non_holiday() {
772 let exclude = Exclude::Holidays(Country::SE);
773 let timestamp = Stockholm.with_ymd_and_hms(2025, 1, 2, 12, 0, 0).unwrap();
775 assert!(!exclude.matches(timestamp));
776 }
777
778 #[test]
779 fn cost_period_matches_with_single_include() {
780 let period = CostPeriod::builder()
781 .load(LoadType::High)
782 .fixed_cost(10, 0)
783 .hours(6, 22)
784 .build();
785
786 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
787 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
788
789 assert!(period.matches(timestamp_match));
790 assert!(!period.matches(timestamp_no_match));
791 }
792
793 #[test]
794 fn cost_period_matches_with_multiple_includes() {
795 let period = CostPeriod::builder()
796 .load(LoadType::High)
797 .fixed_cost(10, 0)
798 .hours(6, 22)
799 .months(Month::November, Month::March)
800 .build();
801
802 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
804 let timestamp_wrong_hours = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
806 let timestamp_wrong_months = Stockholm.with_ymd_and_hms(2025, 7, 15, 14, 0, 0).unwrap();
808
809 assert!(period.matches(timestamp_match));
810 assert!(!period.matches(timestamp_wrong_hours));
811 assert!(!period.matches(timestamp_wrong_months));
812 }
813
814 #[test]
815 fn cost_period_matches_with_exclude_weekends() {
816 let period = CostPeriod::builder()
817 .load(LoadType::High)
818 .fixed_cost(10, 0)
819 .hours(6, 22)
820 .exclude_weekends()
821 .build();
822
823 println!("Excludes: {:?}", period.exclude_period_types());
824 println!("Includes: {:?}", period.include_period_types());
825
826 let timestamp_weekday = Stockholm.with_ymd_and_hms(2025, 1, 6, 14, 0, 0).unwrap();
828 let timestamp_saturday = Stockholm.with_ymd_and_hms(2025, 1, 4, 14, 0, 0).unwrap();
830
831 assert!(period.matches(timestamp_weekday));
832 assert!(!period.matches(timestamp_saturday));
833 }
834
835 #[test]
836 fn cost_period_matches_with_exclude_holidays() {
837 let period = CostPeriod::builder()
838 .load(LoadType::High)
839 .fixed_cost(10, 0)
840 .hours(6, 22)
841 .exclude_holidays(Country::SE)
842 .build();
843
844 let timestamp_regular = Stockholm.with_ymd_and_hms(2025, 1, 2, 14, 0, 0).unwrap();
846 let timestamp_holiday = Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap();
848
849 assert!(period.matches(timestamp_regular));
850 assert!(!period.matches(timestamp_holiday));
851 }
852
853 #[test]
854 fn cost_period_matches_complex_scenario() {
855 let period = CostPeriod::builder()
857 .load(LoadType::High)
858 .fixed_cost(10, 0)
859 .months(Month::November, Month::March)
860 .hours(6, 22)
861 .exclude_weekends()
862 .exclude_holidays(Country::SE)
863 .build();
864
865 let timestamp_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
867
868 let timestamp_wrong_hours = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
870
871 let timestamp_weekend = Stockholm.with_ymd_and_hms(2025, 1, 4, 14, 0, 0).unwrap();
873
874 let timestamp_holiday = Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap();
876
877 let timestamp_summer = Stockholm.with_ymd_and_hms(2025, 7, 15, 14, 0, 0).unwrap();
879
880 assert!(period.matches(timestamp_match));
881 assert!(!period.matches(timestamp_wrong_hours));
882 assert!(!period.matches(timestamp_weekend));
883 assert!(!period.matches(timestamp_holiday));
884 assert!(!period.matches(timestamp_summer));
885 }
886
887 #[test]
888 fn cost_period_matches_base_load() {
889 let period = CostPeriod::builder()
891 .load(LoadType::Base)
892 .fixed_cost(5, 0)
893 .build();
894
895 let timestamp1 = Stockholm.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
897 let timestamp2 = Stockholm.with_ymd_and_hms(2025, 7, 15, 23, 59, 59).unwrap();
898 let timestamp3 = Stockholm.with_ymd_and_hms(2025, 1, 4, 12, 0, 0).unwrap();
899
900 assert!(period.matches(timestamp1));
901 assert!(period.matches(timestamp2));
902 assert!(period.matches(timestamp3));
903 }
904
905 #[test]
906 fn include_matches_hours_wraparound() {
907 let include = Include::Hours(Hours::new(22, 5));
909
910 let timestamp_evening = Stockholm.with_ymd_and_hms(2025, 1, 15, 22, 0, 0).unwrap();
912 assert!(include.matches(timestamp_evening));
913
914 let timestamp_midnight = Stockholm.with_ymd_and_hms(2025, 1, 15, 0, 0, 0).unwrap();
916 assert!(include.matches(timestamp_midnight));
917
918 let timestamp_morning = Stockholm.with_ymd_and_hms(2025, 1, 15, 5, 30, 0).unwrap();
920 assert!(include.matches(timestamp_morning));
921
922 let timestamp_day = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
924 assert!(!include.matches(timestamp_day));
925
926 let timestamp_after = Stockholm.with_ymd_and_hms(2025, 1, 15, 6, 0, 0).unwrap();
928 assert!(!include.matches(timestamp_after));
929
930 let timestamp_before = Stockholm.with_ymd_and_hms(2025, 1, 15, 21, 59, 59).unwrap();
932 assert!(!include.matches(timestamp_before));
933 }
934
935 #[test]
936 fn include_matches_months_wraparound() {
937 let include = Include::Months(Months::new(Month::November, Month::March));
939
940 let timestamp_nov = Stockholm.with_ymd_and_hms(2025, 11, 15, 12, 0, 0).unwrap();
942 assert!(include.matches(timestamp_nov));
943
944 let timestamp_dec = Stockholm.with_ymd_and_hms(2025, 12, 15, 12, 0, 0).unwrap();
946 assert!(include.matches(timestamp_dec));
947
948 let timestamp_jan = Stockholm.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
950 assert!(include.matches(timestamp_jan));
951
952 let timestamp_mar = Stockholm.with_ymd_and_hms(2025, 3, 15, 12, 0, 0).unwrap();
954 assert!(include.matches(timestamp_mar));
955
956 let timestamp_jul = Stockholm.with_ymd_and_hms(2025, 7, 15, 12, 0, 0).unwrap();
958 assert!(!include.matches(timestamp_jul));
959
960 let timestamp_oct = Stockholm
962 .with_ymd_and_hms(2025, 10, 31, 23, 59, 59)
963 .unwrap();
964 assert!(!include.matches(timestamp_oct));
965
966 let timestamp_apr = Stockholm.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap();
968 assert!(!include.matches(timestamp_apr));
969 }
970
971 #[test]
972 fn cost_period_matches_hours_wraparound() {
973 let period = CostPeriod::builder()
975 .load(LoadType::Low)
976 .fixed_cost(5, 0)
977 .hours(22, 5)
978 .build();
979
980 let timestamp_match_evening = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
981 let timestamp_match_morning = Stockholm.with_ymd_and_hms(2025, 1, 15, 3, 0, 0).unwrap();
982 let timestamp_no_match = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
983
984 assert!(period.matches(timestamp_match_evening));
985 assert!(period.matches(timestamp_match_morning));
986 assert!(!period.matches(timestamp_no_match));
987 }
988
989 #[test]
990 fn cost_period_matches_with_both_excludes() {
991 let period = CostPeriod::builder()
992 .load(LoadType::High)
993 .fixed_cost(10, 0)
994 .hours(6, 22)
995 .exclude_weekends()
996 .exclude_holidays(Country::SE)
997 .build();
998
999 let weekday = Stockholm.with_ymd_and_hms(2025, 1, 2, 14, 0, 0).unwrap();
1001 assert!(period.matches(weekday));
1002
1003 let saturday = Stockholm.with_ymd_and_hms(2025, 1, 4, 14, 0, 0).unwrap();
1005 assert!(!period.matches(saturday));
1006
1007 let holiday = Stockholm.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap();
1009 assert!(!period.matches(holiday));
1010
1011 let wrong_hours = Stockholm.with_ymd_and_hms(2025, 1, 2, 23, 0, 0).unwrap();
1013 assert!(!period.matches(wrong_hours));
1014 }
1015
1016 #[test]
1017 fn exclude_matches_friday_is_not_weekend() {
1018 let exclude = Exclude::Weekends;
1019 let friday = Stockholm.with_ymd_and_hms(2025, 1, 3, 12, 0, 0).unwrap();
1021 assert!(!exclude.matches(friday));
1022 }
1023
1024 #[test]
1025 fn exclude_matches_monday_is_not_weekend() {
1026 let exclude = Exclude::Weekends;
1027 let monday = Stockholm.with_ymd_and_hms(2025, 1, 6, 12, 0, 0).unwrap();
1029 assert!(!exclude.matches(monday));
1030 }
1031
1032 #[test]
1033 fn exclude_matches_holiday_midsummer() {
1034 let exclude = Exclude::Holidays(Country::SE);
1035 let midsummer = Stockholm.with_ymd_and_hms(2025, 6, 21, 12, 0, 0).unwrap();
1037 assert!(exclude.matches(midsummer));
1038 }
1039
1040 #[test]
1041 fn cost_period_matches_month_and_hours() {
1042 let period = CostPeriod::builder()
1044 .load(LoadType::Low)
1045 .fixed_cost(5, 0)
1046 .month(Month::June)
1047 .hours(22, 5)
1048 .build();
1049
1050 let match_june_night = Stockholm.with_ymd_and_hms(2025, 6, 15, 23, 0, 0).unwrap();
1052 assert!(period.matches(match_june_night));
1053
1054 let june_day = Stockholm.with_ymd_and_hms(2025, 6, 15, 14, 0, 0).unwrap();
1056 assert!(!period.matches(june_day));
1057
1058 let july_night = Stockholm.with_ymd_and_hms(2025, 7, 15, 23, 0, 0).unwrap();
1060 assert!(!period.matches(july_night));
1061 }
1062
1063 #[test]
1064 fn cost_period_matches_months_and_hours_with_exclude() {
1065 let period = CostPeriod::builder()
1067 .load(LoadType::High)
1068 .fixed_cost(15, 0)
1069 .months(Month::November, Month::March)
1070 .hours(6, 22)
1071 .exclude_weekends()
1072 .exclude_holidays(Country::SE)
1073 .build();
1074
1075 let perfect = Stockholm.with_ymd_and_hms(2025, 1, 15, 10, 0, 0).unwrap();
1077 assert!(period.matches(perfect));
1078
1079 let first_hour = Stockholm.with_ymd_and_hms(2025, 1, 15, 6, 0, 0).unwrap();
1081 assert!(period.matches(first_hour));
1082
1083 let last_hour = Stockholm.with_ymd_and_hms(2025, 1, 15, 22, 59, 59).unwrap();
1085 assert!(period.matches(last_hour));
1086
1087 let too_early = Stockholm.with_ymd_and_hms(2025, 1, 15, 5, 59, 59).unwrap();
1089 assert!(!period.matches(too_early));
1090
1091 let too_late = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
1093 assert!(!period.matches(too_late));
1094
1095 let summer = Stockholm.with_ymd_and_hms(2025, 7, 15, 10, 0, 0).unwrap();
1097 assert!(!period.matches(summer));
1098
1099 let weekend = Stockholm.with_ymd_and_hms(2025, 1, 4, 10, 0, 0).unwrap();
1101 assert!(!period.matches(weekend));
1102 }
1103
1104 #[test]
1105 fn cost_period_matches_base_with_restrictions() {
1106 let period = CostPeriod::builder()
1108 .load(LoadType::Base)
1109 .fixed_cost(3, 0)
1110 .hours(0, 5)
1111 .build();
1112
1113 let match_night = Stockholm.with_ymd_and_hms(2025, 1, 15, 3, 0, 0).unwrap();
1115 assert!(period.matches(match_night));
1116
1117 let no_match_day = Stockholm.with_ymd_and_hms(2025, 1, 15, 14, 0, 0).unwrap();
1119 assert!(!period.matches(no_match_day));
1120 }
1121
1122 #[test]
1123 fn cost_period_matches_single_month() {
1124 let period = CostPeriod::builder()
1125 .load(LoadType::High)
1126 .fixed_cost(10, 0)
1127 .month(Month::December)
1128 .build();
1129
1130 let dec_first = Stockholm.with_ymd_and_hms(2025, 12, 1, 0, 0, 0).unwrap();
1132 assert!(period.matches(dec_first));
1133
1134 let dec_last = Stockholm
1136 .with_ymd_and_hms(2025, 12, 31, 23, 59, 59)
1137 .unwrap();
1138 assert!(period.matches(dec_last));
1139
1140 let nov = Stockholm.with_ymd_and_hms(2025, 11, 30, 12, 0, 0).unwrap();
1142 assert!(!period.matches(nov));
1143
1144 let jan = Stockholm.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
1146 assert!(!period.matches(jan));
1147 }
1148
1149 #[test]
1150 fn cost_period_matches_all_hours() {
1151 let period = CostPeriod::builder()
1153 .load(LoadType::Low)
1154 .fixed_cost(5, 0)
1155 .hours(0, 23)
1156 .build();
1157
1158 let midnight = Stockholm.with_ymd_and_hms(2025, 1, 15, 0, 0, 0).unwrap();
1159 let noon = Stockholm.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap();
1160 let almost_midnight = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 59, 59).unwrap();
1161
1162 assert!(period.matches(midnight));
1163 assert!(period.matches(noon));
1164 assert!(period.matches(almost_midnight));
1165 }
1166
1167 #[test]
1168 fn cost_period_matches_edge_of_month_range() {
1169 let period = CostPeriod::builder()
1171 .load(LoadType::Low)
1172 .fixed_cost(5, 0)
1173 .months(Month::May, Month::September)
1174 .build();
1175
1176 let may_start = Stockholm.with_ymd_and_hms(2025, 5, 1, 0, 0, 0).unwrap();
1178 assert!(period.matches(may_start));
1179
1180 let april_end = Stockholm.with_ymd_and_hms(2025, 4, 30, 23, 59, 59).unwrap();
1182 assert!(!period.matches(april_end));
1183
1184 let sept_end = Stockholm.with_ymd_and_hms(2025, 9, 30, 23, 59, 59).unwrap();
1186 assert!(period.matches(sept_end));
1187
1188 let oct_start = Stockholm.with_ymd_and_hms(2025, 10, 1, 0, 0, 0).unwrap();
1190 assert!(!period.matches(oct_start));
1191 }
1192
1193 #[test]
1194 fn include_matches_month_boundary() {
1195 let include = Include::Month(Month::February);
1197
1198 let feb_start = Stockholm.with_ymd_and_hms(2025, 2, 1, 0, 0, 0).unwrap();
1200 assert!(include.matches(feb_start));
1201
1202 let feb_end = Stockholm.with_ymd_and_hms(2025, 2, 28, 23, 59, 59).unwrap();
1204 assert!(include.matches(feb_end));
1205
1206 let jan_end = Stockholm.with_ymd_and_hms(2025, 1, 31, 23, 59, 59).unwrap();
1208 assert!(!include.matches(jan_end));
1209
1210 let mar_start = Stockholm.with_ymd_and_hms(2025, 3, 1, 0, 0, 0).unwrap();
1212 assert!(!include.matches(mar_start));
1213 }
1214
1215 #[test]
1216 fn include_matches_hours_exact_boundaries() {
1217 let include = Include::Hours(Hours::new(6, 22));
1218
1219 let start = Stockholm.with_ymd_and_hms(2025, 1, 15, 6, 0, 0).unwrap();
1221 assert!(include.matches(start));
1222
1223 let end = Stockholm.with_ymd_and_hms(2025, 1, 15, 22, 59, 59).unwrap();
1225 assert!(include.matches(end));
1226
1227 let before = Stockholm.with_ymd_and_hms(2025, 1, 15, 5, 59, 59).unwrap();
1229 assert!(!include.matches(before));
1230
1231 let after = Stockholm.with_ymd_and_hms(2025, 1, 15, 23, 0, 0).unwrap();
1233 assert!(!include.matches(after));
1234 }
1235}