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 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 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#[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: u8,
201}
202
203#[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: 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 }
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,
289 Low,
291 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: 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}