1use crate::id::{define_id_type, IDCollection};
6use crate::units::{Dimensionless, Year};
7use anyhow::{Context, Result};
8use indexmap::{IndexMap, IndexSet};
9use itertools::Itertools;
10use serde::de::Error;
11use serde::{Deserialize, Serialize};
12use serde_string_enum::DeserializeLabeledStringEnum;
13use std::fmt::Display;
14use std::iter;
15
16define_id_type! {Season}
17define_id_type! {TimeOfDay}
18
19#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
21pub struct TimeSliceID {
22 pub season: Season,
24 pub time_of_day: TimeOfDay,
26}
27
28#[cfg(test)]
30impl From<&str> for TimeSliceID {
31 fn from(value: &str) -> Self {
32 let (season, time_of_day) = value
33 .split(".")
34 .collect_tuple()
35 .expect("Time slice not in form season.time_of_day");
36 TimeSliceID {
37 season: season.into(),
38 time_of_day: time_of_day.into(),
39 }
40 }
41}
42
43impl Display for TimeSliceID {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}.{}", self.season, self.time_of_day)
46 }
47}
48
49impl<'de> Deserialize<'de> for TimeSliceID {
50 fn deserialize<D>(deserialiser: D) -> std::result::Result<Self, D::Error>
51 where
52 D: serde::Deserializer<'de>,
53 {
54 let s: &str = Deserialize::deserialize(deserialiser)?;
55 let (season, time_of_day) = s.split(".").collect_tuple().ok_or_else(|| {
56 D::Error::custom(format!(
57 "Invalid input '{s}': Should be in form season.time_of_day"
58 ))
59 })?;
60 Ok(Self {
61 season: season.into(),
62 time_of_day: time_of_day.into(),
63 })
64 }
65}
66
67impl Serialize for TimeSliceID {
68 fn serialize<S>(&self, serialiser: S) -> std::result::Result<S::Ok, S::Error>
69 where
70 S: serde::Serializer,
71 {
72 serialiser.collect_str(self)
73 }
74}
75
76#[derive(PartialEq, Eq, Hash, Clone, Debug)]
78pub enum TimeSliceSelection {
79 Annual,
81 Season(Season),
83 Single(TimeSliceID),
85}
86
87impl TimeSliceSelection {
88 pub fn level(&self) -> TimeSliceLevel {
90 match self {
91 Self::Annual => TimeSliceLevel::Annual,
92 Self::Season(_) => TimeSliceLevel::Season,
93 Self::Single(_) => TimeSliceLevel::DayNight,
94 }
95 }
96
97 pub fn iter<'a>(
99 &'a self,
100 time_slice_info: &'a TimeSliceInfo,
101 ) -> Box<dyn Iterator<Item = (&'a TimeSliceID, Year)> + 'a> {
102 let ts_info = time_slice_info;
103 match self {
104 Self::Annual => Box::new(ts_info.iter()),
105 Self::Season(season) => {
106 Box::new(ts_info.iter().filter(move |(ts, _)| ts.season == *season))
107 }
108 Self::Single(ts) => Box::new(iter::once((ts, *ts_info.time_slices.get(ts).unwrap()))),
109 }
110 }
111
112 pub fn iter_at_level<'a>(
123 &'a self,
124 time_slice_info: &'a TimeSliceInfo,
125 level: TimeSliceLevel,
126 ) -> Option<Box<dyn Iterator<Item = (Self, Year)> + 'a>> {
127 if level > self.level() {
128 return None;
129 }
130
131 let ts_info = time_slice_info;
132 let iter: Box<dyn Iterator<Item = _>> = match self {
133 Self::Annual => match level {
134 TimeSliceLevel::Annual => Box::new(iter::once((Self::Annual, Year(1.0)))),
135 TimeSliceLevel::Season => Box::new(
136 ts_info
137 .seasons
138 .iter()
139 .map(|(season, duration)| (season.clone().into(), *duration)),
140 ),
141 TimeSliceLevel::DayNight => Box::new(
142 ts_info
143 .time_slices
144 .iter()
145 .map(|(ts, duration)| (ts.clone().into(), *duration)),
146 ),
147 },
148 Self::Season(season) => match level {
149 TimeSliceLevel::Season => Box::new(iter::once((
150 self.clone(),
151 *ts_info.seasons.get(season).unwrap(),
152 ))),
153 TimeSliceLevel::DayNight => Box::new(
154 ts_info
155 .time_slices
156 .iter()
157 .filter(move |(ts, _)| &ts.season == season)
158 .map(|(ts, duration)| (ts.clone().into(), *duration)),
159 ),
160 _ => unreachable!(),
161 },
162 Self::Single(time_slice) => Box::new(iter::once((
163 time_slice.clone().into(),
164 *ts_info.time_slices.get(time_slice).unwrap(),
165 ))),
166 };
167
168 Some(iter)
169 }
170}
171
172impl From<TimeSliceID> for TimeSliceSelection {
173 fn from(value: TimeSliceID) -> Self {
174 Self::Single(value)
175 }
176}
177
178impl From<Season> for TimeSliceSelection {
179 fn from(value: Season) -> Self {
180 Self::Season(value)
181 }
182}
183
184impl Display for TimeSliceSelection {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Self::Annual => write!(f, "annual"),
188 Self::Season(season) => write!(f, "{season}"),
189 Self::Single(ts) => write!(f, "{ts}"),
190 }
191 }
192}
193
194#[derive(PartialEq, PartialOrd, Copy, Clone, Debug, DeserializeLabeledStringEnum)]
196pub enum TimeSliceLevel {
197 #[string = "daynight"]
199 DayNight,
200 #[string = "season"]
202 Season,
203 #[string = "annual"]
205 Annual,
206}
207
208#[derive(PartialEq, Debug)]
210pub struct TimeSliceInfo {
211 pub times_of_day: IndexSet<TimeOfDay>,
213 pub seasons: IndexMap<Season, Year>,
215 pub time_slices: IndexMap<TimeSliceID, Year>,
217}
218
219impl Default for TimeSliceInfo {
220 fn default() -> Self {
222 let id = TimeSliceID {
223 season: "all-year".into(),
224 time_of_day: "all-day".into(),
225 };
226 let time_slices = [(id.clone(), Year(1.0))].into_iter().collect();
227
228 Self {
229 seasons: iter::once((id.season, Year(1.0))).collect(),
230 times_of_day: iter::once(id.time_of_day).collect(),
231 time_slices,
232 }
233 }
234}
235
236impl TimeSliceInfo {
237 pub fn get_time_slice_id_from_str(&self, time_slice: &str) -> Result<TimeSliceID> {
241 let (season, time_of_day) = time_slice
242 .split('.')
243 .collect_tuple()
244 .context("Time slice must be in the form season.time_of_day")?;
245 let season = self
246 .seasons
247 .get_id(season)
248 .with_context(|| format!("{season} is not a known season"))?;
249 let time_of_day = self
250 .times_of_day
251 .get_id(time_of_day)
252 .with_context(|| format!("{time_of_day} is not a known time of day"))?;
253
254 Ok(TimeSliceID {
255 season: season.clone(),
256 time_of_day: time_of_day.clone(),
257 })
258 }
259
260 pub fn get_selection(&self, time_slice: &str) -> Result<TimeSliceSelection> {
264 if time_slice.eq_ignore_ascii_case("annual") {
265 Ok(TimeSliceSelection::Annual)
266 } else if time_slice.contains('.') {
267 let time_slice = self.get_time_slice_id_from_str(time_slice)?;
268 Ok(TimeSliceSelection::Single(time_slice))
269 } else {
270 let season = self
271 .seasons
272 .get_id(time_slice)
273 .with_context(|| format!("'{time_slice}' is not a valid season"))?
274 .clone();
275 Ok(TimeSliceSelection::Season(season))
276 }
277 }
278
279 pub fn iter_ids(&self) -> indexmap::map::Keys<TimeSliceID, Year> {
281 self.time_slices.keys()
282 }
283
284 pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, Year)> {
286 self.time_slices
287 .iter()
288 .map(|(ts, duration)| (ts, *duration))
289 }
290
291 pub fn iter_selections_at_level(
296 &self,
297 level: TimeSliceLevel,
298 ) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {
299 match level {
300 TimeSliceLevel::Annual => Box::new(iter::once(TimeSliceSelection::Annual)),
301 TimeSliceLevel::Season => {
302 Box::new(self.seasons.keys().cloned().map(TimeSliceSelection::Season))
303 }
304 TimeSliceLevel::DayNight => {
305 Box::new(self.iter_ids().cloned().map(TimeSliceSelection::Single))
306 }
307 }
308 }
309
310 pub fn iter_selection_share<'a>(
322 &'a self,
323 selection: &'a TimeSliceSelection,
324 level: TimeSliceLevel,
325 ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)>> {
326 let selections = selection.iter_at_level(self, level)?.collect_vec();
328
329 let total_duration: Year = selections.iter().map(|(_, duration)| *duration).sum();
331
332 let iter = selections
334 .into_iter()
335 .map(move |(selection, duration)| (selection, duration / total_duration));
336 Some(iter)
337 }
338
339 pub fn calculate_share<'a>(
353 &'a self,
354 selection: &'a TimeSliceSelection,
355 level: TimeSliceLevel,
356 value: Dimensionless,
357 ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)>> {
358 let iter = self
359 .iter_selection_share(selection, level)?
360 .map(move |(selection, share)| (selection, value * share));
361 Some(iter)
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::units::UnitType;
369 use itertools::assert_equal;
370 use rstest::{fixture, rstest};
371
372 #[fixture]
373 fn time_slices1() -> [TimeSliceID; 2] {
374 [
375 TimeSliceID {
376 season: "winter".into(),
377 time_of_day: "day".into(),
378 },
379 TimeSliceID {
380 season: "summer".into(),
381 time_of_day: "night".into(),
382 },
383 ]
384 }
385
386 #[fixture]
387 fn time_slice_info1(time_slices1: [TimeSliceID; 2]) -> TimeSliceInfo {
388 TimeSliceInfo {
389 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
390 .into_iter()
391 .collect(),
392 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
393 time_slices: time_slices1.map(|ts| (ts, Year(0.5))).into_iter().collect(),
394 }
395 }
396
397 #[fixture]
398 fn time_slice_info2() -> TimeSliceInfo {
399 let time_slices = [
400 TimeSliceID {
401 season: "winter".into(),
402 time_of_day: "day".into(),
403 },
404 TimeSliceID {
405 season: "winter".into(),
406 time_of_day: "night".into(),
407 },
408 TimeSliceID {
409 season: "summer".into(),
410 time_of_day: "day".into(),
411 },
412 TimeSliceID {
413 season: "summer".into(),
414 time_of_day: "night".into(),
415 },
416 ];
417 TimeSliceInfo {
418 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
419 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
420 .into_iter()
421 .collect(),
422 time_slices: time_slices
423 .iter()
424 .map(|ts| (ts.clone(), Year(0.25)))
425 .collect(),
426 }
427 }
428
429 #[rstest]
430 fn test_ts_selection_iter_annual(
431 time_slice_info1: TimeSliceInfo,
432 time_slices1: [TimeSliceID; 2],
433 ) {
434 assert_equal(
435 TimeSliceSelection::Annual.iter(&time_slice_info1),
436 time_slices1.iter().map(|ts| (ts, Year(0.5))),
437 );
438 }
439
440 #[rstest]
441 fn test_ts_selection_iter_season(
442 time_slice_info1: TimeSliceInfo,
443 time_slices1: [TimeSliceID; 2],
444 ) {
445 assert_equal(
446 TimeSliceSelection::Season("winter".into()).iter(&time_slice_info1),
447 iter::once((&time_slices1[0], Year(0.5))),
448 );
449 }
450
451 #[rstest]
452 fn test_ts_selection_iter_single(
453 time_slice_info1: TimeSliceInfo,
454 time_slices1: [TimeSliceID; 2],
455 ) {
456 let ts = time_slice_info1
457 .get_time_slice_id_from_str("summer.night")
458 .unwrap();
459 assert_equal(
460 TimeSliceSelection::Single(ts).iter(&time_slice_info1),
461 iter::once((&time_slices1[1], Year(0.5))),
462 );
463 }
464
465 fn assert_selection_equal<I, T>(actual: Option<I>, expected: Option<Vec<(&str, T)>>)
466 where
467 T: UnitType,
468 I: Iterator<Item = (TimeSliceSelection, T)>,
469 {
470 let Some(actual) = actual else {
471 assert!(expected.is_none());
472 return;
473 };
474
475 let ts_info = time_slice_info2();
476 let expected = expected
477 .unwrap()
478 .into_iter()
479 .map(move |(sel, frac)| (ts_info.get_selection(sel).unwrap(), frac));
480 assert_equal(actual, expected);
481 }
482
483 #[rstest]
484 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Year(1.0))]))]
485 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Year(0.5)), ("summer", Year(0.5))]))]
486 #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
487 Some(vec![("winter.day", Year(0.25)), ("winter.night", Year(0.25)), ("summer.day", Year(0.25)), ("summer.night", Year(0.25))]))]
488 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
489 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Year(0.5))]))]
490 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
491 Some(vec![("winter.day", Year(0.25)), ("winter.night", Year(0.25))]))]
492 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
493 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
494 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Year(0.25))]))]
495 fn test_ts_selection_iter_at_level(
496 time_slice_info2: TimeSliceInfo,
497 #[case] selection: TimeSliceSelection,
498 #[case] level: TimeSliceLevel,
499 #[case] expected: Option<Vec<(&str, Year)>>,
500 ) {
501 let actual = selection.iter_at_level(&time_slice_info2, level);
502 assert_selection_equal(actual, expected);
503 }
504
505 #[rstest]
506 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Dimensionless(8.0))]))]
507 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(4.0)), ("summer", Dimensionless(4.0))]))]
508 #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
509 Some(vec![("winter.day", Dimensionless(2.0)), ("winter.night", Dimensionless(2.0)), ("summer.day", Dimensionless(2.0)), ("summer.night", Dimensionless(2.0))]))]
510 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
511 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(8.0))]))]
512 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
513 Some(vec![("winter.day", Dimensionless(4.0)), ("winter.night", Dimensionless(4.0))]))]
514 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
515 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
516 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Dimensionless(8.0))]))]
517 fn test_calculate_share(
518 time_slice_info2: TimeSliceInfo,
519 #[case] selection: TimeSliceSelection,
520 #[case] level: TimeSliceLevel,
521 #[case] expected: Option<Vec<(&str, Dimensionless)>>,
522 ) {
523 let actual = time_slice_info2.calculate_share(&selection, level, Dimensionless(8.0));
524 assert_selection_equal(actual, expected);
525 }
526}