edge_schema/pretty_duration/
mod.rs1mod reltime;
2
3use std::time::Duration;
4
5pub use self::reltime::parse_timestamp_or_relative_time;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct PrettyDuration(pub std::time::Duration);
29
30impl PrettyDuration {
31 pub const ZERO: PrettyDuration = PrettyDuration(Duration::ZERO);
32 pub const MAX: PrettyDuration = PrettyDuration(Duration::MAX);
33
34 pub fn new(duration: std::time::Duration) -> Self {
35 Self(duration)
36 }
37
38 pub fn as_duration(&self) -> std::time::Duration {
39 self.0
40 }
41
42 pub fn from_secs(secs: u64) -> Self {
43 Self(std::time::Duration::from_secs(secs))
44 }
45
46 pub fn from_mins(mins: u64) -> Self {
47 Self(std::time::Duration::from_secs(mins * 60))
48 }
49
50 pub fn from_hours(hours: u64) -> Self {
51 Self(std::time::Duration::from_secs(hours * 60 * 60))
52 }
53}
54
55impl std::ops::Deref for PrettyDuration {
56 type Target = std::time::Duration;
57
58 fn deref(&self) -> &Self::Target {
59 &self.0
60 }
61}
62
63impl From<std::time::Duration> for PrettyDuration {
64 fn from(duration: std::time::Duration) -> Self {
65 Self::new(duration)
66 }
67}
68
69impl From<PrettyDuration> for std::time::Duration {
70 fn from(duration: PrettyDuration) -> Self {
71 duration.0
72 }
73}
74
75impl std::fmt::Display for PrettyDuration {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 const DAY: u64 = 60 * 60 * 24;
78 const HOUR: u64 = 60 * 60;
79
80 let secs = self.0.as_secs();
81
82 let days = secs / DAY;
83 if days > 0 {
84 write!(f, "{}d", days)?;
85 }
86 let secs = secs % DAY;
87
88 let hours = secs / HOUR;
89 if hours > 0 {
90 write!(f, "{}h", hours)?;
91 }
92 let secs = secs % HOUR;
93
94 let mins = secs / 60;
95 if mins > 0 {
96 write!(f, "{}m", mins)?;
97 }
98 let secs = secs % 60;
99 if secs > 0 {
100 write!(f, "{}s", secs)?;
101 }
102
103 Ok(())
104 }
105}
106
107#[derive(Debug)]
108pub struct PrettyDurationParseError {
109 value: String,
110 message: String,
111}
112
113impl std::fmt::Display for PrettyDurationParseError {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 write!(f, "Invalid time spec '{}': {}", self.value, self.message)
116 }
117}
118
119impl std::error::Error for PrettyDurationParseError {}
120
121impl std::str::FromStr for PrettyDuration {
122 type Err = PrettyDurationParseError;
123
124 fn from_str(mut input: &str) -> Result<Self, Self::Err> {
125 let mut seconds = 0;
126
127 loop {
128 input = input.strip_prefix(' ').unwrap_or(input);
129 if input.is_empty() {
130 break;
131 }
132
133 let nums = input.chars().take_while(|x| x.is_ascii_digit()).count();
134 if nums < 1 {
135 return Err(PrettyDurationParseError {
136 message: "must start with a number".to_string(),
137 value: input.to_string(),
138 });
139 }
140
141 let number = &input[..nums]
142 .parse::<u64>()
143 .map_err(|e| PrettyDurationParseError {
144 message: format!("invalid number: {}", e),
145 value: input.to_string(),
146 })?;
147
148 input = &input[nums..];
149 let chars = input
150 .chars()
151 .take_while(|x| x.is_ascii_alphabetic())
152 .count();
153 let unit = &input[..chars];
154 input = &input[chars..];
155
156 let scale = match unit {
157 "s" | "sec" | "secs" | "seconds" => 1,
158 "m" | "min" | "mins" | "minutes" => 60,
159 "h" | "hour" | "hours" => 60 * 60,
160 "d" | "day" | "days" => 60 * 60 * 24,
161 _ => {
162 return Err(PrettyDurationParseError {
163 message: "unknown unit".to_string(),
164 value: input.to_string(),
165 });
166 }
167 };
168
169 seconds += number * scale;
170 }
171
172 let dur = std::time::Duration::from_secs(seconds);
173
174 Ok(Self(dur))
175 }
176}
177
178impl serde::Serialize for PrettyDuration {
179 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
180 where
181 S: serde::Serializer,
182 {
183 self.to_string().serialize(serializer)
184 }
185}
186
187impl<'de> serde::Deserialize<'de> for PrettyDuration {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: serde::Deserializer<'de>,
191 {
192 let s = String::deserialize(deserializer)?;
193 s.parse().map_err(serde::de::Error::custom)
194 }
195}
196
197impl schemars::JsonSchema for PrettyDuration {
198 fn schema_name() -> String {
199 "StringWebcPackageIdent".to_string()
200 }
201
202 fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
203 schemars::schema::Schema::Object(schemars::schema::SchemaObject {
204 instance_type: Some(schemars::schema::InstanceType::String.into()),
205 ..Default::default()
206 })
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use std::time::Duration;
213
214 use super::*;
215
216 #[test]
217 fn test_pretty_duration_constructors() {
218 assert_eq!(
219 PrettyDuration::from_secs(1).as_duration(),
220 Duration::from_secs(1)
221 );
222 assert_eq!(
223 PrettyDuration::from_mins(1).as_duration(),
224 Duration::from_secs(60)
225 );
226 assert_eq!(
227 PrettyDuration::from_hours(1).as_duration(),
228 Duration::from_secs(60 * 60)
229 );
230 }
231
232 #[test]
233 fn test_pretty_duration_parse() {
234 let cases = &[
235 ("1s", Duration::from_secs(1), "1s"),
236 ("10s", Duration::from_secs(10), "10s"),
237 ("59s", Duration::from_secs(59), "59s"),
238 ("60s", Duration::from_secs(60), "1m"),
239 ("1m", Duration::from_secs(60), "1m"),
240 ("11m", Duration::from_secs(60) * 11, "11m"),
241 ("60m", Duration::from_secs(60) * 60, "1h"),
242 ("1h", Duration::from_secs(60) * 60, "1h"),
243 ("11h", Duration::from_secs(60) * 60 * 11, "11h"),
244 ("1h1m", Duration::from_secs(61) * 60, "1h1m"),
245 ("1h1m1s", Duration::from_secs(61 * 60 + 1), "1h1m1s"),
246 ];
247
248 for (index, (input, duration, output)) in cases.iter().enumerate() {
249 eprintln!("test case {index}: {input} => {duration:?} => {output}");
250 let p = input.parse::<PrettyDuration>().unwrap();
251 assert_eq!(p, PrettyDuration::new(*duration));
252 assert_eq!(p.to_string(), output.to_string());
253 }
254 }
255
256 #[test]
257 fn test_pretty_duration_serde() {
258 let dur = PrettyDuration::from_secs(1);
259 let json = serde_json::to_string(&dur).unwrap();
260 assert_eq!(json, "\"1s\"");
261
262 let dur2: PrettyDuration = serde_json::from_str(&json).unwrap();
263 assert_eq!(dur, dur2);
264 }
265}