flix_model/
numbers.rs

1//! This module contains season and episode numbers and related errors
2
3use core::ops::RangeInclusive;
4use std::collections::HashSet;
5
6/// Type alias for representing season numbers
7pub type SeasonNumber = u32;
8/// Type alias for representing episode numbers
9pub type EpisodeNumber = u32;
10
11/// Potential errors when building EpisodeNumbers
12#[derive(Debug, thiserror::Error)]
13pub enum Error {
14	/// There are no episodes
15	#[error("zero episodes")]
16	Zero,
17	/// There are gaps in the episodes
18	#[error("noncontiguous episodes")]
19	Noncontiguous,
20}
21
22/// A wrapper for handling single and multi-episode entries
23#[derive(Debug)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct EpisodeNumbers(RangeInclusive<EpisodeNumber>);
26
27impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
28	type Error = Error;
29
30	fn try_from(value: &[EpisodeNumber]) -> Result<Self, Self::Error> {
31		match value {
32			[] => Err(Error::Zero),
33			[n] => Ok(Self(*n..=*n)),
34			_ => {
35				// min and max will always exist
36				let min = value.iter().copied().min().unwrap_or_default();
37				let max = value.iter().copied().max().unwrap_or_default();
38				let len = value.len();
39
40				if usize::try_from(max.saturating_sub(min).saturating_add(1)) != Ok(len) {
41					return Err(Error::Noncontiguous);
42				}
43
44				let set: HashSet<_> = value.iter().copied().collect();
45				if set.len() != len {
46					return Err(Error::Noncontiguous);
47				}
48
49				Ok(Self(min..=max))
50			}
51		}
52	}
53}
54
55impl EpisodeNumbers {
56	/// Get the range of episodes
57	pub fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
58		&self.0
59	}
60}