objectstore_types/
duration.rs1use std::borrow::Cow;
35use std::error::Error;
36use std::fmt;
37use std::time::Duration;
38
39use serde::{Deserialize, Deserializer, Serializer};
40
41const SECS_PER_MINUTE: u64 = 60;
42const SECS_PER_HOUR: u64 = 60 * SECS_PER_MINUTE;
43const SECS_PER_DAY: u64 = 24 * SECS_PER_HOUR;
44
45pub fn format_duration(duration: Duration) -> FormattedDuration {
60 FormattedDuration(duration)
61}
62
63pub fn parse_duration(input: &str) -> Result<Duration, ParseDurationError> {
80 humantime::parse_duration(input).map_err(ParseDurationError)
81}
82
83pub fn serialize<S: Serializer>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
85 serializer.collect_str(&format_duration(*duration))
86}
87
88pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
90 #[derive(Deserialize)]
92 #[serde(transparent)]
93 struct BorrowedStr<'a>(#[serde(borrow)] Cow<'a, str>);
94
95 let BorrowedStr(value) = BorrowedStr::deserialize(deserializer)?;
96 parse_duration(&value).map_err(serde::de::Error::custom)
97}
98
99#[derive(Debug)]
103pub struct ParseDurationError(humantime::DurationError);
104
105impl fmt::Display for ParseDurationError {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 self.0.fmt(f)
108 }
109}
110
111impl Error for ParseDurationError {}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct FormattedDuration(Duration);
118
119impl fmt::Display for FormattedDuration {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 let secs = self.0.as_secs();
122 if secs == 0 {
123 return f.write_str("0s");
124 }
125
126 let components = [
127 (secs / SECS_PER_DAY, "d"),
128 (secs % SECS_PER_DAY / SECS_PER_HOUR, "h"),
129 (secs % SECS_PER_HOUR / SECS_PER_MINUTE, "m"),
130 (secs % SECS_PER_MINUTE, "s"),
131 ];
132
133 let mut separator = "";
134 for (value, unit) in components {
135 if value > 0 {
136 write!(f, "{separator}{value}{unit}")?;
137 separator = " ";
138 }
139 }
140
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn format(duration: Duration) -> String {
150 format_duration(duration).to_string()
151 }
152
153 #[test]
154 fn formats_units() {
155 assert_eq!(format(Duration::ZERO), "0s");
156 assert_eq!(format(Duration::from_secs(30)), "30s");
157 assert_eq!(format(Duration::from_secs(60)), "1m");
158 assert_eq!(format(Duration::from_secs(3600)), "1h");
159 assert_eq!(format(Duration::from_secs(86400)), "1d");
160 }
161
162 #[test]
163 fn formats_combined_units_and_skips_zeroes() {
164 let duration = Duration::from_secs(2 * 86400 + 3 * 3600 + 4);
165 assert_eq!(format(duration), "2d 3h 4s");
166 }
167
168 #[test]
169 fn keeps_long_durations_in_days() {
170 assert_eq!(format(Duration::from_secs(7 * 86400)), "7d");
172 assert_eq!(format(Duration::from_secs(400 * 86400)), "400d");
173 }
174
175 #[test]
176 fn truncates_sub_second_remainder() {
177 assert_eq!(format(Duration::from_millis(1500)), "1s");
178 assert_eq!(format(Duration::from_millis(500)), "0s");
179 }
180
181 #[test]
182 fn round_trips_through_parse() {
183 for secs in [0, 1, 59, 60, 3661, 86400, 396 * 86400 + 62208] {
184 let duration = Duration::from_secs(secs);
185 let formatted = format(duration);
186 assert_eq!(parse_duration(&formatted).unwrap(), duration, "{formatted}");
187 }
188 }
189
190 #[test]
191 fn parses_units_that_are_never_emitted() {
192 assert_eq!(
193 parse_duration("2weeks").unwrap(),
194 Duration::from_secs(1_209_600)
195 );
196 assert_eq!(
197 parse_duration("1year").unwrap(),
198 Duration::from_secs(31_557_600)
199 );
200 }
201}