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