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 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 }
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,
294 Low,
296 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: 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}