flix_model/
numbers.rs

1//! This module contains season and episode numbers and related errors
2
3use core::fmt;
4use core::ops::RangeInclusive;
5use core::str::FromStr;
6use std::collections::HashSet;
7
8use seamantic::sea_orm;
9
10/// Newtype for representing season numbers
11#[derive(
12	Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
13)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(transparent))]
16#[repr(transparent)]
17pub struct SeasonNumber(u32);
18
19impl SeasonNumber {
20	/// Create a `SeasonNumber` from an integer
21	pub fn new(value: u32) -> Self {
22		Self(value)
23	}
24}
25
26impl fmt::Display for SeasonNumber {
27	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28		self.0.fmt(f)
29	}
30}
31
32impl FromStr for SeasonNumber {
33	type Err = <u32 as FromStr>::Err;
34
35	fn from_str(s: &str) -> Result<Self, Self::Err> {
36		u32::from_str(s).map(Self)
37	}
38}
39
40/// Newtype for representing episode numbers
41#[derive(
42	Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
43)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[cfg_attr(feature = "serde", serde(transparent))]
46#[repr(transparent)]
47pub struct EpisodeNumber(u32);
48
49impl EpisodeNumber {
50	/// Create an `EpisodeNumber` from an integer
51	pub fn new(value: u32) -> Self {
52		Self(value)
53	}
54
55	/// Get the underlying value
56	pub fn into_inner(self) -> u32 {
57		self.0
58	}
59}
60
61impl fmt::Display for EpisodeNumber {
62	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63		self.0.fmt(f)
64	}
65}
66
67impl FromStr for EpisodeNumber {
68	type Err = <u32 as FromStr>::Err;
69
70	fn from_str(s: &str) -> Result<Self, Self::Err> {
71		u32::from_str(s).map(Self)
72	}
73}
74
75/// Potential errors when building EpisodeNumbers
76#[derive(Debug, thiserror::Error)]
77pub enum Error {
78	/// There are no episodes
79	#[error("zero episodes")]
80	Zero,
81	/// There are gaps in the episodes
82	#[error("noncontiguous episodes")]
83	Noncontiguous,
84}
85
86/// A wrapper for handling single and multi-episode entries
87#[derive(Debug, Clone, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct EpisodeNumbers(RangeInclusive<EpisodeNumber>);
90
91impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
92	type Error = Error;
93
94	fn try_from(value: &[EpisodeNumber]) -> Result<Self, Self::Error> {
95		match value {
96			[] => Err(Error::Zero),
97			[n] => Ok(Self(*n..=*n)),
98			_ => {
99				// min and max will always exist
100				let min = value.iter().copied().min().unwrap_or_default();
101				let max = value.iter().copied().max().unwrap_or_default();
102				let len = value.len();
103
104				if usize::try_from(max.0.saturating_sub(min.0).saturating_add(1)) != Ok(len) {
105					return Err(Error::Noncontiguous);
106				}
107
108				let set: HashSet<_> = value.iter().copied().collect();
109				if set.len() != len {
110					return Err(Error::Noncontiguous);
111				}
112
113				Ok(Self(min..=max))
114			}
115		}
116	}
117}
118
119impl EpisodeNumbers {
120	/// Create an [EpisodeNumbers] from a starting number and a count.
121	/// `count` should be zero for single episodes.
122	pub fn new(start: EpisodeNumber, count: u8) -> Self {
123		Self(start..=EpisodeNumber(start.0.saturating_add(count.into())))
124	}
125
126	/// Get the range of episodes
127	pub fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
128		&self.0
129	}
130
131	/// Render this [EpisodeNumbers] as a range. If only one episode is
132	/// is present it renders as `01`, if multiple it renders as `01-02`
133	pub fn range_string(&self) -> String {
134		let start = self.0.start();
135		let end = self.0.end();
136
137		if start == end {
138			format!("{:02}", start)
139		} else {
140			format!("{:02}-{:02}", start, end)
141		}
142	}
143}