Skip to main content

v_utils/other/
time.rs

1use eyre::{Result, WrapErr, bail, eyre};
2use serde::{Deserialize, Serialize, de};
3
4/// Meant to work with %H:%M and %H:%M:%S and %M:%S
5#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
6pub struct Timelike(pub u32);
7impl Timelike {
8	pub fn inner(&self) -> u32 {
9		self.0
10	}
11}
12
13impl std::fmt::Display for Timelike {
14	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15		match self.0 {
16			0..=59 => write!(f, "{:02}", self.0),
17			60..=3599 => write!(f, "{:02}:{:02}", self.0 / 60, self.0 % 60),
18			_ => write!(f, "{}:{:02}:{:02}", self.0 / 3600, (self.0 % 3600) / 60, self.0 % 60),
19		}
20	}
21}
22impl AsRef<u32> for Timelike {
23	fn as_ref(&self) -> &u32 {
24		&self.0
25	}
26}
27impl std::ops::Deref for Timelike {
28	type Target = u32;
29
30	fn deref(&self) -> &Self::Target {
31		&self.0
32	}
33}
34
35#[derive(Deserialize)]
36#[serde(untagged)]
37enum TimelikeHelper {
38	String(String),
39	Number(u32),
40}
41
42impl Serialize for Timelike {
43	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44	where
45		S: serde::Serializer, {
46		serializer.serialize_str(&self.to_string())
47	}
48}
49
50impl<'de> Deserialize<'de> for Timelike {
51	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52	where
53		D: de::Deserializer<'de>, {
54		let helper = TimelikeHelper::deserialize(deserializer)?;
55		match helper {
56			TimelikeHelper::String(time) => time_to_units(&time).map_err(|e| de::Error::custom(e.to_string())).map(Timelike),
57			TimelikeHelper::Number(units) => Ok(Timelike(units)),
58		}
59	}
60}
61
62fn time_to_units(time: &str) -> Result<u32> {
63	// If there are no colons, try to parse as seconds directly
64	if !time.contains(':') {
65		return time.parse().wrap_err_with(|| eyre!("Invalid time format: Could not parse '{time}' as seconds"));
66	}
67
68	let mut split = time.split(':');
69
70	let first = split
71		.next()
72		.ok_or_else(|| eyre!("Invalid time format: Expected one of %H:%M, %H:%M:%S, or %M:%S, got '{time}'"))?
73		.parse::<u32>()
74		.wrap_err_with(|| eyre!("Invalid time format: Expected one of %H:%M, %H:%M:%S, or %M:%S, got '{time}'"))?;
75
76	let second = split
77		.next()
78		.ok_or_else(|| eyre!("Invalid time format: Expected one of %H:%M, %H:%M:%S, or %M:%S, got '{time}'"))?
79		.parse::<u32>()
80		.wrap_err_with(|| eyre!("Invalid time format: Expected one of %H:%M, %H:%M:%S, or %M:%S, got '{time}'"))?;
81
82	let units = match split.next() {
83		Some(third) => {
84			let third = third
85				.parse::<u32>()
86				.wrap_err_with(|| eyre!("Invalid time format: Expected one of %H:%M, %H:%M:%S, or %M:%S, got '{time}'"))?;
87			first * 3600 + second * 60 + third
88		}
89		None => first * 60 + second,
90	};
91
92	if split.next().is_some() {
93		bail!("Invalid time format: Expected one of %H:%M, %H:%M:%S, or %M:%S, got '{time}'");
94	}
95
96	Ok(units)
97}
98
99#[cfg(test)]
100mod tests {
101	use serde_json::json;
102
103	use super::*;
104
105	#[test]
106	fn test_time_de() {
107		assert_eq!(serde_json::from_str::<Timelike>(r#""12:34""#).unwrap().inner(), 754);
108		assert_eq!(serde_json::from_str::<Timelike>(r#""12:34:56""#).unwrap().inner(), 45296);
109		assert_eq!(serde_json::from_str::<Timelike>(r#""34:56""#).unwrap().inner(), 2096);
110		assert_eq!(serde_json::from_str::<Timelike>("754").unwrap().inner(), 754);
111		assert_eq!(serde_json::from_str::<Timelike>(r#""34""#).unwrap().inner(), 34);
112		assert!(serde_json::from_str::<Timelike>(r#""12:34:56:78""#).is_err());
113	}
114
115	#[test]
116	fn test_time_ser() {
117		assert_eq!(Timelike(30).to_string(), "30");
118		assert_eq!(Timelike(2096).to_string(), "34:56");
119		assert_eq!(Timelike(3600).to_string(), "1:00:00");
120		assert_eq!(&json!(Timelike(0)).to_string(), "\"00\"");
121		assert_eq!(&json!(Timelike(45296)).to_string(), "\"12:34:56\"");
122	}
123}