mangadex_api_types_rust/
mangadex_duration.rs

1use std::time::Duration;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5const SECONDS_PER_MINUTE: u64 = 60;
6const MINUTES_PER_HOUR: u64 = 60;
7const HOURS_PER_DAY: u64 = 24;
8const DAYS_PER_WEEK: u64 = 7;
9const SECONDS_PER_HOUR: u64 = MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
10const SECONDS_PER_DAY: u64 = HOURS_PER_DAY * SECONDS_PER_HOUR;
11const SECONDS_PER_WEEK: u64 = DAYS_PER_WEEK * SECONDS_PER_DAY;
12
13/// Newtype tuple struct for handling duration fields in MangaDex.
14///
15/// Should respected ISO 8601 duration specification: <https://en.wikipedia.org/wiki/ISO_8601#Durations>
16///
17/// Pattern: `^(P([1-9]|[1-9][0-9])D)?(P?([1-9])W)?(P?T(([1-9]|1[0-9]|2[0-4])H)?(([1-9]|[1-5][0-9]|60)M)?(([1-9]|[1-5][0-9]|60)S)?)?$`
18///
19/// Only the following units are considered to/from the ISO 8601 duration format:
20///
21/// - Weeks
22/// - Days
23/// - Hours
24/// - Minutes
25/// - Seconds
26///
27/// # Examples
28///
29/// - Two days is `P2D`.
30/// - Two seconds is `PT2S`.
31/// - Six weeks and five minutes is `P6WT5M`.
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Copy)]
33#[cfg_attr(feature = "specta", derive(specta::Type))]
34pub struct MangaDexDuration(Duration);
35
36impl MangaDexDuration {
37    pub fn new(duration: Duration) -> Self {
38        Self(duration)
39    }
40}
41
42impl AsRef<Duration> for MangaDexDuration {
43    fn as_ref(&self) -> &Duration {
44        &self.0
45    }
46}
47
48impl std::fmt::Display for MangaDexDuration {
49    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
50        fmt.write_str(format!("{:#?}", self.as_ref()).as_str())
51    }
52}
53
54impl Serialize for MangaDexDuration {
55    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
56    where
57        S: Serializer,
58    {
59        let output = duration_to_iso_8601(self.as_ref());
60        serializer.serialize_str(&output)
61    }
62}
63
64impl<'de> Deserialize<'de> for MangaDexDuration {
65    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        let raw: String = Deserialize::deserialize(deserializer)?;
70
71        let duration = match iso_8601_to_duration(&raw) {
72            Ok(d) => Ok(d),
73            Err(msg) => Err(serde::de::Error::custom(msg)),
74        }?;
75
76        Ok(Self(duration))
77    }
78}
79
80#[cfg(feature = "async-graphql")]
81async_graphql::scalar!(MangaDexDuration);
82/// Parse an ISO 8601 duration string and return a `std::time::Duration` struct.
83///
84/// Should respected ISO 8601 duration specification: <https://en.wikipedia.org/wiki/ISO_8601#Durations>
85///
86/// Pattern: `^(P([1-9]|[1-9][0-9])D)?(P?([1-9])W)?(P?T(([1-9]|1[0-9]|2[0-4])H)?(([1-9]|[1-5][0-9]|60)M)?(([1-9]|[1-5][0-9]|60)S)?)?$`
87///
88/// Only the following units are considered from the ISO 8601 duration format:
89///
90/// - Weeks
91/// - Days
92/// - Hours
93/// - Minutes
94/// - Seconds
95///
96/// # Examples
97///
98/// - Two days is `P2D`.
99/// - Two seconds is `PT2S`.
100/// - Six weeks and five minutes is `P6WT5M`.
101// Disclaimer: The method in which this function parses the ISO 8601 duration format is naïve but is functional.
102// TODO: Fix this hacky solution.
103fn iso_8601_to_duration(date_interval: &str) -> Result<Duration, String> {
104    let mut secs: u64 = 0;
105    let mut num = "".to_string();
106    let mut invalid_input = false;
107
108    let mut it = date_interval.chars().peekable();
109    while let Some(&c) = it.peek() {
110        match c {
111            'P' | 'T' => {
112                it.next();
113            }
114            '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
115                num += c.to_string().as_str();
116                it.next();
117            }
118            'W' => {
119                secs += num.parse::<u64>().unwrap()
120                    * DAYS_PER_WEEK
121                    * HOURS_PER_DAY
122                    * MINUTES_PER_HOUR
123                    * SECONDS_PER_MINUTE;
124                num = "".to_string();
125                it.next();
126            }
127            'D' => {
128                secs += num.parse::<u64>().unwrap()
129                    * HOURS_PER_DAY
130                    * MINUTES_PER_HOUR
131                    * SECONDS_PER_MINUTE;
132                num = "".to_string();
133                it.next();
134            }
135            'H' => {
136                secs += num.parse::<u64>().unwrap() * MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
137                num = "".to_string();
138                it.next();
139            }
140            'M' => {
141                secs += num.parse::<u64>().unwrap() * SECONDS_PER_MINUTE;
142                num = "".to_string();
143                it.next();
144            }
145            'S' => {
146                secs += num.parse::<u64>().unwrap();
147                num = "".to_string();
148                it.next();
149            }
150            _ => {
151                invalid_input = true;
152                break;
153            }
154        }
155    }
156
157    if invalid_input {
158        return Err(format!("invalid DateInterval '{date_interval}'"));
159    }
160
161    Ok(Duration::from_secs(secs))
162}
163
164/// Convert a `std::time::Duration` struct into a ISO 8601 duration string.
165///
166/// Should respected ISO 8601 duration specification: <https://en.wikipedia.org/wiki/ISO_8601#Durations>
167///
168/// Pattern: `^(P([1-9]|[1-9][0-9])D)?(P?([1-9])W)?(P?T(([1-9]|1[0-9]|2[0-4])H)?(([1-9]|[1-5][0-9]|60)M)?(([1-9]|[1-5][0-9]|60)S)?)?$`
169///
170/// Only the following units are considered to the ISO 8601 duration format:
171///
172/// - Weeks
173/// - Days
174/// - Hours
175/// - Minutes
176/// - Seconds
177///
178/// # Examples
179///
180/// - Two days is `P2D`.
181/// - Two seconds is `PT2S`.
182/// - Six weeks and five minutes is `P6WT5M`.
183// The method in which this function serializes the ISO 8601 duration format is naïve but is functional.
184// TODO: Fix this hacky solution.
185fn duration_to_iso_8601(duration: &Duration) -> String {
186    let mut secs = duration.as_secs();
187
188    let weeks = secs / SECONDS_PER_WEEK;
189    secs %= SECONDS_PER_WEEK;
190
191    let days = secs / SECONDS_PER_DAY;
192    secs %= SECONDS_PER_DAY;
193
194    let hours = secs / SECONDS_PER_HOUR;
195    secs %= SECONDS_PER_HOUR;
196
197    let minutes = secs / SECONDS_PER_MINUTE;
198    secs %= SECONDS_PER_MINUTE;
199
200    let mut duration_period = "".to_string();
201    if weeks > 0 {
202        duration_period += &format!("{weeks}W")
203    }
204    if days > 0 {
205        duration_period += &format!("{days}D")
206    }
207    let duration_period = format!("P{duration_period}");
208
209    let mut time_elements = "".to_string();
210    if duration_period == "P" || hours > 0 || minutes > 0 || secs > 0 {
211        time_elements += "T";
212
213        if hours > 0 {
214            time_elements += &format!("{hours}H");
215        }
216
217        if minutes > 0 {
218            time_elements += &format!("{minutes}M");
219        }
220
221        if time_elements == "T" || secs > 0 {
222            time_elements += &format!("{secs}S");
223        }
224    }
225
226    format!("{duration_period}{time_elements}")
227}
228
229#[cfg(test)]
230mod tests {
231    use std::time::Duration;
232
233    use super::*;
234
235    #[test]
236    fn iso_8601_to_duration_works() {
237        let test_cases = [
238            (
239                "P2D",
240                Duration::from_secs(2 * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE),
241            ),
242            ("PT2S", Duration::from_secs(2)),
243            (
244                "P6WT5M",
245                Duration::from_secs(
246                    (6 * DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE)
247                        + (5 * SECONDS_PER_MINUTE),
248                ),
249            ),
250        ];
251
252        for (input, expected) in test_cases {
253            assert_eq!(iso_8601_to_duration(input).unwrap(), expected);
254        }
255    }
256
257    #[test]
258    fn duration_to_iso_8601_works() {
259        let test_cases = [
260            (
261                Duration::from_secs(2 * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE),
262                "P2D",
263            ),
264            (Duration::from_secs(2), "PT2S"),
265            (
266                Duration::from_secs(
267                    (6 * DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE)
268                        + (5 * SECONDS_PER_MINUTE),
269                ),
270                "P6WT5M",
271            ),
272            (Duration::from_secs(0), "PT0S"),
273        ];
274
275        for (input, expected) in test_cases {
276            assert_eq!(duration_to_iso_8601(&input), expected);
277        }
278    }
279}