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,
9    defs::{Hours, Month, Months},
10    money::Money,
11};
12
13#[derive(Debug, Clone, Copy, Serialize)]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15#[serde(tag = "type", content = "value")]
16pub enum Cost {
17    None,
18    /// Cost has not been verified
19    Unverified,
20    Fixed(Money),
21    Fuses(&'static [(u16, Money)]),
22    /// Fuse size combined with a yearly energy consumption limit
23    FusesYearlyConsumption(&'static [(u16, Option<u32>, Money)]),
24    FuseRange(&'static [(u16, u16, Money)]),
25}
26
27impl Cost {
28    pub const fn is_unverified(&self) -> bool {
29        matches!(self, Self::Unverified)
30    }
31
32    pub(super) const fn fuses(values: &'static [(u16, Money)]) -> Self {
33        Self::Fuses(values)
34    }
35
36    pub(super) const fn fuse_range(ranges: &'static [(u16, u16, Money)]) -> Self {
37        Self::FuseRange(ranges)
38    }
39
40    pub(super) const fn fuses_with_yearly_consumption(
41        values: &'static [(u16, Option<u32>, Money)],
42    ) -> Cost {
43        Self::FusesYearlyConsumption(values)
44    }
45
46    pub(super) const fn fixed(int: i64, fract: u8) -> Self {
47        Self::Fixed(Money::new(int, fract))
48    }
49
50    pub(super) const fn fixed_yearly(int: i64, fract: u8) -> Self {
51        Self::Fixed(Money::new(int, fract).divide_by(12))
52    }
53
54    pub(super) const fn fixed_subunit(subunit: f64) -> Self {
55        Self::Fixed(Money::new_subunit(subunit))
56    }
57
58    pub(super) const fn divide_by(&self, by: i64) -> Self {
59        match self {
60            Self::None => Self::None,
61            Self::Unverified => Self::Unverified,
62            Self::Fixed(money) => Self::Fixed(money.divide_by(by)),
63            Self::Fuses(items) => panic!(".divide_by() is unsupported on Cost::Fuses"),
64            Self::FusesYearlyConsumption(items) => {
65                panic!(".divide_by() is unsupported on Cost::FuseRangeYearlyConsumption")
66            }
67            Self::FuseRange(items) => panic!(".divide_by() is unsupported on Cost::FuseRange"),
68        }
69    }
70
71    pub const fn cost_for(&self, fuse_size: u16, yearly_consumption: u32) -> Option<Money> {
72        match *self {
73            Cost::None => None,
74            Cost::Unverified => None,
75            Cost::Fixed(money) => Some(money),
76            Cost::Fuses(values) => {
77                let mut i = 0;
78                while i < values.len() {
79                    let (fsize, money) = values[i];
80                    if fuse_size == fsize {
81                        return Some(money);
82                    }
83                    i += 1;
84                }
85                None
86            }
87            Cost::FusesYearlyConsumption(values) => {
88                let mut i = 0;
89                while i < values.len() {
90                    let (fsize, max_consumption, money) = values[i];
91                    if fsize == fuse_size {
92                        if let Some(max_consumption) = max_consumption {
93                            if max_consumption <= yearly_consumption {
94                                return Some(money);
95                            }
96                        } else {
97                            return Some(money);
98                        }
99                    }
100                    i += 1;
101                }
102                None
103            }
104            Cost::FuseRange(ranges) => {
105                let mut i = 0;
106                while i < ranges.len() {
107                    let (min, max, money) = ranges[i];
108                    if fuse_size >= min && fuse_size <= max {
109                        return Some(money);
110                    }
111                    i += 1;
112                }
113                None
114            }
115        }
116    }
117
118    pub(crate) const fn add_vat(&self, country: Country) -> Cost {
119        let rate = match country {
120            Country::SE => 1.25,
121        };
122        match self {
123            Cost::None => Cost::None,
124            Cost::Unverified => Cost::Unverified,
125            Cost::Fixed(money) => Cost::Fixed(money.add_vat(country)),
126            Cost::Fuses(items) => todo!(),
127            Cost::FusesYearlyConsumption(items) => todo!(),
128            Cost::FuseRange(items) => todo!(),
129        }
130    }
131
132    pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
133        match self {
134            Cost::FusesYearlyConsumption(items) => items
135                .iter()
136                .filter(|(fsize, _, _)| *fsize == fuse_size)
137                .any(|(_, yearly_consumption, _)| yearly_consumption.is_some()),
138            _ => false,
139        }
140    }
141}
142
143#[derive(Debug, Clone, Copy, Serialize)]
144#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
145pub struct CostPeriods {
146    periods: &'static [CostPeriod],
147}
148
149impl CostPeriods {
150    pub(super) const fn new(periods: &'static [CostPeriod]) -> Self {
151        Self { periods }
152    }
153
154    pub(super) fn iter(&self) -> Iter<'_, CostPeriod> {
155        self.periods.iter()
156    }
157
158    pub(crate) fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
159        self.periods
160            .iter()
161            .any(|cp| cp.is_yearly_consumption_based(fuse_size))
162    }
163}
164
165/// Like CostPeriods, but with costs being simple Money objects
166#[derive(Debug, Clone, Serialize)]
167#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
168pub struct CostPeriodsSimple {
169    periods: Vec<CostPeriodSimple>,
170}
171
172impl CostPeriodsSimple {
173    pub(crate) fn new(periods: CostPeriods, fuse_size: u16, yearly_consumption: u32) -> Self {
174        Self {
175            periods: periods
176                .periods
177                .iter()
178                .map(|period| CostPeriodSimple::new(period, fuse_size, yearly_consumption))
179                .flatten()
180                .collect(),
181        }
182    }
183}
184
185#[derive(Debug, Clone, Serialize)]
186#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
187pub(super) struct CostPeriod {
188    cost: Cost,
189    load: LoadType,
190    #[serde(serialize_with = "skip_nones")]
191    include: [Option<PeriodType>; 2],
192    #[serde(serialize_with = "skip_nones")]
193    exclude: [Option<PeriodType>; 2],
194    /// Divide kw by this amount during this period
195    divide_kw_by: u8,
196}
197
198/// Like CostPeriod, but with cost being a simple Money object
199#[derive(Debug, Clone, Serialize)]
200#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
201pub(super) struct CostPeriodSimple {
202    cost: Money,
203    load: LoadType,
204    include: Vec<PeriodType>,
205    exclude: Vec<PeriodType>,
206    /// Divide kw by this amount during this period
207    divide_kw_by: u8,
208}
209
210impl CostPeriodSimple {
211    fn new(period: &CostPeriod, fuse_size: u16, yearly_consumption: u32) -> Option<Self> {
212        let Some(cost) = period.cost().cost_for(fuse_size, yearly_consumption) else {
213            return None;
214        };
215        Some(Self {
216            cost,
217            load: period.load,
218            include: period.include.into_iter().flatten().collect(),
219            exclude: period.exclude.into_iter().flatten().collect(),
220            divide_kw_by: period.divide_kw_by,
221        })
222    }
223}
224
225impl CostPeriod {
226    pub(super) const fn builder() -> CostPeriodBuilder {
227        CostPeriodBuilder::new()
228    }
229
230    pub const fn cost(&self) -> Cost {
231        self.cost
232    }
233
234    pub const fn load(&self) -> LoadType {
235        self.load
236    }
237
238    pub fn matches(&self, _timestamp: DateTime<Tz>) -> bool {
239        for _period_type in self.include_period_types() {
240            // TODO: self-contain PeriodType, i.e. WinterNights becomes Months::new() + Hours::new()
241            // period_type.matches(timestamp)
242        }
243        todo!()
244    }
245
246    fn include_period_types(&self) -> Vec<PeriodType> {
247        self.include.iter().flatten().copied().collect()
248    }
249
250    fn exclude_period_types(&self) -> Vec<PeriodType> {
251        self.exclude.iter().flatten().copied().collect()
252    }
253
254    fn is_yearly_consumption_based(&self, fuse_size: u16) -> bool {
255        self.cost.is_yearly_consumption_based(fuse_size)
256    }
257}
258
259#[derive(Debug, Clone, Copy, Serialize)]
260#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
261pub enum LoadType {
262    /// Base load. Always counts
263    Base,
264    /// Low load period. Commonly counts during night hours and the summer half of the year
265    Low,
266    /// High load period. Commonly counts during daytime hours and the winter half of the year
267    High,
268}
269
270pub(super) use LoadType::*;
271
272#[derive(Clone)]
273pub(super) struct CostPeriodBuilder {
274    cost: Cost,
275    load: Option<LoadType>,
276    include: [Option<PeriodType>; 2],
277    exclude: [Option<PeriodType>; 2],
278    /// Divide kw by this amount during this period
279    divide_kw_by: u8,
280}
281
282impl CostPeriodBuilder {
283    pub(super) const fn new() -> Self {
284        Self {
285            cost: Cost::None,
286            load: None,
287            include: [None; 2],
288            exclude: [None; 2],
289            divide_kw_by: 1,
290        }
291    }
292
293    pub(super) const fn build(self) -> CostPeriod {
294        CostPeriod {
295            cost: self.cost,
296            load: self.load.expect("`load` must be specified"),
297            include: self.include,
298            exclude: self.exclude,
299            divide_kw_by: self.divide_kw_by,
300        }
301    }
302
303    pub(super) const fn cost(mut self, cost: Cost) -> Self {
304        self.cost = cost;
305        self
306    }
307
308    pub(super) const fn load(mut self, load: LoadType) -> Self {
309        self.load = Some(load);
310        self
311    }
312
313    pub(super) const fn fixed_cost(mut self, int: i64, fract: u8) -> Self {
314        self.cost = Cost::fixed(int, fract);
315        self
316    }
317
318    pub(super) const fn fixed_cost_subunit(mut self, subunit: f64) -> Self {
319        self.cost = Cost::fixed_subunit(subunit);
320        self
321    }
322
323    pub(super) const fn include(mut self, period_type: PeriodType) -> Self {
324        let mut i = 0;
325        while i < self.include.len() {
326            if self.include[i].is_some() {
327                i += 1;
328            } else {
329                self.include[i] = Some(period_type);
330                return self;
331            }
332        }
333        panic!("Too many includes");
334    }
335
336    pub(super) const fn months(self, from: Month, to: Month) -> Self {
337        self.include(PeriodType::Months(Months::new(from, to)))
338    }
339
340    pub(super) const fn month(self, month: Month) -> Self {
341        self.include(PeriodType::Month(month))
342    }
343
344    pub(super) const fn hours(self, from: u8, to_inclusive: u8) -> Self {
345        self.include(PeriodType::Hours(Hours::new(from, to_inclusive)))
346    }
347
348    pub(super) const fn exclude(mut self, period_type: PeriodType) -> Self {
349        let mut i = 0;
350        while i < self.exclude.len() {
351            if self.exclude[i].is_some() {
352                i += 1;
353            } else {
354                self.exclude[i] = Some(period_type);
355                return self;
356            }
357        }
358        panic!("Too many excludes");
359    }
360
361    pub(super) const fn exclude_weekends_and_swedish_holidays(self) -> Self {
362        self.exclude_weekends().exclude(PeriodType::SwedishHolidays)
363    }
364
365    pub(super) const fn exclude_weekends(self) -> Self {
366        self.exclude(PeriodType::Weekends)
367    }
368
369    pub(super) const fn divide_kw_by(mut self, value: u8) -> Self {
370        self.divide_kw_by = value;
371        self
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::Cost;
378    use crate::money::Money;
379
380    #[test]
381    fn fuse_based_cost() {
382        const FUSE_BASED: Cost = Cost::fuse_range(&[
383            (16, 35, Money::new(54, 0)),
384            (35, u16::MAX, Money::new(108, 50)),
385        ]);
386        assert_eq!(FUSE_BASED.cost_for(10, 0), None);
387        assert_eq!(FUSE_BASED.cost_for(25, 0), Some(Money::new(54, 0)));
388        assert_eq!(FUSE_BASED.cost_for(200, 0), Some(Money::new(108, 50)));
389    }
390}
391
392#[derive(Debug, Clone, Copy, Serialize)]
393#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
394#[serde(tag = "type", content = "value")]
395pub(super) enum PeriodType {
396    Months(Months),
397    Month(Month),
398    Hours(Hours),
399    Weekends,
400    SwedishHolidays,
401}
402
403fn skip_nones<S>(items: &[Option<PeriodType>; 2], serializer: S) -> Result<S::Ok, S::Error>
404where
405    S: Serializer,
406{
407    let filtered: Vec<_> = items.iter().filter_map(|x| x.as_ref()).collect();
408    let mut seq = serializer.serialize_seq(Some(filtered.len()))?;
409    for item in filtered {
410        seq.serialize_element(item)?;
411    }
412    seq.end()
413}