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        let mut infos = Vec::new();
239        for include in &self.include {
240            infos.push(include.translate(language));
241        }
242        for exclude in &self.exclude {
243            infos.push(exclude.translate(language).into());
244        }
245        self.info = infos.join(", ");
246        self
247    }
248}
249
250impl CostPeriod {
251    pub(super) const fn builder() -> CostPeriodBuilder {
252        CostPeriodBuilder::new()
253    }
254
255    pub const fn cost(&self) -> Cost {
256        self.cost
257    }
258
259    pub const fn load(&self) -> LoadType {
260        self.load
261    }
262
263    pub fn matches(&self, _timestamp: DateTime<Tz>) -> bool {
264        for _period_type in self.include_period_types() {
265            // TODO: self-contain PeriodType, i.e. WinterNights becomes Months::new() + Hours::new()
266            // period_type.matches(timestamp)
267        }
268        todo!()
269    }
270
271    fn include_period_types(&self) -> Vec<Include> {
272        self.include.iter().flatten().copied().collect()
273    }
274
275    fn exclude_period_types(&self) -> Vec<Exclude> {
276        self.exclude.iter().flatten().copied().collect()
277    }
278
279    fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
280        self.cost.is_yearly_consumption_based(fuse_size)
281    }
282}
283
284#[derive(Debug, Clone, Copy, Serialize)]
285#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
286pub enum LoadType {
287    /// Base load. Always counts
288    Base,
289    /// Low load period. Commonly counts during night hours and the summer half of the year
290    Low,
291    /// High load period. Commonly counts during daytime hours and the winter half of the year
292    High,
293}
294
295pub(super) use LoadType::*;
296
297#[derive(Clone)]
298pub(super) struct CostPeriodBuilder {
299    cost: Cost,
300    load: Option<LoadType>,
301    include: [Option<Include>; 2],
302    exclude: [Option<Exclude>; 2],
303    /// Divide kw by this amount during this period
304    divide_kw_by: u8,
305}
306
307impl CostPeriodBuilder {
308    pub(super) const fn new() -> Self {
309        Self {
310            cost: Cost::None,
311            load: None,
312            include: [None; 2],
313            exclude: [None; 2],
314            divide_kw_by: 1,
315        }
316    }
317
318    pub(super) const fn build(self) -> CostPeriod {
319        CostPeriod {
320            cost: self.cost,
321            load: self.load.expect("`load` must be specified"),
322            include: self.include,
323            exclude: self.exclude,
324            divide_kw_by: self.divide_kw_by,
325        }
326    }
327
328    pub(super) const fn cost(mut self, cost: Cost) -> Self {
329        self.cost = cost;
330        self
331    }
332
333    pub(super) const fn load(mut self, load: LoadType) -> Self {
334        self.load = Some(load);
335        self
336    }
337
338    pub(super) const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
339        self.cost = Cost::fixed(int, fract);
340        self
341    }
342
343    pub(super) const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
344        self.cost = Cost::fixed_subunit(subunit);
345        self
346    }
347
348    pub(super) const fn include(mut self, period_type: Include) -> Self {
349        let mut i = 0;
350        while i < self.include.len() {
351            if self.include[i].is_some() {
352                i += 1;
353            } else {
354                self.include[i] = Some(period_type);
355                return self;
356            }
357        }
358        panic!("Too many includes");
359    }
360
361    pub(super) const fn months(self, from: Month, to: Month) -> Self {
362        self.include(Include::Months(Months::new(from, to)))
363    }
364
365    pub(super) const fn month(self, month: Month) -> Self {
366        self.include(Include::Month(month))
367    }
368
369    pub(super) const fn hours(self, from: u8, to_inclusive: u8) -> Self {
370        self.include(Include::Hours(Hours::new(from, to_inclusive)))
371    }
372
373    pub(super) const fn exclude(mut self, period_type: Exclude) -> Self {
374        let mut i = 0;
375        while i < self.exclude.len() {
376            if self.exclude[i].is_some() {
377                i += 1;
378            } else {
379                self.exclude[i] = Some(period_type);
380                return self;
381            }
382        }
383        panic!("Too many excludes");
384    }
385
386    pub(super) const fn exclude_weekends_and_swedish_holidays(self) -> Self {
387        self.exclude_weekends().exclude(Exclude::SwedishHolidays)
388    }
389
390    pub(super) const fn exclude_weekends(self) -> Self {
391        self.exclude(Exclude::Weekends)
392    }
393
394    pub(super) const fn divide_kw_by(mut self, value: u8) -> Self {
395        self.divide_kw_by = value;
396        self
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::Cost;
403    use crate::money::Money;
404
405    #[test]
406    fn fuse_based_cost() {
407        const FUSE_BASED: Cost = Cost::fuse_range(&[
408            (16, 35, Money::new(54, 0)),
409            (35, u16::MAX, Money::new(108, 50)),
410        ]);
411        assert_eq!(FUSE_BASED.cost_for(10, 0), None);
412        assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
413        assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
414    }
415}
416
417#[derive(Debug, Clone, Copy, Serialize)]
418#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
419pub(super) enum Include {
420    Months(Months),
421    Month(Month),
422    Hours(Hours),
423}
424
425impl Include {
426    fn translate(&self, language: Language) -> String {
427        match self {
428            Include::Months(months) => months.translate(language),
429            Include::Month(month) => month.translate(language).into(),
430            Include::Hours(hours) => hours.translate(language),
431        }
432    }
433}
434
435#[derive(Debug, Clone, Copy, Serialize)]
436#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
437pub(super) enum Exclude {
438    Weekends,
439    SwedishHolidays,
440}
441
442impl Exclude {
443    pub(super) fn translate(&self, language: Language) -> &'static str {
444        match language {
445            Language::En => match self {
446                Exclude::Weekends => "Weekends",
447                Exclude::SwedishHolidays => "Swedish holidays",
448            },
449            Language::Sv => match self {
450                Exclude::Weekends => "Helg",
451                Exclude::SwedishHolidays => "Svenska helgdagar",
452            },
453        }
454    }
455}