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)]
141#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
142pub enum CostPeriodMatching {
143 First,
145 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 const fn new_first(periods: &'static [CostPeriod]) -> Self {
158 Self {
159 match_method: CostPeriodMatching::First,
160 periods,
161 }
162 }
163
164 pub const fn new_all(periods: &'static [CostPeriod]) -> Self {
165 Self {
166 match_method: CostPeriodMatching::All,
167 periods,
168 }
169 }
170
171 pub const fn match_method(&self) -> CostPeriodMatching {
172 self.match_method
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<Tz: chrono::TimeZone>(
186 &self,
187 timestamp: DateTime<Tz>,
188 ) -> Vec<&CostPeriod>
189 where
190 DateTime<Tz>: Copy,
191 {
192 let mut ret = vec![];
193 for period in self.periods {
194 if period.matches(timestamp) {
195 ret.push(period);
196 if self.match_method == CostPeriodMatching::First {
197 break;
198 }
199 }
200 }
201 ret
202 }
203}
204
205#[derive(Debug, Clone, Serialize)]
207#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
208pub struct CostPeriodsSimple {
209 periods: Vec<CostPeriodSimple>,
210}
211
212impl CostPeriodsSimple {
213 pub(crate) fn new(
214 periods: CostPeriods,
215 fuse_size: u16,
216 yearly_consumption: u32,
217 language: Language,
218 ) -> Self {
219 Self {
220 periods: periods
221 .periods
222 .iter()
223 .flat_map(|period| {
224 CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
225 })
226 .collect(),
227 }
228 }
229}
230
231#[derive(Debug, Clone, Serialize)]
232#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
233pub struct CostPeriod {
234 cost: Cost,
235 load: LoadType,
236 #[serde(serialize_with = "helpers::skip_nones")]
237 include: [Option<Include>; 2],
238 #[serde(serialize_with = "helpers::skip_nones")]
239 exclude: [Option<Exclude>; 2],
240}
241
242#[derive(Debug, Clone, Serialize)]
244#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
245pub(crate) struct CostPeriodSimple {
246 cost: Money,
247 load: LoadType,
248 include: Vec<Include>,
249 exclude: Vec<Exclude>,
250 info: String,
251}
252
253impl CostPeriodSimple {
254 fn new(
255 period: &CostPeriod,
256 fuse_size: u16,
257 yearly_consumption: u32,
258 language: Language,
259 ) -> Option<Self> {
260 let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
261 Some(
262 Self {
263 cost,
264 load: period.load,
265 include: period.include.into_iter().flatten().collect(),
266 exclude: period.exclude.into_iter().flatten().collect(),
267 info: Default::default(),
268 }
269 .add_info(language),
270 )
271 }
272
273 fn add_info(mut self, language: Language) -> Self {
274 let mut infos = Vec::new();
275 for include in &self.include {
276 infos.push(include.translate(language));
277 }
278 for exclude in &self.exclude {
279 infos.push(exclude.translate(language).into());
280 }
281 self.info = infos.join(", ");
282 self
283 }
284}
285
286impl CostPeriod {
287 pub const fn builder() -> CostPeriodBuilder {
288 CostPeriodBuilder::new()
289 }
290
291 pub const fn cost(&self) -> Cost {
292 self.cost
293 }
294
295 pub const fn load(&self) -> LoadType {
296 self.load
297 }
298
299 pub fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool
300 where
301 DateTime<Tz>: Copy,
302 {
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(Clone)]
331pub struct CostPeriodBuilder {
332 timezone: Option<Timezone>,
333 cost: Cost,
334 load: Option<LoadType>,
335 include: [Option<Include>; 2],
336 exclude: [Option<Exclude>; 2],
337}
338
339impl Default for CostPeriodBuilder {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl CostPeriodBuilder {
346 pub const fn new() -> Self {
347 let builder = Self {
348 timezone: None,
349 cost: Cost::None,
350 load: None,
351 include: [None; 2],
352 exclude: [None; 2],
353 };
354 builder.timezone(Timezone::Stockholm)
356 }
357
358 pub const fn build(self) -> CostPeriod {
359 CostPeriod {
360 cost: self.cost,
361 load: self.load.expect("`load` must be specified"),
362 include: self.include,
363 exclude: self.exclude,
364 }
365 }
366
367 pub const fn cost(mut self, cost: Cost) -> Self {
368 self.cost = cost;
369 self
370 }
371
372 pub const fn load(mut self, load: LoadType) -> Self {
373 self.load = Some(load);
374 self
375 }
376
377 pub const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
378 self.cost = Cost::fixed(int, fract);
379 self
380 }
381
382 pub const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
383 self.cost = Cost::fixed_subunit(subunit);
384 self
385 }
386
387 pub const fn timezone(mut self, timezone: Timezone) -> Self {
388 self.timezone = Some(timezone);
389 self
390 }
391
392 const fn get_timezone(&self) -> Timezone {
393 self.timezone.expect("`timezone` must be specified")
394 }
395
396 pub const fn include(mut self, period_type: Include) -> Self {
397 let mut i = 0;
398 while i < self.include.len() {
399 if self.include[i].is_some() {
400 i += 1;
401 } else {
402 self.include[i] = Some(period_type);
403 return self;
404 }
405 }
406 panic!("Too many includes");
407 }
408
409 pub const fn months(self, from: Month, to: Month) -> Self {
410 let timezone = self.get_timezone();
411 self.include(Include::Months(Months::new(from, to, timezone)))
412 }
413
414 pub const fn month(self, month: Month) -> Self {
415 self.months(month, month)
416 }
417
418 pub const fn hours(self, from: u8, to_inclusive: u8) -> Self {
419 let timezone = self.get_timezone();
420 self.include(Include::Hours(Hours::new(from, to_inclusive, timezone)))
421 }
422
423 const fn exclude(mut self, period_type: Exclude) -> Self {
424 let mut i = 0;
425 while i < self.exclude.len() {
426 if self.exclude[i].is_some() {
427 i += 1;
428 } else {
429 self.exclude[i] = Some(period_type);
430 return self;
431 }
432 }
433 panic!("Too many excludes");
434 }
435
436 pub const fn exclude_holidays(self, country: Country) -> Self {
437 let tz = self.get_timezone();
438 self.exclude(Exclude::Holidays(country, tz))
439 }
440
441 pub const fn exclude_weekends(self) -> Self {
442 let tz = self.get_timezone();
443 self.exclude(Exclude::Weekends(tz))
444 }
445}
446
447#[derive(Debug, Clone, Copy, Serialize)]
448#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
449pub enum Include {
450 Months(Months),
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::Hours(hours) => hours.translate(language),
459 }
460 }
461
462 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
463 match self {
464 Include::Months(months) => months.matches(timestamp),
465 Include::Hours(hours) => hours.matches(timestamp),
466 }
467 }
468}
469
470#[derive(Debug, Clone, Copy, Serialize)]
471#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
472pub enum Exclude {
473 Weekends(Timezone),
474 Holidays(Country, Timezone),
475}
476
477impl Exclude {
478 pub(crate) fn translate(&self, language: Language) -> &'static str {
479 match language {
480 Language::En => match self {
481 Exclude::Weekends(_) => "Weekends",
482 Exclude::Holidays(country, _) => match country {
483 Country::SE => "Swedish holidays",
484 },
485 },
486 Language::Sv => match self {
487 Exclude::Weekends(_) => "Helg",
488 Exclude::Holidays(country, _) => match country {
489 Country::SE => "Svenska helgdagar",
490 },
491 },
492 }
493 }
494
495 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
496 let tz_timestamp = timestamp.with_timezone(&self.tz());
497 match self {
498 Exclude::Weekends(_) => (6..=7).contains(&tz_timestamp.weekday().number_from_monday()),
499 Exclude::Holidays(country, _) => country.is_holiday(tz_timestamp.date_naive()),
500 }
501 }
502
503 const fn tz(&self) -> chrono_tz::Tz {
504 match self {
505 Exclude::Weekends(timezone) => timezone.to_tz(),
506 Exclude::Holidays(_, timezone) => timezone.to_tz(),
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513
514 use super::*;
515 use crate::money::Money;
516 use crate::months::Month::*;
517 use crate::{Stockholm, Utc};
518
519 #[test]
520 fn cost_for_none() {
521 const NONE_COST: Cost = Cost::None;
522 assert_eq!(NONE_COST.cost_for(16, 0), None);
523 assert_eq!(NONE_COST.cost_for(25, 5000), None);
524 }
525
526 #[test]
527 fn cost_for_unverified() {
528 const UNVERIFIED_COST: Cost = Cost::Unverified;
529 assert_eq!(UNVERIFIED_COST.cost_for(16, 0), None);
530 assert_eq!(UNVERIFIED_COST.cost_for(25, 5000), None);
531 }
532
533 #[test]
534 fn cost_for_fixed() {
535 const FIXED_COST: Cost = Cost::Fixed(Money::new(100, 50));
536 assert_eq!(FIXED_COST.cost_for(16, 0), Some(Money::new(100, 50)));
538 assert_eq!(FIXED_COST.cost_for(25, 5000), Some(Money::new(100, 50)));
539 assert_eq!(FIXED_COST.cost_for(63, 10000), Some(Money::new(100, 50)));
540 }
541
542 #[test]
543 fn cost_for_fuses_exact_match() {
544 const FUSES_COST: Cost = Cost::fuses(&[
545 (16, Money::new(50, 0)),
546 (25, Money::new(75, 0)),
547 (35, Money::new(100, 0)),
548 (50, Money::new(150, 0)),
549 ]);
550
551 assert_eq!(FUSES_COST.cost_for(16, 0), Some(Money::new(50, 0)));
553 assert_eq!(FUSES_COST.cost_for(25, 0), Some(Money::new(75, 0)));
554 assert_eq!(FUSES_COST.cost_for(35, 0), Some(Money::new(100, 0)));
555 assert_eq!(FUSES_COST.cost_for(50, 0), Some(Money::new(150, 0)));
556
557 assert_eq!(FUSES_COST.cost_for(25, 500000), Some(Money::new(75, 0)));
559 }
560
561 #[test]
562 fn cost_for_fuses_no_match() {
563 const FUSES_COST: Cost = Cost::fuses(&[(16, Money::new(50, 0)), (25, Money::new(75, 0))]);
564
565 assert_eq!(FUSES_COST.cost_for(20, 0), None);
567 assert_eq!(FUSES_COST.cost_for(63, 0), None);
568 }
569
570 #[test]
571 fn cost_for_fuses_yearly_consumption_with_limit() {
572 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
573 (16, Some(5000), Money::new(50, 0)),
574 (16, None, Money::new(75, 0)),
575 (25, Some(10000), Money::new(100, 0)),
576 (25, None, Money::new(125, 0)),
577 ]);
578
579 assert_eq!(
581 FUSES_WITH_CONSUMPTION.cost_for(16, 3000),
582 Some(Money::new(50, 0))
583 );
584
585 assert_eq!(
587 FUSES_WITH_CONSUMPTION.cost_for(16, 5000),
588 Some(Money::new(50, 0))
589 );
590
591 assert_eq!(
593 FUSES_WITH_CONSUMPTION.cost_for(16, 6000),
594 Some(Money::new(75, 0))
595 );
596
597 assert_eq!(
599 FUSES_WITH_CONSUMPTION.cost_for(16, 20000),
600 Some(Money::new(75, 0))
601 );
602
603 assert_eq!(
605 FUSES_WITH_CONSUMPTION.cost_for(25, 10000),
606 Some(Money::new(100, 0))
607 );
608
609 assert_eq!(
611 FUSES_WITH_CONSUMPTION.cost_for(25, 15000),
612 Some(Money::new(125, 0))
613 );
614
615 assert_eq!(
617 FUSES_WITH_CONSUMPTION.cost_for(25, 5000),
618 Some(Money::new(100, 0))
619 );
620 }
621
622 #[test]
623 fn cost_for_fuses_yearly_consumption_no_limit() {
624 const FUSES_NO_LIMIT: Cost = Cost::fuses_with_yearly_consumption(&[
625 (16, None, Money::new(50, 0)),
626 (25, None, Money::new(75, 0)),
627 ]);
628
629 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 0), Some(Money::new(50, 0)));
631 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 1000), Some(Money::new(50, 0)));
632 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 50000), Some(Money::new(50, 0)));
633 assert_eq!(FUSES_NO_LIMIT.cost_for(25, 100000), Some(Money::new(75, 0)));
634 }
635
636 #[test]
637 fn cost_for_fuses_yearly_consumption_no_fuse_match() {
638 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
639 (16, Some(5000), Money::new(50, 0)),
640 (25, Some(10000), Money::new(100, 0)),
641 ]);
642
643 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(35, 5000), None);
645 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(50, 10000), None);
646 }
647
648 #[test]
649 fn cost_for_fuses_yearly_consumption_max_limit_no_fallback() {
650 const FUSES_ONLY_LIMITS: Cost = Cost::fuses_with_yearly_consumption(&[
651 (16, Some(5000), Money::new(50, 0)),
652 (25, Some(10000), Money::new(100, 0)),
653 ]);
654
655 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 0), Some(Money::new(50, 0)));
657 assert_eq!(
658 FUSES_ONLY_LIMITS.cost_for(16, 3000),
659 Some(Money::new(50, 0))
660 );
661 assert_eq!(
662 FUSES_ONLY_LIMITS.cost_for(16, 4999),
663 Some(Money::new(50, 0))
664 );
665 assert_eq!(
666 FUSES_ONLY_LIMITS.cost_for(16, 5000),
667 Some(Money::new(50, 0))
668 );
669 assert_eq!(
670 FUSES_ONLY_LIMITS.cost_for(25, 9999),
671 Some(Money::new(100, 0))
672 );
673 assert_eq!(
674 FUSES_ONLY_LIMITS.cost_for(25, 10000),
675 Some(Money::new(100, 0))
676 );
677
678 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 5001), None);
680 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 10000), None);
681 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 10001), None);
682 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 20000), None);
683 }
684
685 #[test]
686 fn cost_for_fuse_range_within_range() {
687 const FUSE_BASED: Cost = Cost::fuse_range(&[
688 (16, 35, Money::new(54, 0)),
689 (35, u16::MAX, Money::new(108, 50)),
690 ]);
691
692 assert_eq!(FUSE_BASED.cost_for(10, 0), None);
694 assert_eq!(FUSE_BASED.cost_for(15, 0), None);
695
696 assert_eq!(FUSE_BASED.cost_for(16, 0), Some(Money::new(54, 0)));
698 assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
699 assert_eq!(FUSE_BASED.cost_for(35, 0), Some(Money::new(54, 0)));
700
701 assert_eq!(FUSE_BASED.cost_for(36, 0), Some(Money::new(108, 50)));
703 assert_eq!(FUSE_BASED.cost_for(50, 0), Some(Money::new(108, 50)));
704 assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
705 assert_eq!(FUSE_BASED.cost_for(u16::MAX, 0), Some(Money::new(108, 50)));
706 }
707
708 #[test]
709 fn cost_for_fuse_range_multiple_ranges() {
710 const MULTI_RANGE: Cost = Cost::fuse_range(&[
711 (1, 15, Money::new(20, 0)),
712 (16, 35, Money::new(50, 0)),
713 (36, 63, Money::new(100, 0)),
714 (64, u16::MAX, Money::new(200, 0)),
715 ]);
716
717 assert_eq!(MULTI_RANGE.cost_for(10, 0), Some(Money::new(20, 0)));
719 assert_eq!(MULTI_RANGE.cost_for(25, 0), Some(Money::new(50, 0)));
720 assert_eq!(MULTI_RANGE.cost_for(50, 0), Some(Money::new(100, 0)));
721 assert_eq!(MULTI_RANGE.cost_for(100, 0), Some(Money::new(200, 0)));
722
723 assert_eq!(MULTI_RANGE.cost_for(25, 10000), Some(Money::new(50, 0)));
725 }
726
727 #[test]
728 fn include_matches_hours() {
729 let include = Include::Hours(Hours::new(6, 22, Stockholm));
730 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
731 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
732
733 assert!(include.matches(timestamp_match));
734 assert!(!include.matches(timestamp_no_match));
735 }
736
737 #[test]
738 fn include_matches_months() {
739 let include = Include::Months(Months::new(November, March, Stockholm));
740 let timestamp_match = Stockholm.dt(2025, 1, 15, 12, 0, 0);
741 let timestamp_no_match = Stockholm.dt(2025, 7, 15, 12, 0, 0);
742
743 assert!(include.matches(timestamp_match));
744 assert!(!include.matches(timestamp_no_match));
745 }
746
747 #[test]
748 fn exclude_matches_weekends_saturday() {
749 let exclude = Exclude::Weekends(Stockholm);
750 let timestamp = Stockholm.dt(2025, 1, 4, 12, 0, 0);
752 assert!(exclude.matches(timestamp));
753 }
754
755 #[test]
756 fn exclude_matches_weekends_sunday() {
757 let exclude = Exclude::Weekends(Stockholm);
758 let timestamp = Stockholm.dt(2025, 1, 5, 12, 0, 0);
760 assert!(exclude.matches(timestamp));
761 }
762
763 #[test]
764 fn exclude_does_not_match_weekday() {
765 let exclude = Exclude::Weekends(Stockholm);
766 let timestamp = Stockholm.dt(2025, 1, 6, 12, 0, 0);
768 assert!(!exclude.matches(timestamp));
769 }
770
771 #[test]
772 fn exclude_matches_swedish_new_year() {
773 let exclude = Exclude::Holidays(Country::SE, Stockholm);
774 let timestamp = Stockholm.dt(2025, 1, 1, 12, 0, 0);
776 assert!(exclude.matches(timestamp));
777 }
778
779 #[test]
780 fn exclude_does_not_match_non_holiday() {
781 let exclude = Exclude::Holidays(Country::SE, Stockholm);
782 let timestamp = Stockholm.dt(2025, 1, 2, 12, 0, 0);
784 assert!(!exclude.matches(timestamp));
785 }
786
787 #[test]
788 fn cost_period_matches_with_single_include() {
789 let period = CostPeriod::builder()
790 .load(LoadType::High)
791 .fixed_cost(10, 0)
792 .hours(6, 22)
793 .build();
794
795 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
796 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
797
798 assert!(period.matches(timestamp_match));
799 assert!(!period.matches(timestamp_no_match));
800 }
801
802 #[test]
803 fn cost_period_matches_with_multiple_includes() {
804 let period = CostPeriod::builder()
805 .load(LoadType::High)
806 .fixed_cost(10, 0)
807 .hours(6, 22)
808 .months(November, March)
809 .build();
810
811 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
813 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
815 let timestamp_wrong_months = Stockholm.dt(2025, 7, 15, 14, 0, 0);
817
818 assert!(period.matches(timestamp_match));
819 assert!(!period.matches(timestamp_wrong_hours));
820 assert!(!period.matches(timestamp_wrong_months));
821 }
822
823 #[test]
824 fn cost_period_matches_with_exclude_weekends() {
825 let period = CostPeriod::builder()
826 .load(LoadType::High)
827 .fixed_cost(10, 0)
828 .hours(6, 22)
829 .exclude_weekends()
830 .build();
831
832 println!("Excludes: {:?}", period.exclude_period_types());
833 println!("Includes: {:?}", period.include_period_types());
834
835 let timestamp_weekday = Stockholm.dt(2025, 1, 6, 14, 0, 0);
837 let timestamp_saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
839
840 assert!(period.matches(timestamp_weekday));
841 assert!(!period.matches(timestamp_saturday));
842 }
843
844 #[test]
845 fn cost_period_matches_with_exclude_holidays() {
846 let period = CostPeriod::builder()
847 .load(LoadType::High)
848 .fixed_cost(10, 0)
849 .hours(6, 22)
850 .exclude_holidays(Country::SE)
851 .build();
852
853 let timestamp_regular = Stockholm.dt(2025, 1, 2, 14, 0, 0);
855 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
857
858 assert!(period.matches(timestamp_regular));
859 assert!(!period.matches(timestamp_holiday));
860 }
861
862 #[test]
863 fn cost_period_matches_complex_scenario() {
864 let period = CostPeriod::builder()
866 .load(LoadType::High)
867 .fixed_cost(10, 0)
868 .months(November, March)
869 .hours(6, 22)
870 .exclude_weekends()
871 .exclude_holidays(Country::SE)
872 .build();
873
874 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
876
877 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
879
880 let timestamp_weekend = Stockholm.dt(2025, 1, 4, 14, 0, 0);
882
883 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
885
886 let timestamp_summer = Stockholm.dt(2025, 7, 15, 14, 0, 0);
888
889 assert!(period.matches(timestamp_match));
890 assert!(!period.matches(timestamp_wrong_hours));
891 assert!(!period.matches(timestamp_weekend));
892 assert!(!period.matches(timestamp_holiday));
893 assert!(!period.matches(timestamp_summer));
894 }
895
896 #[test]
897 fn cost_period_matches_base_load() {
898 let period = CostPeriod::builder()
900 .load(LoadType::Base)
901 .fixed_cost(5, 0)
902 .build();
903
904 let timestamp1 = Stockholm.dt(2025, 1, 1, 0, 0, 0);
906 let timestamp2 = Stockholm.dt(2025, 7, 15, 23, 59, 59);
907 let timestamp3 = Stockholm.dt(2025, 1, 4, 12, 0, 0);
908
909 assert!(period.matches(timestamp1));
910 assert!(period.matches(timestamp2));
911 assert!(period.matches(timestamp3));
912 }
913
914 #[test]
915 fn include_matches_hours_wraparound() {
916 let include = Include::Hours(Hours::new(22, 5, Stockholm));
918
919 let timestamp_evening = Stockholm.dt(2025, 1, 15, 22, 0, 0);
921 assert!(include.matches(timestamp_evening));
922
923 let timestamp_midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
925 assert!(include.matches(timestamp_midnight));
926
927 let timestamp_morning = Stockholm.dt(2025, 1, 15, 5, 30, 0);
929 assert!(include.matches(timestamp_morning));
930
931 let timestamp_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
933 assert!(!include.matches(timestamp_day));
934
935 let timestamp_after = Stockholm.dt(2025, 1, 15, 6, 0, 0);
937 assert!(!include.matches(timestamp_after));
938
939 let timestamp_before = Stockholm.dt(2025, 1, 15, 21, 59, 59);
941 assert!(!include.matches(timestamp_before));
942 }
943
944 #[test]
945 fn include_matches_months_wraparound() {
946 let include = Include::Months(Months::new(November, March, Stockholm));
948
949 let timestamp_nov = Stockholm.dt(2025, 11, 15, 12, 0, 0);
951 assert!(include.matches(timestamp_nov));
952
953 let timestamp_dec = Stockholm.dt(2025, 12, 15, 12, 0, 0);
955 assert!(include.matches(timestamp_dec));
956
957 let timestamp_jan = Stockholm.dt(2025, 1, 15, 12, 0, 0);
959 assert!(include.matches(timestamp_jan));
960
961 let timestamp_mar = Stockholm.dt(2025, 3, 15, 12, 0, 0);
963 assert!(include.matches(timestamp_mar));
964
965 let timestamp_jul = Stockholm.dt(2025, 7, 15, 12, 0, 0);
967 assert!(!include.matches(timestamp_jul));
968
969 let timestamp_oct = Stockholm.dt(2025, 10, 31, 23, 59, 59);
971 assert!(!include.matches(timestamp_oct));
972
973 let timestamp_apr = Stockholm.dt(2025, 4, 1, 0, 0, 0);
975 assert!(!include.matches(timestamp_apr));
976 }
977
978 #[test]
979 fn cost_period_matches_hours_wraparound() {
980 let period = CostPeriod::builder()
982 .load(LoadType::Low)
983 .fixed_cost(5, 0)
984 .hours(22, 5)
985 .build();
986
987 let timestamp_match_evening = Stockholm.dt(2025, 1, 15, 23, 0, 0);
988 let timestamp_match_morning = Stockholm.dt(2025, 1, 15, 3, 0, 0);
989 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
990
991 assert!(period.matches(timestamp_match_evening));
992 assert!(period.matches(timestamp_match_morning));
993 assert!(!period.matches(timestamp_no_match));
994 }
995
996 #[test]
997 fn cost_period_matches_with_both_excludes() {
998 let period = CostPeriod::builder()
999 .load(LoadType::High)
1000 .fixed_cost(10, 0)
1001 .hours(6, 22)
1002 .exclude_weekends()
1003 .exclude_holidays(Country::SE)
1004 .build();
1005
1006 let weekday = Stockholm.dt(2025, 1, 2, 14, 0, 0);
1008 assert!(period.matches(weekday));
1009
1010 let saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
1012 assert!(!period.matches(saturday));
1013
1014 let holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
1016 assert!(!period.matches(holiday));
1017
1018 let wrong_hours = Stockholm.dt(2025, 1, 2, 23, 0, 0);
1020 assert!(!period.matches(wrong_hours));
1021 }
1022
1023 #[test]
1024 fn exclude_matches_friday_is_not_weekend() {
1025 let exclude = Exclude::Weekends(Stockholm);
1026 let friday = Stockholm.dt(2025, 1, 3, 12, 0, 0);
1028 assert!(!exclude.matches(friday));
1029 }
1030
1031 #[test]
1032 fn exclude_matches_monday_is_not_weekend() {
1033 let exclude = Exclude::Weekends(Stockholm);
1034 let monday = Stockholm.dt(2025, 1, 6, 12, 0, 0);
1036 assert!(!exclude.matches(monday));
1037 }
1038
1039 #[test]
1040 fn exclude_matches_holiday_midsummer() {
1041 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1042 let midsummer = Stockholm.dt(2025, 6, 21, 12, 0, 0);
1044 assert!(exclude.matches(midsummer));
1045 }
1046
1047 #[test]
1048 fn cost_period_matches_month_and_hours() {
1049 let period = CostPeriod::builder()
1051 .load(LoadType::Low)
1052 .fixed_cost(5, 0)
1053 .month(June)
1054 .hours(22, 5)
1055 .build();
1056
1057 let match_june_night = Stockholm.dt(2025, 6, 15, 23, 0, 0);
1059 assert!(period.matches(match_june_night));
1060
1061 let june_day = Stockholm.dt(2025, 6, 15, 14, 0, 0);
1063 assert!(!period.matches(june_day));
1064
1065 let july_night = Stockholm.dt(2025, 7, 15, 23, 0, 0);
1067 assert!(!period.matches(july_night));
1068 }
1069
1070 #[test]
1071 fn cost_period_matches_months_and_hours_with_exclude() {
1072 let period = CostPeriod::builder()
1074 .load(LoadType::High)
1075 .fixed_cost(15, 0)
1076 .months(November, March)
1077 .hours(6, 22)
1078 .exclude_weekends()
1079 .exclude_holidays(Country::SE)
1080 .build();
1081
1082 let perfect = Stockholm.dt(2025, 1, 15, 10, 0, 0);
1084 assert!(period.matches(perfect));
1085
1086 let first_hour = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1088 assert!(period.matches(first_hour));
1089
1090 let last_hour = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1092 assert!(period.matches(last_hour));
1093
1094 let too_early = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1096 assert!(!period.matches(too_early));
1097
1098 let too_late = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1100 assert!(!period.matches(too_late));
1101
1102 let summer = Stockholm.dt(2025, 7, 15, 10, 0, 0);
1104 assert!(!period.matches(summer));
1105
1106 let weekend = Stockholm.dt(2025, 1, 4, 10, 0, 0);
1108 assert!(!period.matches(weekend));
1109 }
1110
1111 #[test]
1112 fn cost_period_matches_base_with_restrictions() {
1113 let period = CostPeriod::builder()
1115 .load(LoadType::Base)
1116 .fixed_cost(3, 0)
1117 .hours(0, 5)
1118 .build();
1119
1120 let match_night = Stockholm.dt(2025, 1, 15, 3, 0, 0);
1122 assert!(period.matches(match_night));
1123
1124 let no_match_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
1126 assert!(!period.matches(no_match_day));
1127 }
1128
1129 #[test]
1130 fn cost_period_matches_single_month() {
1131 let period = CostPeriod::builder()
1132 .load(LoadType::High)
1133 .fixed_cost(10, 0)
1134 .month(December)
1135 .build();
1136
1137 let dec_first = Stockholm.dt(2025, 12, 1, 0, 0, 0);
1139 assert!(period.matches(dec_first));
1140
1141 let dec_last = Stockholm.dt(2025, 12, 31, 23, 59, 59);
1143 assert!(period.matches(dec_last));
1144
1145 let nov = Stockholm.dt(2025, 11, 30, 12, 0, 0);
1147 assert!(!period.matches(nov));
1148
1149 let jan = Stockholm.dt(2025, 1, 1, 12, 0, 0);
1151 assert!(!period.matches(jan));
1152 }
1153
1154 #[test]
1155 fn cost_period_matches_all_hours() {
1156 let period = CostPeriod::builder()
1158 .load(LoadType::Low)
1159 .fixed_cost(5, 0)
1160 .hours(0, 23)
1161 .build();
1162
1163 let midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
1164 let noon = Stockholm.dt(2025, 1, 15, 12, 0, 0);
1165 let almost_midnight = Stockholm.dt(2025, 1, 15, 23, 59, 59);
1166
1167 assert!(period.matches(midnight));
1168 assert!(period.matches(noon));
1169 assert!(period.matches(almost_midnight));
1170 }
1171
1172 #[test]
1173 fn cost_period_matches_edge_of_month_range() {
1174 let period = CostPeriod::builder()
1176 .load(LoadType::Low)
1177 .fixed_cost(5, 0)
1178 .months(May, September)
1179 .build();
1180
1181 let may_start = Stockholm.dt(2025, 5, 1, 0, 0, 0);
1183 assert!(period.matches(may_start));
1184
1185 let april_end = Stockholm.dt(2025, 4, 30, 23, 59, 59);
1187 assert!(!period.matches(april_end));
1188
1189 let sept_end = Stockholm.dt(2025, 9, 30, 23, 59, 59);
1191 assert!(period.matches(sept_end));
1192
1193 let oct_start = Stockholm.dt(2025, 10, 1, 0, 0, 0);
1195 assert!(!period.matches(oct_start));
1196 }
1197
1198 #[test]
1199 fn include_matches_month_boundary() {
1200 let include = Include::Months(Months::new(February, February, Stockholm));
1202
1203 let feb_start = Stockholm.dt(2025, 2, 1, 0, 0, 0);
1205 assert!(include.matches(feb_start));
1206
1207 let feb_end = Stockholm.dt(2025, 2, 28, 23, 59, 59);
1209 assert!(include.matches(feb_end));
1210
1211 let jan_end = Stockholm.dt(2025, 1, 31, 23, 59, 59);
1213 assert!(!include.matches(jan_end));
1214
1215 let mar_start = Stockholm.dt(2025, 3, 1, 0, 0, 0);
1217 assert!(!include.matches(mar_start));
1218 }
1219
1220 #[test]
1221 fn include_matches_hours_exact_boundaries() {
1222 let include = Include::Hours(Hours::new(6, 22, Stockholm));
1223
1224 let start = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1226 assert!(include.matches(start));
1227
1228 let end = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1230 assert!(include.matches(end));
1231
1232 let before = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1234 assert!(!include.matches(before));
1235
1236 let after = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1238 assert!(!include.matches(after));
1239 }
1240
1241 #[test]
1242 fn exclude_matches_weekends_with_utc_timestamps() {
1243 let exclude = Exclude::Weekends(Stockholm);
1244
1245 let saturday_utc = Utc.dt(2025, 1, 4, 11, 0, 0);
1248 assert!(exclude.matches(saturday_utc));
1249
1250 let sunday_utc = Utc.dt(2025, 1, 5, 11, 0, 0);
1253 assert!(exclude.matches(sunday_utc));
1254
1255 let monday_utc = Utc.dt(2025, 1, 6, 11, 0, 0);
1258 assert!(!exclude.matches(monday_utc));
1259 }
1260
1261 #[test]
1262 fn exclude_matches_weekends_timezone_boundary() {
1263 let exclude = Exclude::Weekends(Stockholm);
1264
1265 let friday_utc_saturday_stockholm = Utc.dt(2025, 1, 3, 23, 0, 0);
1269 assert!(
1270 exclude.matches(friday_utc_saturday_stockholm),
1271 "Should match because it's Saturday in Stockholm timezone"
1272 );
1273
1274 let sunday_utc_monday_stockholm = Utc.dt(2025, 1, 5, 23, 0, 0);
1278 assert!(
1279 !exclude.matches(sunday_utc_monday_stockholm),
1280 "Should not match because it's Monday in Stockholm timezone"
1281 );
1282
1283 let sunday_late_utc = Utc.dt(2025, 1, 5, 22, 59, 0);
1286 assert!(
1287 exclude.matches(sunday_late_utc),
1288 "Should match because it's still Sunday in Stockholm timezone"
1289 );
1290 }
1291
1292 #[test]
1293 fn exclude_matches_holidays_with_utc_timestamps() {
1294 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1295
1296 let new_year_utc = Utc.dt(2025, 1, 1, 11, 0, 0);
1299 assert!(exclude.matches(new_year_utc));
1300
1301 let regular_day_utc = Utc.dt(2025, 1, 2, 11, 0, 0);
1304 assert!(!exclude.matches(regular_day_utc));
1305 }
1306
1307 #[test]
1308 fn exclude_matches_holidays_timezone_boundary() {
1309 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1310
1311 let dec31_utc_jan1_stockholm = Utc.dt(2024, 12, 31, 23, 0, 0);
1315 assert!(
1316 exclude.matches(dec31_utc_jan1_stockholm),
1317 "Should match because it's New Year's Day in Stockholm timezone"
1318 );
1319
1320 let jan1_utc_jan2_stockholm = Utc.dt(2025, 1, 1, 23, 0, 0);
1324 assert!(
1325 !exclude.matches(jan1_utc_jan2_stockholm),
1326 "Should not match because it's January 2 in Stockholm timezone"
1327 );
1328 }
1329
1330 #[test]
1331 fn exclude_matches_weekends_summer_timezone() {
1332 let exclude = Exclude::Weekends(Stockholm);
1333
1334 let saturday_summer_utc = Utc.dt(2025, 6, 7, 10, 0, 0);
1337 assert!(exclude.matches(saturday_summer_utc));
1338
1339 let friday_utc_saturday_stockholm_summer = Utc.dt(2025, 6, 6, 22, 0, 0);
1342 assert!(
1343 exclude.matches(friday_utc_saturday_stockholm_summer),
1344 "Should match because it's Saturday in Stockholm timezone (CEST)"
1345 );
1346 }
1347}