1use anyhow::{Context, Result};
6use indexmap::{IndexMap, IndexSet};
7use itertools::Itertools;
8use serde_string_enum::DeserializeLabeledStringEnum;
9use std::fmt::Display;
10use std::iter;
11use std::rc::Rc;
12
13#[derive(Hash, Eq, PartialEq, Clone, Debug)]
15pub struct TimeSliceID {
16 pub season: Rc<str>,
18 pub time_of_day: Rc<str>,
20}
21
22impl Display for TimeSliceID {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "{}.{}", self.season, self.time_of_day)
25 }
26}
27
28#[derive(PartialEq, Clone, Debug)]
30pub enum TimeSliceSelection {
31 Annual,
33 Season(Rc<str>),
35 Single(TimeSliceID),
37}
38
39#[derive(PartialEq, Copy, Clone, Debug, DeserializeLabeledStringEnum)]
41pub enum TimeSliceLevel {
42 #[string = "annual"]
44 Annual,
45 #[string = "season"]
47 Season,
48 #[string = "daynight"]
50 DayNight,
51}
52
53#[derive(PartialEq, Debug)]
55pub struct TimeSliceInfo {
56 pub seasons: IndexSet<Rc<str>>,
58 pub times_of_day: IndexSet<Rc<str>>,
60 pub fractions: IndexMap<TimeSliceID, f64>,
62}
63
64impl Default for TimeSliceInfo {
65 fn default() -> Self {
67 let id = TimeSliceID {
68 season: "all-year".into(),
69 time_of_day: "all-day".into(),
70 };
71 let fractions = [(id.clone(), 1.0)].into_iter().collect();
72
73 Self {
74 seasons: [id.season].into_iter().collect(),
75 times_of_day: [id.time_of_day].into_iter().collect(),
76 fractions,
77 }
78 }
79}
80
81impl TimeSliceInfo {
82 pub fn get_time_slice_id_from_str(&self, time_slice: &str) -> Result<TimeSliceID> {
86 let (season, time_of_day) = time_slice
87 .split('.')
88 .collect_tuple()
89 .context("Time slice must be in the form season.time_of_day")?;
90 let season = self
91 .seasons
92 .iter()
93 .find(|item| item.eq_ignore_ascii_case(season))
94 .with_context(|| format!("{} is not a known season", season))?;
95 let time_of_day = self
96 .times_of_day
97 .iter()
98 .find(|item| item.eq_ignore_ascii_case(time_of_day))
99 .with_context(|| format!("{} is not a known time of day", time_of_day))?;
100
101 Ok(TimeSliceID {
102 season: Rc::clone(season),
103 time_of_day: Rc::clone(time_of_day),
104 })
105 }
106
107 pub fn get_selection(&self, time_slice: &str) -> Result<TimeSliceSelection> {
111 if time_slice.is_empty() || time_slice.eq_ignore_ascii_case("annual") {
112 Ok(TimeSliceSelection::Annual)
113 } else if time_slice.contains('.') {
114 let time_slice = self.get_time_slice_id_from_str(time_slice)?;
115 Ok(TimeSliceSelection::Single(time_slice))
116 } else {
117 let season = self
118 .seasons
119 .get(time_slice)
120 .with_context(|| format!("'{time_slice}' is not a valid season"))?
121 .clone();
122 Ok(TimeSliceSelection::Season(season))
123 }
124 }
125
126 pub fn iter_ids(&self) -> impl Iterator<Item = &TimeSliceID> {
131 self.fractions.keys()
132 }
133
134 pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, f64)> {
139 self.fractions.iter().map(|(ts, fraction)| (ts, *fraction))
140 }
141
142 pub fn iter_selection<'a>(
147 &'a self,
148 selection: &'a TimeSliceSelection,
149 ) -> Box<dyn Iterator<Item = (&'a TimeSliceID, f64)> + 'a> {
150 match selection {
151 TimeSliceSelection::Annual => Box::new(self.iter()),
152 TimeSliceSelection::Season(season) => {
153 Box::new(self.iter().filter(move |(ts, _)| ts.season == *season))
154 }
155 TimeSliceSelection::Single(ts) => {
156 Box::new(iter::once((ts, *self.fractions.get(ts).unwrap())))
157 }
158 }
159 }
160
161 pub fn iter_selections_for_level(
166 &self,
167 level: TimeSliceLevel,
168 ) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {
169 match level {
170 TimeSliceLevel::Annual => Box::new(iter::once(TimeSliceSelection::Annual)),
171 TimeSliceLevel::Season => {
172 Box::new(self.seasons.iter().cloned().map(TimeSliceSelection::Season))
173 }
174 TimeSliceLevel::DayNight => {
175 Box::new(self.iter_ids().cloned().map(TimeSliceSelection::Single))
176 }
177 }
178 }
179
180 pub fn iterate_selection_share<'a>(
193 &'a self,
194 selection: &'a TimeSliceSelection,
195 ) -> impl Iterator<Item = (&'a TimeSliceID, f64)> {
196 let time_slices = self.iter_selection(selection).collect_vec();
198
199 let time_total: f64 = time_slices.iter().map(|(_, fraction)| *fraction).sum();
201
202 time_slices
204 .into_iter()
205 .map(move |(ts, time_fraction)| (ts, time_fraction / time_total))
206 }
207
208 pub fn calculate_share<'a>(
222 &'a self,
223 selection: &'a TimeSliceSelection,
224 value: f64,
225 ) -> impl Iterator<Item = (&'a TimeSliceID, f64)> {
226 self.iterate_selection_share(selection)
227 .map(move |(ts, share)| (ts, value * share))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use float_cmp::assert_approx_eq;
235 use itertools::assert_equal;
236
237 #[test]
238 fn test_iter_selection() {
239 let slices = [
240 TimeSliceID {
241 season: "winter".into(),
242 time_of_day: "day".into(),
243 },
244 TimeSliceID {
245 season: "summer".into(),
246 time_of_day: "night".into(),
247 },
248 ];
249 let ts_info = TimeSliceInfo {
250 seasons: ["winter".into(), "summer".into()].into_iter().collect(),
251 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
252 fractions: [(slices[0].clone(), 0.5), (slices[1].clone(), 0.5)]
253 .into_iter()
254 .collect(),
255 };
256
257 assert_equal(
258 ts_info
259 .iter_selection(&TimeSliceSelection::Annual)
260 .map(|(ts, _)| ts),
261 slices.iter(),
262 );
263 assert_equal(
264 ts_info
265 .iter_selection(&TimeSliceSelection::Season("winter".into()))
266 .map(|(ts, _)| ts),
267 iter::once(&slices[0]),
268 );
269 let ts = ts_info.get_time_slice_id_from_str("summer.night").unwrap();
270 assert_equal(
271 ts_info
272 .iter_selection(&TimeSliceSelection::Single(ts))
273 .map(|(ts, _)| ts),
274 iter::once(&slices[1]),
275 );
276 }
277
278 #[test]
279 fn test_calculate_share() {
280 let slices = [
281 TimeSliceID {
282 season: "winter".into(),
283 time_of_day: "day".into(),
284 },
285 TimeSliceID {
286 season: "winter".into(),
287 time_of_day: "night".into(),
288 },
289 TimeSliceID {
290 season: "summer".into(),
291 time_of_day: "day".into(),
292 },
293 TimeSliceID {
294 season: "summer".into(),
295 time_of_day: "night".into(),
296 },
297 ];
298 let ts_info = TimeSliceInfo {
299 seasons: ["winter".into(), "summer".into()].into_iter().collect(),
300 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
301 fractions: slices.iter().map(|ts| (ts.clone(), 0.25)).collect(),
302 };
303
304 macro_rules! check_share {
305 ($selection:expr, $expected:expr) => {
306 let expected = $expected;
307 let actual: IndexMap<_, _> = IndexMap::from_iter(
308 ts_info
309 .calculate_share(&$selection, 8.0)
310 .map(|(ts, share)| (ts.clone(), share)),
311 );
312 assert!(actual.len() == expected.len());
313 for (k, v) in actual {
314 assert_approx_eq!(f64, v, *expected.get(&k).unwrap());
315 }
316 };
317 }
318
319 let expected: IndexMap<_, _> =
321 IndexMap::from_iter(slices.iter().map(|ts| (ts.clone(), 2.0)));
322 check_share!(TimeSliceSelection::Annual, expected);
323
324 let selection = TimeSliceSelection::Season("winter".into());
326 let expected: IndexMap<_, _> = IndexMap::from_iter(
327 ts_info
328 .iter_selection(&selection)
329 .map(|(ts, _)| (ts.clone(), 4.0)),
330 );
331 check_share!(selection, expected);
332
333 let time_slice = ts_info.get_time_slice_id_from_str("winter.day").unwrap();
335 let selection = TimeSliceSelection::Single(time_slice.clone());
336 let expected: IndexMap<_, _> = IndexMap::from_iter(iter::once((time_slice, 8.0)));
337 check_share!(selection, expected);
338 }
339}