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 fixed_subunit_plus_vat(subunit: f64, country: Country) -> Self {
54 Self::Fixed(Money::new_subunit(subunit).add_vat(country))
55 }
56
57 pub const fn is_unverified(&self) -> bool {
58 matches!(self, Self::Unverified)
59 }
60
61 pub const fn divide_by(&self, by: i64) -> Self {
62 match self {
63 Self::None => Self::None,
64 Self::Unverified => Self::Unverified,
65 Self::Fixed(money) => Self::Fixed(money.divide_by(by)),
66 Self::Fuses(_) => panic!(".divide_by() is unsupported on Cost::Fuses"),
67 Self::FusesYearlyConsumption(_) => {
68 panic!(".divide_by() is unsupported on Cost::FuseRangeYearlyConsumption")
69 }
70 Self::FuseRange(_) => panic!(".divide_by() is unsupported on Cost::FuseRange"),
71 }
72 }
73
74 pub const fn cost_for(&self, fuse_size: u16, yearly_consumption: u32) -> Option<Money> {
75 match *self {
76 Cost::None => None,
77 Cost::Unverified => None,
78 Cost::Fixed(money) => Some(money),
79 Cost::Fuses(values) => {
80 let mut i = 0;
81 while i < values.len() {
82 let (fsize, money) = values[i];
83 if fuse_size == fsize {
84 return Some(money);
85 }
86 i += 1;
87 }
88 None
89 }
90 Cost::FusesYearlyConsumption(values) => {
91 let mut i = 0;
92 while i < values.len() {
93 let (fsize, max_consumption, money) = values[i];
94 if fsize == fuse_size {
95 if let Some(max_consumption) = max_consumption {
96 if yearly_consumption <= max_consumption {
97 return Some(money);
98 }
99 } else {
100 return Some(money);
101 }
102 }
103 i += 1;
104 }
105 None
106 }
107 Cost::FuseRange(ranges) => {
108 let mut i = 0;
109 while i < ranges.len() {
110 let (min, max, money) = ranges[i];
111 if fuse_size >= min && fuse_size <= max {
112 return Some(money);
113 }
114 i += 1;
115 }
116 None
117 }
118 }
119 }
120
121 pub const fn add_vat(&self, country: Country) -> Cost {
122 match self {
123 Cost::None => Cost::None,
124 Cost::Unverified => Cost::Unverified,
125 Cost::Fixed(money) => Cost::Fixed(money.add_vat(country)),
126 Cost::Fuses(_) => todo!(),
127 Cost::FusesYearlyConsumption(_) => todo!(),
128 Cost::FuseRange(_) => todo!(),
129 }
130 }
131
132 pub fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
133 match self {
134 Cost::FusesYearlyConsumption(items) => items
135 .iter()
136 .filter(|(fsize, _, _)| *fsize == fuse_size)
137 .any(|(_, yearly_consumption, _)| yearly_consumption.is_some()),
138 _ => false,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
145#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
146pub enum CostPeriodMatching {
147 First,
149 All,
151}
152
153#[derive(Debug, Clone, Copy, Serialize)]
154#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
155pub struct CostPeriods {
156 match_method: CostPeriodMatching,
157 periods: &'static [CostPeriod],
158}
159
160impl CostPeriods {
161 pub const fn new_first(periods: &'static [CostPeriod]) -> Self {
163 Self {
164 match_method: CostPeriodMatching::First,
165 periods,
166 }
167 }
168
169 pub const fn new_all(periods: &'static [CostPeriod]) -> Self {
171 Self {
172 match_method: CostPeriodMatching::All,
173 periods,
174 }
175 }
176
177 pub const fn match_method(&self) -> CostPeriodMatching {
178 self.match_method
179 }
180
181 pub fn iter(&self) -> Iter<'_, CostPeriod> {
182 self.periods.iter()
183 }
184
185 pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
186 self.periods
187 .iter()
188 .any(|cp| cp.is_yearly_consumption_based(fuse_size))
189 }
190
191 pub fn matching_periods<Tz: chrono::TimeZone>(
192 &self,
193 timestamp: DateTime<Tz>,
194 ) -> Vec<&CostPeriod>
195 where
196 DateTime<Tz>: Copy,
197 {
198 let mut ret = vec![];
199 for period in self.periods {
200 if period.matches(timestamp) {
201 ret.push(period);
202 if self.match_method == CostPeriodMatching::First {
203 break;
204 }
205 }
206 }
207 ret
208 }
209}
210
211#[derive(Debug, Clone, Serialize)]
213#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
214pub struct CostPeriodsSimple {
215 periods: Vec<CostPeriodSimple>,
216}
217
218impl CostPeriodsSimple {
219 pub(crate) fn new(
220 periods: CostPeriods,
221 fuse_size: u16,
222 yearly_consumption: u32,
223 language: Language,
224 ) -> Self {
225 Self {
226 periods: periods
227 .periods
228 .iter()
229 .flat_map(|period| {
230 CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
231 })
232 .collect(),
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize)]
238#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
239pub struct CostPeriod {
240 cost: Cost,
241 load: LoadType,
242 #[serde(serialize_with = "helpers::skip_nones")]
243 include: [Option<Include>; 2],
244 #[serde(serialize_with = "helpers::skip_nones")]
245 exclude: [Option<Exclude>; 2],
246}
247
248#[derive(Debug, Clone, Serialize)]
250#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
251pub(crate) struct CostPeriodSimple {
252 cost: Money,
253 load: LoadType,
254 include: Vec<Include>,
255 exclude: Vec<Exclude>,
256 info: String,
257}
258
259impl CostPeriodSimple {
260 fn new(
261 period: &CostPeriod,
262 fuse_size: u16,
263 yearly_consumption: u32,
264 language: Language,
265 ) -> Option<Self> {
266 let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
267 Some(
268 Self {
269 cost,
270 load: period.load,
271 include: period.include.into_iter().flatten().collect(),
272 exclude: period.exclude.into_iter().flatten().collect(),
273 info: Default::default(),
274 }
275 .add_info(language),
276 )
277 }
278
279 fn add_info(mut self, language: Language) -> Self {
280 let mut infos = Vec::new();
281 for include in &self.include {
282 infos.push(include.translate(language));
283 }
284 for exclude in &self.exclude {
285 infos.push(exclude.translate(language).into());
286 }
287 self.info = infos.join(", ");
288 self
289 }
290}
291
292impl CostPeriod {
293 pub const fn builder() -> CostPeriodBuilder {
294 CostPeriodBuilder::new()
295 }
296
297 pub const fn cost(&self) -> Cost {
298 self.cost
299 }
300
301 pub const fn load(&self) -> LoadType {
302 self.load
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}
344
345impl Default for CostPeriodBuilder {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351impl CostPeriodBuilder {
352 pub const fn new() -> Self {
353 let builder = Self {
354 timezone: None,
355 cost: Cost::None,
356 load: None,
357 include: [None; 2],
358 exclude: [None; 2],
359 };
360 builder.timezone(Timezone::Stockholm)
362 }
363
364 pub const fn build(self) -> CostPeriod {
365 CostPeriod {
366 cost: self.cost,
367 load: self.load.expect("`load` must be specified"),
368 include: self.include,
369 exclude: self.exclude,
370 }
371 }
372
373 pub const fn cost(mut self, cost: Cost) -> Self {
374 self.cost = cost;
375 self
376 }
377
378 pub const fn load(mut self, load: LoadType) -> Self {
379 self.load = Some(load);
380 self
381 }
382
383 pub const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
384 self.cost = Cost::fixed(int, fract);
385 self
386 }
387
388 pub const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
389 self.cost = Cost::fixed_subunit(subunit);
390 self
391 }
392
393 pub const fn timezone(mut self, timezone: Timezone) -> Self {
394 self.timezone = Some(timezone);
395 self
396 }
397
398 const fn get_timezone(&self) -> Timezone {
399 self.timezone.expect("`timezone` must be specified")
400 }
401
402 pub const fn include(mut self, period_type: Include) -> Self {
403 let mut i = 0;
404 while i < self.include.len() {
405 if self.include[i].is_some() {
406 i += 1;
407 } else {
408 self.include[i] = Some(period_type);
409 return self;
410 }
411 }
412 panic!("Too many includes");
413 }
414
415 pub const fn months(self, from: Month, to: Month) -> Self {
416 let timezone = self.get_timezone();
417 self.include(Include::Months(Months::new(from, to, timezone)))
418 }
419
420 pub const fn month(self, month: Month) -> Self {
421 self.months(month, month)
422 }
423
424 pub const fn hours(self, from: u8, to_inclusive: u8) -> Self {
425 let timezone = self.get_timezone();
426 self.include(Include::Hours(Hours::new(from, to_inclusive, timezone)))
427 }
428
429 const fn exclude(mut self, period_type: Exclude) -> Self {
430 let mut i = 0;
431 while i < self.exclude.len() {
432 if self.exclude[i].is_some() {
433 i += 1;
434 } else {
435 self.exclude[i] = Some(period_type);
436 return self;
437 }
438 }
439 panic!("Too many excludes");
440 }
441
442 pub const fn exclude_holidays(self, country: Country) -> Self {
443 let tz = self.get_timezone();
444 self.exclude(Exclude::Holidays(country, tz))
445 }
446
447 pub const fn exclude_weekends(self) -> Self {
448 let tz = self.get_timezone();
449 self.exclude(Exclude::Weekends(tz))
450 }
451}
452
453#[derive(Debug, Clone, Copy, Serialize)]
454#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
455pub enum Include {
456 Months(Months),
457 Hours(Hours),
458}
459
460impl Include {
461 fn translate(&self, language: Language) -> String {
462 match self {
463 Include::Months(months) => months.translate(language),
464 Include::Hours(hours) => hours.translate(language),
465 }
466 }
467
468 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
469 match self {
470 Include::Months(months) => months.matches(timestamp),
471 Include::Hours(hours) => hours.matches(timestamp),
472 }
473 }
474}
475
476#[derive(Debug, Clone, Copy, Serialize)]
477#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
478pub enum Exclude {
479 Weekends(Timezone),
480 Holidays(Country, Timezone),
481}
482
483impl Exclude {
484 pub(crate) fn translate(&self, language: Language) -> &'static str {
485 match language {
486 Language::En => match self {
487 Exclude::Weekends(_) => "Weekends",
488 Exclude::Holidays(country, _) => match country {
489 Country::SE => "Swedish holidays",
490 },
491 },
492 Language::Sv => match self {
493 Exclude::Weekends(_) => "Helg",
494 Exclude::Holidays(country, _) => match country {
495 Country::SE => "Svenska helgdagar",
496 },
497 },
498 }
499 }
500
501 fn matches<Tz: chrono::TimeZone>(&self, timestamp: DateTime<Tz>) -> bool {
502 let tz_timestamp = timestamp.with_timezone(&self.tz());
503 match self {
504 Exclude::Weekends(_) => (6..=7).contains(&tz_timestamp.weekday().number_from_monday()),
505 Exclude::Holidays(country, _) => country.is_holiday(tz_timestamp.date_naive()),
506 }
507 }
508
509 const fn tz(&self) -> chrono_tz::Tz {
510 match self {
511 Exclude::Weekends(timezone) => timezone.to_tz(),
512 Exclude::Holidays(_, timezone) => timezone.to_tz(),
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519
520 use super::*;
521 use crate::money::Money;
522 use crate::months::Month::*;
523 use crate::{Stockholm, Utc};
524
525 #[test]
526 fn cost_for_none() {
527 const NONE_COST: Cost = Cost::None;
528 assert_eq!(NONE_COST.cost_for(16, 0), None);
529 assert_eq!(NONE_COST.cost_for(25, 5000), None);
530 }
531
532 #[test]
533 fn cost_for_unverified() {
534 const UNVERIFIED_COST: Cost = Cost::Unverified;
535 assert_eq!(UNVERIFIED_COST.cost_for(16, 0), None);
536 assert_eq!(UNVERIFIED_COST.cost_for(25, 5000), None);
537 }
538
539 #[test]
540 fn cost_for_fixed() {
541 const FIXED_COST: Cost = Cost::Fixed(Money::new(100, 50));
542 assert_eq!(FIXED_COST.cost_for(16, 0), Some(Money::new(100, 50)));
544 assert_eq!(FIXED_COST.cost_for(25, 5000), Some(Money::new(100, 50)));
545 assert_eq!(FIXED_COST.cost_for(63, 10000), Some(Money::new(100, 50)));
546 }
547
548 #[test]
549 fn cost_for_fuses_exact_match() {
550 const FUSES_COST: Cost = Cost::fuses(&[
551 (16, Money::new(50, 0)),
552 (25, Money::new(75, 0)),
553 (35, Money::new(100, 0)),
554 (50, Money::new(150, 0)),
555 ]);
556
557 assert_eq!(FUSES_COST.cost_for(16, 0), Some(Money::new(50, 0)));
559 assert_eq!(FUSES_COST.cost_for(25, 0), Some(Money::new(75, 0)));
560 assert_eq!(FUSES_COST.cost_for(35, 0), Some(Money::new(100, 0)));
561 assert_eq!(FUSES_COST.cost_for(50, 0), Some(Money::new(150, 0)));
562
563 assert_eq!(FUSES_COST.cost_for(25, 500000), Some(Money::new(75, 0)));
565 }
566
567 #[test]
568 fn cost_for_fuses_no_match() {
569 const FUSES_COST: Cost = Cost::fuses(&[(16, Money::new(50, 0)), (25, Money::new(75, 0))]);
570
571 assert_eq!(FUSES_COST.cost_for(20, 0), None);
573 assert_eq!(FUSES_COST.cost_for(63, 0), None);
574 }
575
576 #[test]
577 fn cost_for_fuses_yearly_consumption_with_limit() {
578 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
579 (16, Some(5000), Money::new(50, 0)),
580 (16, None, Money::new(75, 0)),
581 (25, Some(10000), Money::new(100, 0)),
582 (25, None, Money::new(125, 0)),
583 ]);
584
585 assert_eq!(
587 FUSES_WITH_CONSUMPTION.cost_for(16, 3000),
588 Some(Money::new(50, 0))
589 );
590
591 assert_eq!(
593 FUSES_WITH_CONSUMPTION.cost_for(16, 5000),
594 Some(Money::new(50, 0))
595 );
596
597 assert_eq!(
599 FUSES_WITH_CONSUMPTION.cost_for(16, 6000),
600 Some(Money::new(75, 0))
601 );
602
603 assert_eq!(
605 FUSES_WITH_CONSUMPTION.cost_for(16, 20000),
606 Some(Money::new(75, 0))
607 );
608
609 assert_eq!(
611 FUSES_WITH_CONSUMPTION.cost_for(25, 10000),
612 Some(Money::new(100, 0))
613 );
614
615 assert_eq!(
617 FUSES_WITH_CONSUMPTION.cost_for(25, 15000),
618 Some(Money::new(125, 0))
619 );
620
621 assert_eq!(
623 FUSES_WITH_CONSUMPTION.cost_for(25, 5000),
624 Some(Money::new(100, 0))
625 );
626 }
627
628 #[test]
629 fn cost_for_fuses_yearly_consumption_no_limit() {
630 const FUSES_NO_LIMIT: Cost = Cost::fuses_with_yearly_consumption(&[
631 (16, None, Money::new(50, 0)),
632 (25, None, Money::new(75, 0)),
633 ]);
634
635 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 0), Some(Money::new(50, 0)));
637 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 1000), Some(Money::new(50, 0)));
638 assert_eq!(FUSES_NO_LIMIT.cost_for(16, 50000), Some(Money::new(50, 0)));
639 assert_eq!(FUSES_NO_LIMIT.cost_for(25, 100000), Some(Money::new(75, 0)));
640 }
641
642 #[test]
643 fn cost_for_fuses_yearly_consumption_no_fuse_match() {
644 const FUSES_WITH_CONSUMPTION: Cost = Cost::fuses_with_yearly_consumption(&[
645 (16, Some(5000), Money::new(50, 0)),
646 (25, Some(10000), Money::new(100, 0)),
647 ]);
648
649 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(35, 5000), None);
651 assert_eq!(FUSES_WITH_CONSUMPTION.cost_for(50, 10000), None);
652 }
653
654 #[test]
655 fn cost_for_fuses_yearly_consumption_max_limit_no_fallback() {
656 const FUSES_ONLY_LIMITS: Cost = Cost::fuses_with_yearly_consumption(&[
657 (16, Some(5000), Money::new(50, 0)),
658 (25, Some(10000), Money::new(100, 0)),
659 ]);
660
661 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 0), Some(Money::new(50, 0)));
663 assert_eq!(
664 FUSES_ONLY_LIMITS.cost_for(16, 3000),
665 Some(Money::new(50, 0))
666 );
667 assert_eq!(
668 FUSES_ONLY_LIMITS.cost_for(16, 4999),
669 Some(Money::new(50, 0))
670 );
671 assert_eq!(
672 FUSES_ONLY_LIMITS.cost_for(16, 5000),
673 Some(Money::new(50, 0))
674 );
675 assert_eq!(
676 FUSES_ONLY_LIMITS.cost_for(25, 9999),
677 Some(Money::new(100, 0))
678 );
679 assert_eq!(
680 FUSES_ONLY_LIMITS.cost_for(25, 10000),
681 Some(Money::new(100, 0))
682 );
683
684 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 5001), None);
686 assert_eq!(FUSES_ONLY_LIMITS.cost_for(16, 10000), None);
687 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 10001), None);
688 assert_eq!(FUSES_ONLY_LIMITS.cost_for(25, 20000), None);
689 }
690
691 #[test]
692 fn cost_for_fuse_range_within_range() {
693 const FUSE_BASED: Cost = Cost::fuse_range(&[
694 (16, 35, Money::new(54, 0)),
695 (35, u16::MAX, Money::new(108, 50)),
696 ]);
697
698 assert_eq!(FUSE_BASED.cost_for(10, 0), None);
700 assert_eq!(FUSE_BASED.cost_for(15, 0), None);
701
702 assert_eq!(FUSE_BASED.cost_for(16, 0), Some(Money::new(54, 0)));
704 assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
705 assert_eq!(FUSE_BASED.cost_for(35, 0), Some(Money::new(54, 0)));
706
707 assert_eq!(FUSE_BASED.cost_for(36, 0), Some(Money::new(108, 50)));
709 assert_eq!(FUSE_BASED.cost_for(50, 0), Some(Money::new(108, 50)));
710 assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
711 assert_eq!(FUSE_BASED.cost_for(u16::MAX, 0), Some(Money::new(108, 50)));
712 }
713
714 #[test]
715 fn cost_for_fuse_range_multiple_ranges() {
716 const MULTI_RANGE: Cost = Cost::fuse_range(&[
717 (1, 15, Money::new(20, 0)),
718 (16, 35, Money::new(50, 0)),
719 (36, 63, Money::new(100, 0)),
720 (64, u16::MAX, Money::new(200, 0)),
721 ]);
722
723 assert_eq!(MULTI_RANGE.cost_for(10, 0), Some(Money::new(20, 0)));
725 assert_eq!(MULTI_RANGE.cost_for(25, 0), Some(Money::new(50, 0)));
726 assert_eq!(MULTI_RANGE.cost_for(50, 0), Some(Money::new(100, 0)));
727 assert_eq!(MULTI_RANGE.cost_for(100, 0), Some(Money::new(200, 0)));
728
729 assert_eq!(MULTI_RANGE.cost_for(25, 10000), Some(Money::new(50, 0)));
731 }
732
733 #[test]
734 fn include_matches_hours() {
735 let include = Include::Hours(Hours::new(6, 22, Stockholm));
736 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
737 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
738
739 assert!(include.matches(timestamp_match));
740 assert!(!include.matches(timestamp_no_match));
741 }
742
743 #[test]
744 fn include_matches_months() {
745 let include = Include::Months(Months::new(November, March, Stockholm));
746 let timestamp_match = Stockholm.dt(2025, 1, 15, 12, 0, 0);
747 let timestamp_no_match = Stockholm.dt(2025, 7, 15, 12, 0, 0);
748
749 assert!(include.matches(timestamp_match));
750 assert!(!include.matches(timestamp_no_match));
751 }
752
753 #[test]
754 fn exclude_matches_weekends_saturday() {
755 let exclude = Exclude::Weekends(Stockholm);
756 let timestamp = Stockholm.dt(2025, 1, 4, 12, 0, 0);
758 assert!(exclude.matches(timestamp));
759 }
760
761 #[test]
762 fn exclude_matches_weekends_sunday() {
763 let exclude = Exclude::Weekends(Stockholm);
764 let timestamp = Stockholm.dt(2025, 1, 5, 12, 0, 0);
766 assert!(exclude.matches(timestamp));
767 }
768
769 #[test]
770 fn exclude_does_not_match_weekday() {
771 let exclude = Exclude::Weekends(Stockholm);
772 let timestamp = Stockholm.dt(2025, 1, 6, 12, 0, 0);
774 assert!(!exclude.matches(timestamp));
775 }
776
777 #[test]
778 fn exclude_matches_swedish_new_year() {
779 let exclude = Exclude::Holidays(Country::SE, Stockholm);
780 let timestamp = Stockholm.dt(2025, 1, 1, 12, 0, 0);
782 assert!(exclude.matches(timestamp));
783 }
784
785 #[test]
786 fn exclude_does_not_match_non_holiday() {
787 let exclude = Exclude::Holidays(Country::SE, Stockholm);
788 let timestamp = Stockholm.dt(2025, 1, 2, 12, 0, 0);
790 assert!(!exclude.matches(timestamp));
791 }
792
793 #[test]
794 fn cost_period_matches_with_single_include() {
795 let period = CostPeriod::builder()
796 .load(LoadType::High)
797 .fixed_cost(10, 0)
798 .hours(6, 22)
799 .build();
800
801 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
802 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 23, 0, 0);
803
804 assert!(period.matches(timestamp_match));
805 assert!(!period.matches(timestamp_no_match));
806 }
807
808 #[test]
809 fn cost_period_matches_with_multiple_includes() {
810 let period = CostPeriod::builder()
811 .load(LoadType::High)
812 .fixed_cost(10, 0)
813 .hours(6, 22)
814 .months(November, March)
815 .build();
816
817 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
819 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
821 let timestamp_wrong_months = Stockholm.dt(2025, 7, 15, 14, 0, 0);
823
824 assert!(period.matches(timestamp_match));
825 assert!(!period.matches(timestamp_wrong_hours));
826 assert!(!period.matches(timestamp_wrong_months));
827 }
828
829 #[test]
830 fn cost_period_matches_with_exclude_weekends() {
831 let period = CostPeriod::builder()
832 .load(LoadType::High)
833 .fixed_cost(10, 0)
834 .hours(6, 22)
835 .exclude_weekends()
836 .build();
837
838 println!("Excludes: {:?}", period.exclude_period_types());
839 println!("Includes: {:?}", period.include_period_types());
840
841 let timestamp_weekday = Stockholm.dt(2025, 1, 6, 14, 0, 0);
843 let timestamp_saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
845
846 assert!(period.matches(timestamp_weekday));
847 assert!(!period.matches(timestamp_saturday));
848 }
849
850 #[test]
851 fn cost_period_matches_with_exclude_holidays() {
852 let period = CostPeriod::builder()
853 .load(LoadType::High)
854 .fixed_cost(10, 0)
855 .hours(6, 22)
856 .exclude_holidays(Country::SE)
857 .build();
858
859 let timestamp_regular = Stockholm.dt(2025, 1, 2, 14, 0, 0);
861 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
863
864 assert!(period.matches(timestamp_regular));
865 assert!(!period.matches(timestamp_holiday));
866 }
867
868 #[test]
869 fn cost_period_matches_complex_scenario() {
870 let period = CostPeriod::builder()
872 .load(LoadType::High)
873 .fixed_cost(10, 0)
874 .months(November, March)
875 .hours(6, 22)
876 .exclude_weekends()
877 .exclude_holidays(Country::SE)
878 .build();
879
880 let timestamp_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
882
883 let timestamp_wrong_hours = Stockholm.dt(2025, 1, 15, 23, 0, 0);
885
886 let timestamp_weekend = Stockholm.dt(2025, 1, 4, 14, 0, 0);
888
889 let timestamp_holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
891
892 let timestamp_summer = Stockholm.dt(2025, 7, 15, 14, 0, 0);
894
895 assert!(period.matches(timestamp_match));
896 assert!(!period.matches(timestamp_wrong_hours));
897 assert!(!period.matches(timestamp_weekend));
898 assert!(!period.matches(timestamp_holiday));
899 assert!(!period.matches(timestamp_summer));
900 }
901
902 #[test]
903 fn cost_period_matches_base_load() {
904 let period = CostPeriod::builder()
906 .load(LoadType::Base)
907 .fixed_cost(5, 0)
908 .build();
909
910 let timestamp1 = Stockholm.dt(2025, 1, 1, 0, 0, 0);
912 let timestamp2 = Stockholm.dt(2025, 7, 15, 23, 59, 59);
913 let timestamp3 = Stockholm.dt(2025, 1, 4, 12, 0, 0);
914
915 assert!(period.matches(timestamp1));
916 assert!(period.matches(timestamp2));
917 assert!(period.matches(timestamp3));
918 }
919
920 #[test]
921 fn include_matches_hours_wraparound() {
922 let include = Include::Hours(Hours::new(22, 5, Stockholm));
924
925 let timestamp_evening = Stockholm.dt(2025, 1, 15, 22, 0, 0);
927 assert!(include.matches(timestamp_evening));
928
929 let timestamp_midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
931 assert!(include.matches(timestamp_midnight));
932
933 let timestamp_morning = Stockholm.dt(2025, 1, 15, 5, 30, 0);
935 assert!(include.matches(timestamp_morning));
936
937 let timestamp_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
939 assert!(!include.matches(timestamp_day));
940
941 let timestamp_after = Stockholm.dt(2025, 1, 15, 6, 0, 0);
943 assert!(!include.matches(timestamp_after));
944
945 let timestamp_before = Stockholm.dt(2025, 1, 15, 21, 59, 59);
947 assert!(!include.matches(timestamp_before));
948 }
949
950 #[test]
951 fn include_matches_months_wraparound() {
952 let include = Include::Months(Months::new(November, March, Stockholm));
954
955 let timestamp_nov = Stockholm.dt(2025, 11, 15, 12, 0, 0);
957 assert!(include.matches(timestamp_nov));
958
959 let timestamp_dec = Stockholm.dt(2025, 12, 15, 12, 0, 0);
961 assert!(include.matches(timestamp_dec));
962
963 let timestamp_jan = Stockholm.dt(2025, 1, 15, 12, 0, 0);
965 assert!(include.matches(timestamp_jan));
966
967 let timestamp_mar = Stockholm.dt(2025, 3, 15, 12, 0, 0);
969 assert!(include.matches(timestamp_mar));
970
971 let timestamp_jul = Stockholm.dt(2025, 7, 15, 12, 0, 0);
973 assert!(!include.matches(timestamp_jul));
974
975 let timestamp_oct = Stockholm.dt(2025, 10, 31, 23, 59, 59);
977 assert!(!include.matches(timestamp_oct));
978
979 let timestamp_apr = Stockholm.dt(2025, 4, 1, 0, 0, 0);
981 assert!(!include.matches(timestamp_apr));
982 }
983
984 #[test]
985 fn cost_period_matches_hours_wraparound() {
986 let period = CostPeriod::builder()
988 .load(LoadType::Low)
989 .fixed_cost(5, 0)
990 .hours(22, 5)
991 .build();
992
993 let timestamp_match_evening = Stockholm.dt(2025, 1, 15, 23, 0, 0);
994 let timestamp_match_morning = Stockholm.dt(2025, 1, 15, 3, 0, 0);
995 let timestamp_no_match = Stockholm.dt(2025, 1, 15, 14, 0, 0);
996
997 assert!(period.matches(timestamp_match_evening));
998 assert!(period.matches(timestamp_match_morning));
999 assert!(!period.matches(timestamp_no_match));
1000 }
1001
1002 #[test]
1003 fn cost_period_matches_with_both_excludes() {
1004 let period = CostPeriod::builder()
1005 .load(LoadType::High)
1006 .fixed_cost(10, 0)
1007 .hours(6, 22)
1008 .exclude_weekends()
1009 .exclude_holidays(Country::SE)
1010 .build();
1011
1012 let weekday = Stockholm.dt(2025, 1, 2, 14, 0, 0);
1014 assert!(period.matches(weekday));
1015
1016 let saturday = Stockholm.dt(2025, 1, 4, 14, 0, 0);
1018 assert!(!period.matches(saturday));
1019
1020 let holiday = Stockholm.dt(2025, 1, 1, 14, 0, 0);
1022 assert!(!period.matches(holiday));
1023
1024 let wrong_hours = Stockholm.dt(2025, 1, 2, 23, 0, 0);
1026 assert!(!period.matches(wrong_hours));
1027 }
1028
1029 #[test]
1030 fn exclude_matches_friday_is_not_weekend() {
1031 let exclude = Exclude::Weekends(Stockholm);
1032 let friday = Stockholm.dt(2025, 1, 3, 12, 0, 0);
1034 assert!(!exclude.matches(friday));
1035 }
1036
1037 #[test]
1038 fn exclude_matches_monday_is_not_weekend() {
1039 let exclude = Exclude::Weekends(Stockholm);
1040 let monday = Stockholm.dt(2025, 1, 6, 12, 0, 0);
1042 assert!(!exclude.matches(monday));
1043 }
1044
1045 #[test]
1046 fn exclude_matches_holiday_midsummer() {
1047 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1048 let midsummer = Stockholm.dt(2025, 6, 21, 12, 0, 0);
1050 assert!(exclude.matches(midsummer));
1051 }
1052
1053 #[test]
1054 fn cost_period_matches_month_and_hours() {
1055 let period = CostPeriod::builder()
1057 .load(LoadType::Low)
1058 .fixed_cost(5, 0)
1059 .month(June)
1060 .hours(22, 5)
1061 .build();
1062
1063 let match_june_night = Stockholm.dt(2025, 6, 15, 23, 0, 0);
1065 assert!(period.matches(match_june_night));
1066
1067 let june_day = Stockholm.dt(2025, 6, 15, 14, 0, 0);
1069 assert!(!period.matches(june_day));
1070
1071 let july_night = Stockholm.dt(2025, 7, 15, 23, 0, 0);
1073 assert!(!period.matches(july_night));
1074 }
1075
1076 #[test]
1077 fn cost_period_matches_months_and_hours_with_exclude() {
1078 let period = CostPeriod::builder()
1080 .load(LoadType::High)
1081 .fixed_cost(15, 0)
1082 .months(November, March)
1083 .hours(6, 22)
1084 .exclude_weekends()
1085 .exclude_holidays(Country::SE)
1086 .build();
1087
1088 let perfect = Stockholm.dt(2025, 1, 15, 10, 0, 0);
1090 assert!(period.matches(perfect));
1091
1092 let first_hour = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1094 assert!(period.matches(first_hour));
1095
1096 let last_hour = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1098 assert!(period.matches(last_hour));
1099
1100 let too_early = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1102 assert!(!period.matches(too_early));
1103
1104 let too_late = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1106 assert!(!period.matches(too_late));
1107
1108 let summer = Stockholm.dt(2025, 7, 15, 10, 0, 0);
1110 assert!(!period.matches(summer));
1111
1112 let weekend = Stockholm.dt(2025, 1, 4, 10, 0, 0);
1114 assert!(!period.matches(weekend));
1115 }
1116
1117 #[test]
1118 fn cost_period_matches_base_with_restrictions() {
1119 let period = CostPeriod::builder()
1121 .load(LoadType::Base)
1122 .fixed_cost(3, 0)
1123 .hours(0, 5)
1124 .build();
1125
1126 let match_night = Stockholm.dt(2025, 1, 15, 3, 0, 0);
1128 assert!(period.matches(match_night));
1129
1130 let no_match_day = Stockholm.dt(2025, 1, 15, 14, 0, 0);
1132 assert!(!period.matches(no_match_day));
1133 }
1134
1135 #[test]
1136 fn cost_period_matches_single_month() {
1137 let period = CostPeriod::builder()
1138 .load(LoadType::High)
1139 .fixed_cost(10, 0)
1140 .month(December)
1141 .build();
1142
1143 let dec_first = Stockholm.dt(2025, 12, 1, 0, 0, 0);
1145 assert!(period.matches(dec_first));
1146
1147 let dec_last = Stockholm.dt(2025, 12, 31, 23, 59, 59);
1149 assert!(period.matches(dec_last));
1150
1151 let nov = Stockholm.dt(2025, 11, 30, 12, 0, 0);
1153 assert!(!period.matches(nov));
1154
1155 let jan = Stockholm.dt(2025, 1, 1, 12, 0, 0);
1157 assert!(!period.matches(jan));
1158 }
1159
1160 #[test]
1161 fn cost_period_matches_all_hours() {
1162 let period = CostPeriod::builder()
1164 .load(LoadType::Low)
1165 .fixed_cost(5, 0)
1166 .hours(0, 23)
1167 .build();
1168
1169 let midnight = Stockholm.dt(2025, 1, 15, 0, 0, 0);
1170 let noon = Stockholm.dt(2025, 1, 15, 12, 0, 0);
1171 let almost_midnight = Stockholm.dt(2025, 1, 15, 23, 59, 59);
1172
1173 assert!(period.matches(midnight));
1174 assert!(period.matches(noon));
1175 assert!(period.matches(almost_midnight));
1176 }
1177
1178 #[test]
1179 fn cost_period_matches_edge_of_month_range() {
1180 let period = CostPeriod::builder()
1182 .load(LoadType::Low)
1183 .fixed_cost(5, 0)
1184 .months(May, September)
1185 .build();
1186
1187 let may_start = Stockholm.dt(2025, 5, 1, 0, 0, 0);
1189 assert!(period.matches(may_start));
1190
1191 let april_end = Stockholm.dt(2025, 4, 30, 23, 59, 59);
1193 assert!(!period.matches(april_end));
1194
1195 let sept_end = Stockholm.dt(2025, 9, 30, 23, 59, 59);
1197 assert!(period.matches(sept_end));
1198
1199 let oct_start = Stockholm.dt(2025, 10, 1, 0, 0, 0);
1201 assert!(!period.matches(oct_start));
1202 }
1203
1204 #[test]
1205 fn include_matches_month_boundary() {
1206 let include = Include::Months(Months::new(February, February, Stockholm));
1208
1209 let feb_start = Stockholm.dt(2025, 2, 1, 0, 0, 0);
1211 assert!(include.matches(feb_start));
1212
1213 let feb_end = Stockholm.dt(2025, 2, 28, 23, 59, 59);
1215 assert!(include.matches(feb_end));
1216
1217 let jan_end = Stockholm.dt(2025, 1, 31, 23, 59, 59);
1219 assert!(!include.matches(jan_end));
1220
1221 let mar_start = Stockholm.dt(2025, 3, 1, 0, 0, 0);
1223 assert!(!include.matches(mar_start));
1224 }
1225
1226 #[test]
1227 fn include_matches_hours_exact_boundaries() {
1228 let include = Include::Hours(Hours::new(6, 22, Stockholm));
1229
1230 let start = Stockholm.dt(2025, 1, 15, 6, 0, 0);
1232 assert!(include.matches(start));
1233
1234 let end = Stockholm.dt(2025, 1, 15, 22, 59, 59);
1236 assert!(include.matches(end));
1237
1238 let before = Stockholm.dt(2025, 1, 15, 5, 59, 59);
1240 assert!(!include.matches(before));
1241
1242 let after = Stockholm.dt(2025, 1, 15, 23, 0, 0);
1244 assert!(!include.matches(after));
1245 }
1246
1247 #[test]
1248 fn exclude_matches_weekends_with_utc_timestamps() {
1249 let exclude = Exclude::Weekends(Stockholm);
1250
1251 let saturday_utc = Utc.dt(2025, 1, 4, 11, 0, 0);
1254 assert!(exclude.matches(saturday_utc));
1255
1256 let sunday_utc = Utc.dt(2025, 1, 5, 11, 0, 0);
1259 assert!(exclude.matches(sunday_utc));
1260
1261 let monday_utc = Utc.dt(2025, 1, 6, 11, 0, 0);
1264 assert!(!exclude.matches(monday_utc));
1265 }
1266
1267 #[test]
1268 fn exclude_matches_weekends_timezone_boundary() {
1269 let exclude = Exclude::Weekends(Stockholm);
1270
1271 let friday_utc_saturday_stockholm = Utc.dt(2025, 1, 3, 23, 0, 0);
1275 assert!(
1276 exclude.matches(friday_utc_saturday_stockholm),
1277 "Should match because it's Saturday in Stockholm timezone"
1278 );
1279
1280 let sunday_utc_monday_stockholm = Utc.dt(2025, 1, 5, 23, 0, 0);
1284 assert!(
1285 !exclude.matches(sunday_utc_monday_stockholm),
1286 "Should not match because it's Monday in Stockholm timezone"
1287 );
1288
1289 let sunday_late_utc = Utc.dt(2025, 1, 5, 22, 59, 0);
1292 assert!(
1293 exclude.matches(sunday_late_utc),
1294 "Should match because it's still Sunday in Stockholm timezone"
1295 );
1296 }
1297
1298 #[test]
1299 fn exclude_matches_holidays_with_utc_timestamps() {
1300 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1301
1302 let new_year_utc = Utc.dt(2025, 1, 1, 11, 0, 0);
1305 assert!(exclude.matches(new_year_utc));
1306
1307 let regular_day_utc = Utc.dt(2025, 1, 2, 11, 0, 0);
1310 assert!(!exclude.matches(regular_day_utc));
1311 }
1312
1313 #[test]
1314 fn exclude_matches_holidays_timezone_boundary() {
1315 let exclude = Exclude::Holidays(Country::SE, Stockholm);
1316
1317 let dec31_utc_jan1_stockholm = Utc.dt(2024, 12, 31, 23, 0, 0);
1321 assert!(
1322 exclude.matches(dec31_utc_jan1_stockholm),
1323 "Should match because it's New Year's Day in Stockholm timezone"
1324 );
1325
1326 let jan1_utc_jan2_stockholm = Utc.dt(2025, 1, 1, 23, 0, 0);
1330 assert!(
1331 !exclude.matches(jan1_utc_jan2_stockholm),
1332 "Should not match because it's January 2 in Stockholm timezone"
1333 );
1334 }
1335
1336 #[test]
1337 fn exclude_matches_weekends_summer_timezone() {
1338 let exclude = Exclude::Weekends(Stockholm);
1339
1340 let saturday_summer_utc = Utc.dt(2025, 6, 7, 10, 0, 0);
1343 assert!(exclude.matches(saturday_summer_utc));
1344
1345 let friday_utc_saturday_stockholm_summer = Utc.dt(2025, 6, 6, 22, 0, 0);
1348 assert!(
1349 exclude.matches(friday_utc_saturday_stockholm_summer),
1350 "Should match because it's Saturday in Stockholm timezone (CEST)"
1351 );
1352 }
1353}