1use std::slice::Iter;
2
3use chrono::{DateTime, Datelike};
4use serde::Serialize;
5
6use crate::{
7 Country, Language, LoadType, Money, Timezone, helpers,
8 hours::Hours,
9 months::{Month, Months},
10};
11
12#[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 fuses(values: &'static [(u16, Money)]) -> Self {
28 Self::Fuses(values)
29 }
30
31 pub const fn fuse_range(ranges: &'static [(u16, u16, Money)]) -> Self {
32 Self::FuseRange(ranges)
33 }
34
35 pub const fn fuses_with_yearly_consumption(
36 values: &'static [(u16, Option<u32>, Money)],
37 ) -> Self {
38 Self::FusesYearlyConsumption(values)
39 }
40
41 pub const fn fixed(int: i64, fract: u8) -> Self {
42 Self::Fixed(Money::new(int, fract))
43 }
44
45 pub const fn fixed_yearly(int: i64, fract: u8) -> Self {
46 Self::Fixed(Money::new(int, fract).divide_by(12))
47 }
48
49 pub const fn fixed_subunit(subunit: f64) -> Self {
50 Self::Fixed(Money::new_subunit(subunit))
51 }
52
53 pub const fn is_unverified(&self) -> bool {
54 matches!(self, Self::Unverified)
55 }
56
57 pub 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 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 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 const fn new_first(periods: &'static [CostPeriod]) -> Self {
155 Self {
156 match_method: CostPeriodMatching::First,
157 periods,
158 }
159 }
160
161 pub const fn new_all(periods: &'static [CostPeriod]) -> Self {
162 Self {
163 match_method: CostPeriodMatching::All,
164 periods,
165 }
166 }
167
168 pub fn match_method(&self) -> CostPeriodMatching {
169 self.match_method
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<Tz: chrono::TimeZone>(
183 &self,
184 timestamp: DateTime<Tz>,
185 ) -> Vec<&CostPeriod>
186 where
187 DateTime<Tz>: Copy,
188 {
189 let mut ret = vec![];
190 for period in self.periods {
191 if period.matches(timestamp) {
192 ret.push(period);
193 if self.match_method == CostPeriodMatching::First {
194 break;
195 }
196 }
197 }
198 ret
199 }
200}
201
202#[derive(Debug, Clone, Serialize)]
204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
205pub struct CostPeriodsSimple {
206 periods: Vec<CostPeriodSimple>,
207}
208
209impl CostPeriodsSimple {
210 pub(crate) fn new(
211 periods: CostPeriods,
212 fuse_size: u16,
213 yearly_consumption: u32,
214 language: Language,
215 ) -> Self {
216 Self {
217 periods: periods
218 .periods
219 .iter()
220 .flat_map(|period| {
221 CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
222 })
223 .collect(),
224 }
225 }
226}
227
228#[derive(Debug, Clone, Serialize)]
229#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
230pub struct CostPeriod {
231 cost: Cost,
232 load: LoadType,
233 #[serde(serialize_with = "helpers::skip_nones")]
234 include: [Option<Include>; 2],
235 #[serde(serialize_with = "helpers::skip_nones")]
236 exclude: [Option<Exclude>; 2],
237 divide_kw_by: u8,
239}
240
241#[derive(Debug, Clone, Serialize)]
243#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
244pub(crate) struct CostPeriodSimple {
245 cost: Money,
246 load: LoadType,
247 include: Vec<Include>,
248 exclude: Vec<Exclude>,
249 divide_kw_by: u8,
251 info: String,
252}
253
254impl CostPeriodSimple {
255 fn new(
256 period: &CostPeriod,
257 fuse_size: u16,
258 yearly_consumption: u32,
259 language: Language,
260 ) -> Option<Self> {
261 let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
262 Some(
263 Self {
264 cost,
265 load: period.load,
266 include: period.include.into_iter().flatten().collect(),
267 exclude: period.exclude.into_iter().flatten().collect(),
268 divide_kw_by: period.divide_kw_by,
269 info: Default::default(),
270 }
271 .add_info(language),
272 )
273 }
274
275 fn add_info(mut self, language: Language) -> Self {
276 let mut infos = Vec::new();
277 for include in &self.include {
278 infos.push(include.translate(language));
279 }
280 for exclude in &self.exclude {
281 infos.push(exclude.translate(language).into());
282 }
283 self.info = infos.join(", ");
284 self
285 }
286}
287
288impl CostPeriod {
289 pub const fn builder() -> CostPeriodBuilder {
290 CostPeriodBuilder::new()
291 }
292
293 pub const fn cost(&self) -> Cost {
294 self.cost
295 }
296
297 pub const fn load(&self) -> LoadType {
298 self.load
299 }
300
301 pub const fn power_multiplier(&self) -> f64 {
302 1. / self.divide_kw_by as f64
303 }
304
305 pub fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool
306 where
307 DateTime<Tz>: Copy,
308 {
309 for include in self.include_period_types() {
310 if !include.matches(timestamp) {
311 return false;
312 }
313 }
314
315 for exclude in self.exclude_period_types() {
316 if exclude.matches(timestamp) {
317 return false;
318 }
319 }
320 true
321 }
322
323 fn include_period_types(&self) -> Vec<Include> {
324 self.include.iter().flatten().copied().collect()
325 }
326
327 fn exclude_period_types(&self) -> Vec<Exclude> {
328 self.exclude.iter().flatten().copied().collect()
329 }
330
331 fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
332 self.cost.is_yearly_consumption_based(fuse_size)
333 }
334}
335
336#[derive(Clone)]
337pub struct CostPeriodBuilder {
338 timezone: Option<Timezone>,
339 cost: Cost,
340 load: Option<LoadType>,
341 include: [Option<Include>; 2],
342 exclude: [Option<Exclude>; 2],
343 divide_kw_by: u8,
345}
346
347impl CostPeriodBuilder {
348 pub const fn new() -> Self {
349 let builder = Self {
350 timezone: None,
351 cost: Cost::None,
352 load: None,
353 include: [None; 2],
354 exclude: [None; 2],
355 divide_kw_by: 1,
356 };
357 builder.timezone(Timezone::Stockholm)
359 }
360
361 pub 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 const fn cost(mut self, cost: Cost) -> Self {
372 self.cost = cost;
373 self
374 }
375
376 pub const fn load(mut self, load: LoadType) -> Self {
377 self.load = Some(load);
378 self
379 }
380
381 pub const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
382 self.cost = Cost::fixed(int, fract);
383 self
384 }
385
386 pub const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
387 self.cost = Cost::fixed_subunit(subunit);
388 self
389 }
390
391 pub const fn timezone(mut self, timezone: Timezone) -> Self {
392 self.timezone = Some(timezone);
393 self
394 }
395
396 const fn get_timezone(&self) -> Timezone {
397 self.timezone.expect("`timezone` must be specified")
398 }
399
400 pub const fn include(mut self, period_type: Include) -> Self {
401 let mut i = 0;
402 while i < self.include.len() {
403 if self.include[i].is_some() {
404 i += 1;
405 } else {
406 self.include[i] = Some(period_type);
407 return self;
408 }
409 }
410 panic!("Too many includes");
411 }
412
413 pub const fn months(self, from: Month, to: Month) -> Self {
414 let timezone = self.get_timezone();
415 self.include(Include::Months(Months::new(from, to, timezone)))
416 }
417
418 pub const fn month(self, month: Month) -> Self {
419 self.months(month, month)
420 }
421
422 pub const fn hours(self, from: u8, to_inclusive: u8) -> Self {
423 let timezone = self.get_timezone();
424 self.include(Include::Hours(Hours::new(from, to_inclusive, timezone)))
425 }
426
427 const fn exclude(mut self, period_type: Exclude) -> Self {
428 let mut i = 0;
429 while i < self.exclude.len() {
430 if self.exclude[i].is_some() {
431 i += 1;
432 } else {
433 self.exclude[i] = Some(period_type);
434 return self;
435 }
436 }
437 panic!("Too many excludes");
438 }
439
440 pub const fn exclude_holidays(self, country: Country) -> Self {
441 let tz = self.get_timezone();
442 self.exclude(Exclude::Holidays(country, tz))
443 }
444
445 pub const fn exclude_weekends(self) -> Self {
446 let tz = self.get_timezone();
447 self.exclude(Exclude::Weekends(tz))
448 }
449}
450
451#[derive(Debug, Clone, Copy, Serialize)]
452#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
453pub enum Include {
454 Months(Months),
455 Hours(Hours),
456}
457
458impl Include {
459 fn translate(&self, language: Language) -> String {
460 match self {
461 Include::Months(months) => months.translate(language),
462 Include::Hours(hours) => hours.translate(language),
463 }
464 }
465
466 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
467 match self {
468 Include::Months(months) => months.matches(timestamp),
469 Include::Hours(hours) => hours.matches(timestamp),
470 }
471 }
472}
473
474#[derive(Debug, Clone, Copy, Serialize)]
475#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
476pub enum Exclude {
477 Weekends(Timezone),
478 Holidays(Country, Timezone),
479}
480
481impl Exclude {
482 pub(crate) fn translate(&self, language: Language) -> &'static str {
483 match language {
484 Language::En => match self {
485 Exclude::Weekends(_) => "Weekends",
486 Exclude::Holidays(country, _) => match country {
487 Country::SE => "Swedish holidays",
488 },
489 },
490 Language::Sv => match self {
491 Exclude::Weekends(_) => "Helg",
492 Exclude::Holidays(country, _) => match country {
493 Country::SE => "Svenska helgdagar",
494 },
495 },
496 }
497 }
498
499 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
500 let tz_timestamp = timestamp.with_timezone(&self.tz());
501 match self {
502 Exclude::Weekends(_) => (6..=7).contains(&tz_timestamp.weekday().number_from_monday()),
503 Exclude::Holidays(country, _) => country.is_holiday(tz_timestamp.date_naive()),
504 }
505 }
506
507 const fn tz(&self) -> chrono_tz::Tz {
508 match self {
509 Exclude::Weekends(timezone) => timezone.to_tz(),
510 Exclude::Holidays(_, timezone) => timezone.to_tz(),
511 }
512 }
513}
514
515#[cfg(test)]
516mod tests {
517
518 use super::*;
519 use crate::money::Money;
520 use crate::months::Month::*;
521 use crate::{Stockholm, Utc};
522
523 #[test]
524 fn cost_for_none() {
525 const NONE_COST: Cost = Cost::None;
526 assert_eq!(NONE_COST.cost_for(16, 0), None);
527 assert_eq!(NONE_COST.cost_for(25, 5000), None);
528 }
529
530 #[test]
531 fn cost_for_unverified() {
532 const UNVERIFIED_COST: Cost = Cost::Unverified;
533 assert_eq!(UNVERIFIED_COST.cost_for(16, 0), None);
534 assert_eq!(UNVERIFIED_COST.cost_for(25, 5000), None);
535 }
536
537 #[test]
538 fn cost_for_fixed() {
539 const FIXED_COST: Cost = Cost::Fixed(Money::new(100, 50));
540 assert_eq!(FIXED_COST.cost_for(16, 0), Some(Money::new(100, 50)));
542 assert_eq!(FIXED_COST.cost_for(25, 5000), Some(Money::new(100, 50)));
543 assert_eq!(FIXED_COST.cost_for(63, 10000), Some(Money::new(100, 50)));
544 }
545
546 #[test]
547 fn cost_for_fuses_exact_match() {
548 const FUSES_COST: Cost = Cost::fuses(&[
549 (16, Money::new(50, 0)),
550 (25, Money::new(75, 0)),
551 (35, Money::new(100, 0)),
552 (50, Money::new(150, 0)),
553 ]);
554
555 assert_eq!(FUSES_COST.cost_for(16, 0), Some(Money::new(50, 0)));
557 assert_eq!(FUSES_COST.cost_for(25, 0), Some(Money::new(75, 0)));
558 assert_eq!(FUSES_COST.cost_for(35, 0), Some(Money::new(100, 0)));
559 assert_eq!(FUSES_COST.cost_for(50, 0), Some(Money::new(150, 0)));
560
561 assert_eq!(FUSES_COST.cost_for(25, 500000), Some(Money::new(75, 0)));
563 }
564
565 #[test]
566 fn cost_for_fuses_no_match() {
567 const FUSES_COST: Cost = Cost::fuses(&[(16, Money::new(50, 0)), (25, Money::new(75, 0))]);
568
569 assert_eq!(FUSES_COST.cost_for(20, 0), None);
571 assert_eq!(FUSES_COST.cost_for(63, 0), None);
572 }
573
574 #[test]
575 fn cost_for_fuses_yearly_consumption_with_limit() {
576 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
577 (16, Some(5000), Money::new(50, 0)),
578 (16, None, Money::new(75, 0)),
579 (25, Some(10000), Money::new(100, 0)),
580 (25, None, Money::new(125, 0)),
581 ]);
582
583 assert_eq!(
585 FUSES_WITH_CONSUMPTION.cost_for(16, 3000),
586 Some(Money::new(50, 0))
587 );
588
589 assert_eq!(
591 FUSES_WITH_CONSUMPTION.cost_for(16, 5000),
592 Some(Money::new(50, 0))
593 );
594
595 assert_eq!(
597 FUSES_WITH_CONSUMPTION.cost_for(16, 6000),
598 Some(Money::new(75, 0))
599 );
600
601 assert_eq!(
603 FUSES_WITH_CONSUMPTION.cost_for(16, 20000),
604 Some(Money::new(75, 0))
605 );
606
607 assert_eq!(
609 FUSES_WITH_CONSUMPTION.cost_for(25, 10000),
610 Some(Money::new(100, 0))
611 );
612
613 assert_eq!(
615 FUSES_WITH_CONSUMPTION.cost_for(25, 15000),
616 Some(Money::new(125, 0))
617 );
618
619 assert_eq!(
621 FUSES_WITH_CONSUMPTION.cost_for(25, 5000),
622 Some(Money::new(100, 0))
623 );
624 }
625
626 #[test]
627 fn cost_for_fuses_yearly_consumption_no_limit() {
628 const FUSES_NO_LIMIT: Cost = Cost::fuses_with_yearly_consumption(&[
629 (16, None, Money::new(50, 0)),
630 (25, None, Money::new(75, 0)),
631 ]);
632
633 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 0), Some(Money::new(50, 0)));
635 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 1000), Some(Money::new(50, 0)));
636 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 50000), Some(Money::new(50, 0)));
637 assert_eq!(FUSES_NO_LIMIT.cost_for(25, 100000), Some(Money::new(75, 0)));
638 }
639
640 #[test]
641 fn cost_for_fuses_yearly_consumption_no_fuse_match() {
642 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
643 (16, Some(5000), Money::new(50, 0)),
644 (25, Some(10000), Money::new(100, 0)),
645 ]);
646
647 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(35, 5000), None);
649 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(50, 10000), None);
650 }
651
652 #[test]
653 fn cost_for_fuses_yearly_consumption_max_limit_no_fallback() {
654 const FUSES_ONLY_LIMITS: Cost = Cost::fuses_with_yearly_consumption(&[
655 (16, Some(5000), Money::new(50, 0)),
656 (25, Some(10000), Money::new(100, 0)),
657 ]);
658
659 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 0), Some(Money::new(50, 0)));
661 assert_eq!(
662 FUSES_ONLY_LIMITS.cost_for(16, 3000),
663 Some(Money::new(50, 0))
664 );
665 assert_eq!(
666 FUSES_ONLY_LIMITS.cost_for(16, 4999),
667 Some(Money::new(50, 0))
668 );
669 assert_eq!(
670 FUSES_ONLY_LIMITS.cost_for(16, 5000),
671 Some(Money::new(50, 0))
672 );
673 assert_eq!(
674 FUSES_ONLY_LIMITS.cost_for(25, 9999),
675 Some(Money::new(100, 0))
676 );
677 assert_eq!(
678 FUSES_ONLY_LIMITS.cost_for(25, 10000),
679 Some(Money::new(100, 0))
680 );
681
682 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 5001), None);
684 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 10000), None);
685 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 10001), None);
686 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 20000), None);
687 }
688
689 #[test]
690 fn cost_for_fuse_range_within_range() {
691 const FUSE_BASED: Cost = Cost::fuse_range(&[
692 (16, 35, Money::new(54, 0)),
693 (35, u16::MAX, Money::new(108, 50)),
694 ]);
695
696 assert_eq!(FUSE_BASED.cost_for(10, 0), None);
698 assert_eq!(FUSE_BASED.cost_for(15, 0), None);
699
700 assert_eq!(FUSE_BASED.cost_for(16, 0), Some(Money::new(54, 0)));
702 assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
703 assert_eq!(FUSE_BASED.cost_for(35, 0), Some(Money::new(54, 0)));
704
705 assert_eq!(FUSE_BASED.cost_for(36, 0), Some(Money::new(108, 50)));
707 assert_eq!(FUSE_BASED.cost_for(50, 0), Some(Money::new(108, 50)));
708 assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
709 assert_eq!(FUSE_BASED.cost_for(u16::MAX, 0), Some(Money::new(108, 50)));
710 }
711
712 #[test]
713 fn cost_for_fuse_range_multiple_ranges() {
714 const MULTI_RANGE: Cost = Cost::fuse_range(&[
715 (1, 15, Money::new(20, 0)),
716 (16, 35, Money::new(50, 0)),
717 (36, 63, Money::new(100, 0)),
718 (64, u16::MAX, Money::new(200, 0)),
719 ]);
720
721 assert_eq!(MULTI_RANGE.cost_for(10, 0), Some(Money::new(20, 0)));
723 assert_eq!(MULTI_RANGE.cost_for(25, 0), Some(Money::new(50, 0)));
724 assert_eq!(MULTI_RANGE.cost_for(50, 0), Some(Money::new(100, 0)));
725 assert_eq!(MULTI_RANGE.cost_for(100, 0), Some(Money::new(200, 0)));
726
727 assert_eq!(MULTI_RANGE.cost_for(25, 10000), Some(Money::new(50, 0)));
729 }
730
731 #[test]
732 fn include_matches_hours() {
733 let include = Include::Hours(Hours::new(6, 22, Stockholm));
734 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
735 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
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(November, March, Stockholm));
744 let timestamp_match = Stockholm.dt(2025, 1, 15, 12, 0, 0);
745 let timestamp_no_match = Stockholm.dt(2025, 7, 15, 12, 0, 0);
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(Stockholm);
754 let timestamp = Stockholm.dt(2025, 1, 4, 12, 0, 0);
756 assert!(exclude.matches(timestamp));
757 }
758
759 #[test]
760 fn exclude_matches_weekends_sunday() {
761 let exclude = Exclude::Weekends(Stockholm);
762 let timestamp = Stockholm.dt(2025, 1, 5, 12, 0, 0);
764 assert!(exclude.matches(timestamp));
765 }
766
767 #[test]
768 fn exclude_does_not_match_weekday() {
769 let exclude = Exclude::Weekends(Stockholm);
770 let timestamp = Stockholm.dt(2025, 1, 6, 12, 0, 0);
772 assert!(!exclude.matches(timestamp));
773 }
774
775 #[test]
776 fn exclude_matches_swedish_new_year() {
777 let exclude = Exclude::Holidays(Country::SE, Stockholm);
778 let timestamp = Stockholm.dt(2025, 1, 1, 12, 0, 0);
780 assert!(exclude.matches(timestamp));
781 }
782
783 #[test]
784 fn exclude_does_not_match_non_holiday() {
785 let exclude = Exclude::Holidays(Country::SE, Stockholm);
786 let timestamp = Stockholm.dt(2025, 1, 2, 12, 0, 0);
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.dt(2025, 1, 15, 14, 0, 0);
800 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
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(November, March)
813 .build();
814
815 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
817 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
819 let timestamp_wrong_months = Stockholm.dt(2025, 7, 15, 14, 0, 0);
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.dt(2025, 1, 6, 14, 0, 0);
841 let timestamp_saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
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.dt(2025, 1, 2, 14, 0, 0);
859 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
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(November, March)
873 .hours(6, 22)
874 .exclude_weekends()
875 .exclude_holidays(Country::SE)
876 .build();
877
878 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
880
881 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
883
884 let timestamp_weekend = Stockholm.dt(2025, 1, 4, 14, 0, 0);
886
887 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
889
890 let timestamp_summer = Stockholm.dt(2025, 7, 15, 14, 0, 0);
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.dt(2025, 1, 1, 0, 0, 0);
910 let timestamp2 = Stockholm.dt(2025, 7, 15, 23, 59, 59);
911 let timestamp3 = Stockholm.dt(2025, 1, 4, 12, 0, 0);
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, Stockholm));
922
923 let timestamp_evening = Stockholm.dt(2025, 1, 15, 22, 0, 0);
925 assert!(include.matches(timestamp_evening));
926
927 let timestamp_midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
929 assert!(include.matches(timestamp_midnight));
930
931 let timestamp_morning = Stockholm.dt(2025, 1, 15, 5, 30, 0);
933 assert!(include.matches(timestamp_morning));
934
935 let timestamp_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
937 assert!(!include.matches(timestamp_day));
938
939 let timestamp_after = Stockholm.dt(2025, 1, 15, 6, 0, 0);
941 assert!(!include.matches(timestamp_after));
942
943 let timestamp_before = Stockholm.dt(2025, 1, 15, 21, 59, 59);
945 assert!(!include.matches(timestamp_before));
946 }
947
948 #[test]
949 fn include_matches_months_wraparound() {
950 let include = Include::Months(Months::new(November, March, Stockholm));
952
953 let timestamp_nov = Stockholm.dt(2025, 11, 15, 12, 0, 0);
955 assert!(include.matches(timestamp_nov));
956
957 let timestamp_dec = Stockholm.dt(2025, 12, 15, 12, 0, 0);
959 assert!(include.matches(timestamp_dec));
960
961 let timestamp_jan = Stockholm.dt(2025, 1, 15, 12, 0, 0);
963 assert!(include.matches(timestamp_jan));
964
965 let timestamp_mar = Stockholm.dt(2025, 3, 15, 12, 0, 0);
967 assert!(include.matches(timestamp_mar));
968
969 let timestamp_jul = Stockholm.dt(2025, 7, 15, 12, 0, 0);
971 assert!(!include.matches(timestamp_jul));
972
973 let timestamp_oct = Stockholm.dt(2025, 10, 31, 23, 59, 59);
975 assert!(!include.matches(timestamp_oct));
976
977 let timestamp_apr = Stockholm.dt(2025, 4, 1, 0, 0, 0);
979 assert!(!include.matches(timestamp_apr));
980 }
981
982 #[test]
983 fn cost_period_matches_hours_wraparound() {
984 let period = CostPeriod::builder()
986 .load(LoadType::Low)
987 .fixed_cost(5, 0)
988 .hours(22, 5)
989 .build();
990
991 let timestamp_match_evening = Stockholm.dt(2025, 1, 15, 23, 0, 0);
992 let timestamp_match_morning = Stockholm.dt(2025, 1, 15, 3, 0, 0);
993 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
994
995 assert!(period.matches(timestamp_match_evening));
996 assert!(period.matches(timestamp_match_morning));
997 assert!(!period.matches(timestamp_no_match));
998 }
999
1000 #[test]
1001 fn cost_period_matches_with_both_excludes() {
1002 let period = CostPeriod::builder()
1003 .load(LoadType::High)
1004 .fixed_cost(10, 0)
1005 .hours(6, 22)
1006 .exclude_weekends()
1007 .exclude_holidays(Country::SE)
1008 .build();
1009
1010 let weekday = Stockholm.dt(2025, 1, 2, 14, 0, 0);
1012 assert!(period.matches(weekday));
1013
1014 let saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
1016 assert!(!period.matches(saturday));
1017
1018 let holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
1020 assert!(!period.matches(holiday));
1021
1022 let wrong_hours = Stockholm.dt(2025, 1, 2, 23, 0, 0);
1024 assert!(!period.matches(wrong_hours));
1025 }
1026
1027 #[test]
1028 fn exclude_matches_friday_is_not_weekend() {
1029 let exclude = Exclude::Weekends(Stockholm);
1030 let friday = Stockholm.dt(2025, 1, 3, 12, 0, 0);
1032 assert!(!exclude.matches(friday));
1033 }
1034
1035 #[test]
1036 fn exclude_matches_monday_is_not_weekend() {
1037 let exclude = Exclude::Weekends(Stockholm);
1038 let monday = Stockholm.dt(2025, 1, 6, 12, 0, 0);
1040 assert!(!exclude.matches(monday));
1041 }
1042
1043 #[test]
1044 fn exclude_matches_holiday_midsummer() {
1045 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1046 let midsummer = Stockholm.dt(2025, 6, 21, 12, 0, 0);
1048 assert!(exclude.matches(midsummer));
1049 }
1050
1051 #[test]
1052 fn cost_period_matches_month_and_hours() {
1053 let period = CostPeriod::builder()
1055 .load(LoadType::Low)
1056 .fixed_cost(5, 0)
1057 .month(June)
1058 .hours(22, 5)
1059 .build();
1060
1061 let match_june_night = Stockholm.dt(2025, 6, 15, 23, 0, 0);
1063 assert!(period.matches(match_june_night));
1064
1065 let june_day = Stockholm.dt(2025, 6, 15, 14, 0, 0);
1067 assert!(!period.matches(june_day));
1068
1069 let july_night = Stockholm.dt(2025, 7, 15, 23, 0, 0);
1071 assert!(!period.matches(july_night));
1072 }
1073
1074 #[test]
1075 fn cost_period_matches_months_and_hours_with_exclude() {
1076 let period = CostPeriod::builder()
1078 .load(LoadType::High)
1079 .fixed_cost(15, 0)
1080 .months(November, March)
1081 .hours(6, 22)
1082 .exclude_weekends()
1083 .exclude_holidays(Country::SE)
1084 .build();
1085
1086 let perfect = Stockholm.dt(2025, 1, 15, 10, 0, 0);
1088 assert!(period.matches(perfect));
1089
1090 let first_hour = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1092 assert!(period.matches(first_hour));
1093
1094 let last_hour = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1096 assert!(period.matches(last_hour));
1097
1098 let too_early = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1100 assert!(!period.matches(too_early));
1101
1102 let too_late = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1104 assert!(!period.matches(too_late));
1105
1106 let summer = Stockholm.dt(2025, 7, 15, 10, 0, 0);
1108 assert!(!period.matches(summer));
1109
1110 let weekend = Stockholm.dt(2025, 1, 4, 10, 0, 0);
1112 assert!(!period.matches(weekend));
1113 }
1114
1115 #[test]
1116 fn cost_period_matches_base_with_restrictions() {
1117 let period = CostPeriod::builder()
1119 .load(LoadType::Base)
1120 .fixed_cost(3, 0)
1121 .hours(0, 5)
1122 .build();
1123
1124 let match_night = Stockholm.dt(2025, 1, 15, 3, 0, 0);
1126 assert!(period.matches(match_night));
1127
1128 let no_match_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
1130 assert!(!period.matches(no_match_day));
1131 }
1132
1133 #[test]
1134 fn cost_period_matches_single_month() {
1135 let period = CostPeriod::builder()
1136 .load(LoadType::High)
1137 .fixed_cost(10, 0)
1138 .month(December)
1139 .build();
1140
1141 let dec_first = Stockholm.dt(2025, 12, 1, 0, 0, 0);
1143 assert!(period.matches(dec_first));
1144
1145 let dec_last = Stockholm.dt(2025, 12, 31, 23, 59, 59);
1147 assert!(period.matches(dec_last));
1148
1149 let nov = Stockholm.dt(2025, 11, 30, 12, 0, 0);
1151 assert!(!period.matches(nov));
1152
1153 let jan = Stockholm.dt(2025, 1, 1, 12, 0, 0);
1155 assert!(!period.matches(jan));
1156 }
1157
1158 #[test]
1159 fn cost_period_matches_all_hours() {
1160 let period = CostPeriod::builder()
1162 .load(LoadType::Low)
1163 .fixed_cost(5, 0)
1164 .hours(0, 23)
1165 .build();
1166
1167 let midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
1168 let noon = Stockholm.dt(2025, 1, 15, 12, 0, 0);
1169 let almost_midnight = Stockholm.dt(2025, 1, 15, 23, 59, 59);
1170
1171 assert!(period.matches(midnight));
1172 assert!(period.matches(noon));
1173 assert!(period.matches(almost_midnight));
1174 }
1175
1176 #[test]
1177 fn cost_period_matches_edge_of_month_range() {
1178 let period = CostPeriod::builder()
1180 .load(LoadType::Low)
1181 .fixed_cost(5, 0)
1182 .months(May, September)
1183 .build();
1184
1185 let may_start = Stockholm.dt(2025, 5, 1, 0, 0, 0);
1187 assert!(period.matches(may_start));
1188
1189 let april_end = Stockholm.dt(2025, 4, 30, 23, 59, 59);
1191 assert!(!period.matches(april_end));
1192
1193 let sept_end = Stockholm.dt(2025, 9, 30, 23, 59, 59);
1195 assert!(period.matches(sept_end));
1196
1197 let oct_start = Stockholm.dt(2025, 10, 1, 0, 0, 0);
1199 assert!(!period.matches(oct_start));
1200 }
1201
1202 #[test]
1203 fn include_matches_month_boundary() {
1204 let include = Include::Months(Months::new(February, February, Stockholm));
1206
1207 let feb_start = Stockholm.dt(2025, 2, 1, 0, 0, 0);
1209 assert!(include.matches(feb_start));
1210
1211 let feb_end = Stockholm.dt(2025, 2, 28, 23, 59, 59);
1213 assert!(include.matches(feb_end));
1214
1215 let jan_end = Stockholm.dt(2025, 1, 31, 23, 59, 59);
1217 assert!(!include.matches(jan_end));
1218
1219 let mar_start = Stockholm.dt(2025, 3, 1, 0, 0, 0);
1221 assert!(!include.matches(mar_start));
1222 }
1223
1224 #[test]
1225 fn include_matches_hours_exact_boundaries() {
1226 let include = Include::Hours(Hours::new(6, 22, Stockholm));
1227
1228 let start = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1230 assert!(include.matches(start));
1231
1232 let end = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1234 assert!(include.matches(end));
1235
1236 let before = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1238 assert!(!include.matches(before));
1239
1240 let after = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1242 assert!(!include.matches(after));
1243 }
1244
1245 #[test]
1246 fn exclude_matches_weekends_with_utc_timestamps() {
1247 let exclude = Exclude::Weekends(Stockholm);
1248
1249 let saturday_utc = Utc.dt(2025, 1, 4, 11, 0, 0);
1252 assert!(exclude.matches(saturday_utc));
1253
1254 let sunday_utc = Utc.dt(2025, 1, 5, 11, 0, 0);
1257 assert!(exclude.matches(sunday_utc));
1258
1259 let monday_utc = Utc.dt(2025, 1, 6, 11, 0, 0);
1262 assert!(!exclude.matches(monday_utc));
1263 }
1264
1265 #[test]
1266 fn exclude_matches_weekends_timezone_boundary() {
1267 let exclude = Exclude::Weekends(Stockholm);
1268
1269 let friday_utc_saturday_stockholm = Utc.dt(2025, 1, 3, 23, 0, 0);
1273 assert!(
1274 exclude.matches(friday_utc_saturday_stockholm),
1275 "Should match because it's Saturday in Stockholm timezone"
1276 );
1277
1278 let sunday_utc_monday_stockholm = Utc.dt(2025, 1, 5, 23, 0, 0);
1282 assert!(
1283 !exclude.matches(sunday_utc_monday_stockholm),
1284 "Should not match because it's Monday in Stockholm timezone"
1285 );
1286
1287 let sunday_late_utc = Utc.dt(2025, 1, 5, 22, 59, 0);
1290 assert!(
1291 exclude.matches(sunday_late_utc),
1292 "Should match because it's still Sunday in Stockholm timezone"
1293 );
1294 }
1295
1296 #[test]
1297 fn exclude_matches_holidays_with_utc_timestamps() {
1298 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1299
1300 let new_year_utc = Utc.dt(2025, 1, 1, 11, 0, 0);
1303 assert!(exclude.matches(new_year_utc));
1304
1305 let regular_day_utc = Utc.dt(2025, 1, 2, 11, 0, 0);
1308 assert!(!exclude.matches(regular_day_utc));
1309 }
1310
1311 #[test]
1312 fn exclude_matches_holidays_timezone_boundary() {
1313 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1314
1315 let dec31_utc_jan1_stockholm = Utc.dt(2024, 12, 31, 23, 0, 0);
1319 assert!(
1320 exclude.matches(dec31_utc_jan1_stockholm),
1321 "Should match because it's New Year's Day in Stockholm timezone"
1322 );
1323
1324 let jan1_utc_jan2_stockholm = Utc.dt(2025, 1, 1, 23, 0, 0);
1328 assert!(
1329 !exclude.matches(jan1_utc_jan2_stockholm),
1330 "Should not match because it's January 2 in Stockholm timezone"
1331 );
1332 }
1333
1334 #[test]
1335 fn exclude_matches_weekends_summer_timezone() {
1336 let exclude = Exclude::Weekends(Stockholm);
1337
1338 let saturday_summer_utc = Utc.dt(2025, 6, 7, 10, 0, 0);
1341 assert!(exclude.matches(saturday_summer_utc));
1342
1343 let friday_utc_saturday_stockholm_summer = Utc.dt(2025, 6, 6, 22, 0, 0);
1346 assert!(
1347 exclude.matches(friday_utc_saturday_stockholm_summer),
1348 "Should match because it's Saturday in Stockholm timezone (CEST)"
1349 );
1350 }
1351}