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