1use core::fmt;
4use core::ops::RangeInclusive;
5use core::str::FromStr;
6use std::collections::HashSet;
7
8use seamantic::sea_orm;
9
10#[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 #[inline]
22 #[must_use]
23 pub const fn new(value: u32) -> Self {
24 Self(value)
25 }
26}
27
28impl fmt::Display for SeasonNumber {
29 #[inline]
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 self.0.fmt(f)
32 }
33}
34
35impl FromStr for SeasonNumber {
36 type Err = <u32 as FromStr>::Err;
37
38 #[inline]
39 fn from_str(s: &str) -> Result<Self, Self::Err> {
40 u32::from_str(s).map(Self)
41 }
42}
43
44#[derive(
46 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, sea_orm::DeriveValueType,
47)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "serde", serde(transparent))]
50#[repr(transparent)]
51pub struct EpisodeNumber(u32);
52
53impl EpisodeNumber {
54 #[inline]
56 #[must_use]
57 pub const fn new(value: u32) -> Self {
58 Self(value)
59 }
60
61 #[inline]
63 #[must_use]
64 pub const fn into_inner(self) -> u32 {
65 self.0
66 }
67}
68
69impl fmt::Display for EpisodeNumber {
70 #[inline]
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 self.0.fmt(f)
73 }
74}
75
76impl FromStr for EpisodeNumber {
77 type Err = <u32 as FromStr>::Err;
78
79 #[inline]
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 u32::from_str(s).map(Self)
82 }
83}
84
85#[derive(Debug, Clone, Copy, thiserror::Error)]
87#[expect(clippy::exhaustive_enums, reason = "unlikely to add new variants")]
88pub enum EpisodeNumbersError {
89 #[error("zero episodes")]
91 Zero,
92 #[error("noncontiguous episodes")]
94 Noncontiguous,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100#[expect(
101 clippy::module_name_repetitions,
102 reason = "Episodes is not a good name"
103)]
104pub struct EpisodeNumbers(RangeInclusive<EpisodeNumber>);
105
106impl TryFrom<&[EpisodeNumber]> for EpisodeNumbers {
107 type Error = EpisodeNumbersError;
108
109 #[inline]
110 fn try_from(value: &[EpisodeNumber]) -> Result<Self, Self::Error> {
111 match value {
112 [] => Err(EpisodeNumbersError::Zero),
113 [n] => Ok(Self(*n..=*n)),
114 _ => {
115 let min = value.iter().copied().min().unwrap_or_default();
117 let max = value.iter().copied().max().unwrap_or_default();
118 let len = value.len();
119
120 if usize::try_from(max.0.saturating_sub(min.0).saturating_add(1)) != Ok(len) {
121 return Err(EpisodeNumbersError::Noncontiguous);
122 }
123
124 let set: HashSet<_> = value.iter().copied().collect();
125 if set.len() != len {
126 return Err(EpisodeNumbersError::Noncontiguous);
127 }
128
129 Ok(Self(min..=max))
130 }
131 }
132 }
133}
134
135impl EpisodeNumbers {
136 #[inline]
139 #[must_use]
140 pub fn new(start: EpisodeNumber, count: u8) -> Self {
141 Self(start..=EpisodeNumber(start.0.saturating_add(count.into())))
142 }
143
144 #[inline]
146 #[must_use]
147 pub const fn as_range(&self) -> &RangeInclusive<EpisodeNumber> {
148 &self.0
149 }
150
151 #[inline]
154 #[must_use]
155 pub fn range_string(&self) -> String {
156 let start = self.0.start();
157 let end = self.0.end();
158
159 if start == end {
160 format!("{start:02}")
161 } else {
162 format!("{start:02}-{end:02}")
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::{EpisodeNumber, EpisodeNumbers, SeasonNumber};
170
171 #[test]
172 fn use_fn() {
173 _ = SeasonNumber::new(0);
174 let e = EpisodeNumber::new(0);
175 _ = EpisodeNumbers::new(e, 1);
176 }
177}