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