grid_tariffs/
costs.rs

1use std::slice::Iter;
2
3use chrono::DateTime;
4use chrono_tz::Tz;
5use serde::{Serialize, Serializer, ser::SerializeSeq};
6
7use crate::{
8    Country, Language, Money,
9    defs::{Hours, Month, Months},
10    helpers,
11};
12
13#[derive(Debug, Clone, Copy, Serialize)]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15pub enum Cost {
16    None,
17    /// Cost has not been verified
18    Unverified,
19    Fixed(Money),
20    Fuses(&'static [(u16, Money)]),
21    /// Fuse size combined with a yearly energy consumption limit
22    FusesYearlyConsumption(&'static [(u16, Option<u32>, Money)]),
23    FuseRange(&'static [(u16, u16, Money)]),
24}
25
26impl Cost {
27    pub const fn is_unverified(&self) -> bool {
28        matches!(self, Self::Unverified)
29    }
30
31    pub(super) const fn fuses(values: &'static [(u16, Money)]) -> Self {
32        Self::Fuses(values)
33    }
34
35    pub(super) const fn fuse_range(ranges: &'static [(u16, u16, Money)]) -> Self {
36        Self::FuseRange(ranges)
37    }
38
39    pub(super) const fn fuses_with_yearly_consumption(
40        values: &'static [(u16, Option<u32>, Money)],
41    ) -> Cost {
42        Self::FusesYearlyConsumption(values)
43    }
44
45    pub(super) const fn fixed(int: i64, fract: u8) -> Self {
46        Self::Fixed(Money::new(int, fract))
47    }
48
49    pub(super) const fn fixed_yearly(int: i64, fract: u8) -> Self {
50        Self::Fixed(Money::new(int, fract).divide_by(12))
51    }
52
53    pub(super) const fn fixed_subunit(subunit: f64) -> Self {
54        Self::Fixed(Money::new_subunit(subunit))
55    }
56
57    pub(super) 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(items) => panic!(".divide_by() is unsupported on Cost::Fuses"),
63            Self::FusesYearlyConsumption(items) => {
64                panic!(".divide_by() is unsupported on Cost::FuseRangeYearlyConsumption")
65            }
66            Self::FuseRange(items) => 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 max_consumption <= yearly_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(crate) const fn add_vat(&self, country: Country) -> Cost {
118        let rate = match country {
119            Country::SE => 1.25,
120        };
121        match self {
122            Cost::None => Cost::None,
123            Cost::Unverified => Cost::Unverified,
124            Cost::Fixed(money) => Cost::Fixed(money.add_vat(country)),
125            Cost::Fuses(items) => todo!(),
126            Cost::FusesYearlyConsumption(items) => todo!(),
127            Cost::FuseRange(items) => todo!(),
128        }
129    }
130
131    pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
132        match self {
133            Cost::FusesYearlyConsumption(items) => items
134                .iter()
135                .filter(|(fsize, _, _)| *fsize == fuse_size)
136                .any(|(_, yearly_consumption, _)| yearly_consumption.is_some()),
137            _ => false,
138        }
139    }
140}
141
142#[derive(Debug, Clone, Copy, Serialize)]
143#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
144pub struct CostPeriods {
145    periods: &'static [CostPeriod],
146}
147
148impl CostPeriods {
149    pub(super) const fn new(periods: &'static [CostPeriod]) -> Self {
150        Self { periods }
151    }
152
153    pub(super) fn iter(&self) -> Iter<'_, CostPeriod> {
154        self.periods.iter()
155    }
156
157    pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
158        self.periods
159            .iter()
160            .any(|cp| cp.is_yearly_consumption_based(fuse_size))
161    }
162}
163
164/// Like CostPeriods, but with costs being simple Money objects
165#[derive(Debug, Clone, Serialize)]
166#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
167pub struct CostPeriodsSimple {
168    periods: Vec<CostPeriodSimple>,
169}
170
171impl CostPeriodsSimple {
172    pub(crate) fn new(
173        periods: CostPeriods,
174        fuse_size: u16,
175        yearly_consumption: u32,
176        language: Language,
177    ) -> Self {
178        Self {
179            periods: periods
180                .periods
181                .iter()
182                .flat_map(|period| {
183                    CostPeriodSimple::new(period, fuse_size, yearly_consumption, language)
184                })
185                .collect(),
186        }
187    }
188}
189
190#[derive(Debug, Clone, Serialize)]
191#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
192pub(super) struct CostPeriod {
193    cost: Cost,
194    load: LoadType,
195    #[serde(serialize_with = "helpers::skip_nones")]
196    include: [Option<Include>; 2],
197    #[serde(serialize_with = "helpers::skip_nones")]
198    exclude: [Option<Exclude>; 2],
199    /// Divide kw by this amount during this period
200    divide_kw_by: u8,
201}
202
203/// Like CostPeriod, but with cost being a simple Money object
204#[derive(Debug, Clone, Serialize)]
205#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
206pub(super) struct CostPeriodSimple {
207    cost: Money,
208    load: LoadType,
209    include: Vec<Include>,
210    exclude: Vec<Exclude>,
211    /// Divide kw by this amount during this period
212    divide_kw_by: u8,
213    info: String,
214}
215
216impl CostPeriodSimple {
217    fn new(
218        period: &CostPeriod,
219        fuse_size: u16,
220        yearly_consumption: u32,
221        language: Language,
222    ) -> Option<Self> {
223        let cost = period.cost().cost_for(fuse_size, yearly_consumption)?;
224        Some(
225            Self {
226                cost,
227                load: period.load,
228                include: period.include.into_iter().flatten().collect(),
229                exclude: period.exclude.into_iter().flatten().collect(),
230                divide_kw_by: period.divide_kw_by,
231                info: Default::default(),
232            }
233            .add_info(language),
234        )
235    }
236
237    fn add_info(mut self, language: Language) -> Self {
238        match language {
239            Language::En => todo!(),
240            Language::Sv => {
241                let mut infos = Vec::new();
242                for include in &self.include {
243                    infos.push(include.translate(language));
244                }
245                for exclude in &self.exclude {
246                    infos.push(exclude.translate(language).into());
247                }
248                self.info = infos.join(", ");
249            }
250        }
251        self
252    }
253}
254
255impl CostPeriod {
256    pub(super) const fn builder() -> CostPeriodBuilder {
257        CostPeriodBuilder::new()
258    }
259
260    pub const fn cost(&self) -> Cost {
261        self.cost
262    }
263
264    pub const fn load(&self) -> LoadType {
265        self.load
266    }
267
268    pub fn matches(&self, _timestamp: DateTime<Tz>) -> bool {
269        for _period_type in self.include_period_types() {
270            // TODO: self-contain PeriodType, i.e. WinterNights becomes Months::new() + Hours::new()
271            // period_type.matches(timestamp)
272        }
273        todo!()
274    }
275
276    fn include_period_types(&self) -> Vec<Include> {
277        self.include.iter().flatten().copied().collect()
278    }
279
280    fn exclude_period_types(&self) -> Vec<Exclude> {
281        self.exclude.iter().flatten().copied().collect()
282    }
283
284    fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
285        self.cost.is_yearly_consumption_based(fuse_size)
286    }
287}
288
289#[derive(Debug, Clone, Copy, Serialize)]
290#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
291pub enum LoadType {
292    /// Base load. Always counts
293    Base,
294    /// Low load period. Commonly counts during night hours and the summer half of the year
295    Low,
296    /// High load period. Commonly counts during daytime hours and the winter half of the year
297    High,
298}
299
300pub(super) use LoadType::*;
301
302#[derive(Clone)]
303pub(super) struct CostPeriodBuilder {
304    cost: Cost,
305    load: Option<LoadType>,
306    include: [Option<Include>; 2],
307    exclude: [Option<Exclude>; 2],
308    /// Divide kw by this amount during this period
309    divide_kw_by: u8,
310}
311
312impl CostPeriodBuilder {
313    pub(super) const fn new() -> Self {
314        Self {
315            cost: Cost::None,
316            load: None,
317            include: [None; 2],
318            exclude: [None; 2],
319            divide_kw_by: 1,
320        }
321    }
322
323    pub(super) const fn build(self) -> CostPeriod {
324        CostPeriod {
325            cost: self.cost,
326            load: self.load.expect("`load` must be specified"),
327            include: self.include,
328            exclude: self.exclude,
329            divide_kw_by: self.divide_kw_by,
330        }
331    }
332
333    pub(super) const fn cost(mut self, cost: Cost) -> Self {
334        self.cost = cost;
335        self
336    }
337
338    pub(super) const fn load(mut self, load: LoadType) -> Self {
339        self.load = Some(load);
340        self
341    }
342
343    pub(super) const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
344        self.cost = Cost::fixed(int, fract);
345        self
346    }
347
348    pub(super) const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
349        self.cost = Cost::fixed_subunit(subunit);
350        self
351    }
352
353    pub(super) const fn include(mut self, period_type: Include) -> Self {
354        let mut i = 0;
355        while i < self.include.len() {
356            if self.include[i].is_some() {
357                i += 1;
358            } else {
359                self.include[i] = Some(period_type);
360                return self;
361            }
362        }
363        panic!("Too many includes");
364    }
365
366    pub(super) const fn months(self, from: Month, to: Month) -> Self {
367        self.include(Include::Months(Months::new(from, to)))
368    }
369
370    pub(super) const fn month(self, month: Month) -> Self {
371        self.include(Include::Month(month))
372    }
373
374    pub(super) const fn hours(self, from: u8, to_inclusive: u8) -> Self {
375        self.include(Include::Hours(Hours::new(from, to_inclusive)))
376    }
377
378    pub(super) const fn exclude(mut self, period_type: Exclude) -> Self {
379        let mut i = 0;
380        while i < self.exclude.len() {
381            if self.exclude[i].is_some() {
382                i += 1;
383            } else {
384                self.exclude[i] = Some(period_type);
385                return self;
386            }
387        }
388        panic!("Too many excludes");
389    }
390
391    pub(super) const fn exclude_weekends_and_swedish_holidays(self) -> Self {
392        self.exclude_weekends().exclude(Exclude::SwedishHolidays)
393    }
394
395    pub(super) const fn exclude_weekends(self) -> Self {
396        self.exclude(Exclude::Weekends)
397    }
398
399    pub(super) const fn divide_kw_by(mut self, value: u8) -> Self {
400        self.divide_kw_by = value;
401        self
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::Cost;
408    use crate::money::Money;
409
410    #[test]
411    fn fuse_based_cost() {
412        const FUSE_BASED: Cost = Cost::fuse_range(&[
413            (16, 35, Money::new(54, 0)),
414            (35, u16::MAX, Money::new(108, 50)),
415        ]);
416        assert_eq!(FUSE_BASED.cost_for(10, 0), None);
417        assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
418        assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
419    }
420}
421
422#[derive(Debug, Clone, Copy, Serialize)]
423#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
424pub(super) enum Include {
425    Months(Months),
426    Month(Month),
427    Hours(Hours),
428}
429
430impl Include {
431    fn translate(&self, language: Language) -> String {
432        match self {
433            Include::Months(months) => months.translate(language),
434            Include::Month(month) => month.translate(language).into(),
435            Include::Hours(hours) => hours.translate(language),
436        }
437    }
438}
439
440#[derive(Debug, Clone, Copy, Serialize)]
441#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
442pub(super) enum Exclude {
443    Weekends,
444    SwedishHolidays,
445}
446
447impl Exclude {
448    pub(super) fn translate(&self, language: Language) -> &'static str {
449        match language {
450            Language::En => match self {
451                Exclude::Weekends => "Weekends",
452                Exclude::SwedishHolidays => "Swedish holidays",
453            },
454            Language::Sv => match self {
455                Exclude::Weekends => "Helg",
456                Exclude::SwedishHolidays => "Svenska helgdagar",
457            },
458        }
459    }
460}