mangadex_api_types_rust/
mangadex_duration.rs1use 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#[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);
82fn 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
164fn 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}