surrealdb_types/value/
duration.rs1use std::fmt::Debug;
2use std::ops::Deref;
3use std::str::FromStr;
4
5use anyhow::anyhow;
6use serde::{Deserialize, Serialize};
7
8use crate::sql::{SqlFormat, ToSql};
9
10pub(crate) static SECONDS_PER_YEAR: u64 = 365 * SECONDS_PER_DAY;
11pub(crate) static SECONDS_PER_WEEK: u64 = 7 * SECONDS_PER_DAY;
12pub(crate) static SECONDS_PER_DAY: u64 = 24 * SECONDS_PER_HOUR;
13pub(crate) static SECONDS_PER_HOUR: u64 = 60 * SECONDS_PER_MINUTE;
14pub(crate) static SECONDS_PER_MINUTE: u64 = 60;
15pub(crate) static NANOSECONDS_PER_MILLISECOND: u32 = 1000000;
16pub(crate) static NANOSECONDS_PER_MICROSECOND: u32 = 1000;
17
18#[derive(
24 Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
25)]
26#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
27pub struct Duration(pub(crate) std::time::Duration);
28
29impl Duration {
30 pub const MAX: Duration = Duration(std::time::Duration::MAX);
32 pub const ZERO: Duration = Duration(std::time::Duration::ZERO);
34
35 pub fn new(secs: u64, nanos: u32) -> Duration {
37 std::time::Duration::new(secs, nanos).into()
38 }
39
40 pub fn from_std(d: std::time::Duration) -> Self {
42 Self(d)
43 }
44
45 pub fn into_inner(self) -> std::time::Duration {
47 self.0
48 }
49
50 pub fn nanos(&self) -> u128 {
52 self.0.as_nanos()
53 }
54 pub fn micros(&self) -> u128 {
56 self.0.as_micros()
57 }
58 pub fn millis(&self) -> u128 {
60 self.0.as_millis()
61 }
62 pub fn secs(&self) -> u64 {
64 self.0.as_secs()
65 }
66 pub fn mins(&self) -> u64 {
68 self.0.as_secs() / SECONDS_PER_MINUTE
69 }
70 pub fn hours(&self) -> u64 {
72 self.0.as_secs() / SECONDS_PER_HOUR
73 }
74 pub fn days(&self) -> u64 {
76 self.0.as_secs() / SECONDS_PER_DAY
77 }
78 pub fn weeks(&self) -> u64 {
80 self.0.as_secs() / SECONDS_PER_WEEK
81 }
82 pub fn years(&self) -> u64 {
84 self.0.as_secs() / SECONDS_PER_YEAR
85 }
86 pub fn from_nanos(nanos: u64) -> Duration {
88 std::time::Duration::from_nanos(nanos).into()
89 }
90 pub fn from_micros(micros: u64) -> Duration {
92 std::time::Duration::from_micros(micros).into()
93 }
94 pub fn from_millis(millis: u64) -> Duration {
96 std::time::Duration::from_millis(millis).into()
97 }
98 pub fn from_secs(secs: u64) -> Duration {
100 std::time::Duration::from_secs(secs).into()
101 }
102 pub fn from_mins(mins: u64) -> Option<Duration> {
104 mins.checked_mul(SECONDS_PER_MINUTE).map(std::time::Duration::from_secs).map(|x| x.into())
105 }
106 pub fn from_hours(hours: u64) -> Option<Duration> {
108 hours.checked_mul(SECONDS_PER_HOUR).map(std::time::Duration::from_secs).map(|x| x.into())
109 }
110 pub fn from_days(days: u64) -> Option<Duration> {
112 days.checked_mul(SECONDS_PER_DAY).map(std::time::Duration::from_secs).map(|x| x.into())
113 }
114 pub fn from_weeks(weeks: u64) -> Option<Duration> {
116 weeks.checked_mul(SECONDS_PER_WEEK).map(std::time::Duration::from_secs).map(|x| x.into())
117 }
118
119 pub(crate) fn fmt_sql_internal(&self, f: &mut String) {
120 let secs = self.0.as_secs();
122 let nano = self.0.subsec_nanos();
123 if secs == 0 && nano == 0 {
125 return f.push_str("0ns");
126 }
127 let year = secs / SECONDS_PER_YEAR;
129 let secs = secs % SECONDS_PER_YEAR;
130 let week = secs / SECONDS_PER_WEEK;
132 let secs = secs % SECONDS_PER_WEEK;
133 let days = secs / SECONDS_PER_DAY;
135 let secs = secs % SECONDS_PER_DAY;
136 let hour = secs / SECONDS_PER_HOUR;
138 let secs = secs % SECONDS_PER_HOUR;
139 let mins = secs / SECONDS_PER_MINUTE;
141 let secs = secs % SECONDS_PER_MINUTE;
142 let msec = nano / NANOSECONDS_PER_MILLISECOND;
144 let nano = nano % NANOSECONDS_PER_MILLISECOND;
145 let usec = nano / NANOSECONDS_PER_MICROSECOND;
147 let nano = nano % NANOSECONDS_PER_MICROSECOND;
148 if year > 0 {
150 f.push_str(&year.to_string());
151 f.push('y');
152 }
153 if week > 0 {
154 f.push_str(&week.to_string());
155 f.push('w');
156 }
157 if days > 0 {
158 f.push_str(&days.to_string());
159 f.push('d');
160 }
161 if hour > 0 {
162 f.push_str(&hour.to_string());
163 f.push('h');
164 }
165 if mins > 0 {
166 f.push_str(&mins.to_string());
167 f.push('m');
168 }
169 if secs > 0 {
170 f.push_str(&secs.to_string());
171 f.push('s');
172 }
173 if msec > 0 {
174 f.push_str(&msec.to_string());
175 f.push_str("ms");
176 }
177 if usec > 0 {
178 f.push_str(&usec.to_string());
179 f.push_str("µs");
180 }
181 if nano > 0 {
182 f.push_str(&nano.to_string());
183 f.push_str("ns");
184 }
185 }
186}
187
188impl FromStr for Duration {
189 type Err = anyhow::Error;
190
191 fn from_str(s: &str) -> Result<Self, Self::Err> {
192 let mut total_secs = 0u64;
193 let mut total_nanos = 0u32;
194 let mut remaining = s.trim();
195
196 if remaining.is_empty() {
198 return Err(anyhow!("Invalid duration string: {s}, empty string"));
199 }
200
201 if remaining == "0ns" || remaining == "0" {
203 return Ok(Duration::new(0, 0));
204 }
205
206 while !remaining.is_empty() {
207 let mut end = 0;
209 for (i, c) in remaining.char_indices() {
210 if !c.is_ascii_digit() {
211 end = i;
212 break;
213 }
214 end = i + c.len_utf8();
215 }
216
217 if end == 0 {
218 return Err(anyhow!("Invalid duration string: {s}, empty characters"));
219 }
220
221 let value_str = &remaining[..end];
222 let value: u64 = value_str.parse().map_err(|err| {
223 anyhow!("Invalid duration string: {s}, failed to parse value: {err}")
224 })?;
225
226 remaining = &remaining[end..];
227
228 let unit = if remaining.starts_with("ms") {
230 remaining = &remaining[2..];
231 "ms"
232 } else if remaining.starts_with("µs") {
233 remaining = &remaining[2..];
234 "µs"
235 } else if remaining.starts_with("us") {
236 remaining = &remaining[2..];
237 "us"
238 } else if remaining.starts_with("ns") {
239 remaining = &remaining[2..];
240 "ns"
241 } else if remaining.starts_with("y") {
242 remaining = &remaining[1..];
243 "y"
244 } else if remaining.starts_with("w") {
245 remaining = &remaining[1..];
246 "w"
247 } else if remaining.starts_with("d") {
248 remaining = &remaining[1..];
249 "d"
250 } else if remaining.starts_with("h") {
251 remaining = &remaining[1..];
252 "h"
253 } else if remaining.starts_with("m") {
254 remaining = &remaining[1..];
255 "m"
256 } else if remaining.starts_with("s") {
257 remaining = &remaining[1..];
258 "s"
259 } else {
260 return Err(anyhow!(
261 "Invalid duration string: {s}, unexpected remainder: {remaining}"
262 ));
263 };
264
265 match unit {
267 "y" => {
268 total_secs = total_secs.saturating_add(value.saturating_mul(SECONDS_PER_YEAR));
269 }
270 "w" => {
271 total_secs = total_secs.saturating_add(value.saturating_mul(SECONDS_PER_WEEK));
272 }
273 "d" => {
274 total_secs = total_secs.saturating_add(value.saturating_mul(SECONDS_PER_DAY));
275 }
276 "h" => {
277 total_secs = total_secs.saturating_add(value.saturating_mul(SECONDS_PER_HOUR));
278 }
279 "m" => {
280 total_secs =
281 total_secs.saturating_add(value.saturating_mul(SECONDS_PER_MINUTE));
282 }
283 "s" => {
284 total_secs = total_secs.saturating_add(value);
285 }
286 "ms" => {
287 let millis = value.saturating_mul(NANOSECONDS_PER_MILLISECOND as u64);
288 let (secs, nanos) = (millis / 1_000_000_000, (millis % 1_000_000_000) as u32);
289 total_secs = total_secs.saturating_add(secs);
290 total_nanos = total_nanos.saturating_add(nanos);
291 }
292 "µs" | "us" => {
293 let micros = value.saturating_mul(NANOSECONDS_PER_MICROSECOND as u64);
294 let (secs, nanos) = (micros / 1_000_000_000, (micros % 1_000_000_000) as u32);
295 total_secs = total_secs.saturating_add(secs);
296 total_nanos = total_nanos.saturating_add(nanos);
297 }
298 "ns" => {
299 let (secs, nanos) = (value / 1_000_000_000, (value % 1_000_000_000) as u32);
300 total_secs = total_secs.saturating_add(secs);
301 total_nanos = total_nanos.saturating_add(nanos);
302 }
303 unexpected => {
304 return Err(anyhow!(
305 "Invalid duration string: {s}, unexpected unit: {unexpected}"
306 ));
307 }
308 }
309 }
310
311 if total_nanos >= 1_000_000_000 {
313 let additional_secs = total_nanos / 1_000_000_000;
314 total_secs = total_secs.saturating_add(additional_secs as u64);
315 total_nanos %= 1_000_000_000;
316 }
317
318 Ok(Duration::new(total_secs, total_nanos))
319 }
320}
321
322impl From<std::time::Duration> for Duration {
323 fn from(v: std::time::Duration) -> Self {
324 Self(v)
325 }
326}
327
328impl From<Duration> for std::time::Duration {
329 fn from(v: Duration) -> Self {
330 v.0
331 }
332}
333
334impl Deref for Duration {
335 type Target = std::time::Duration;
336 fn deref(&self) -> &Self::Target {
337 &self.0
338 }
339}
340
341impl std::fmt::Display for Duration {
342 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343 f.write_str(&self.to_sql())
344 }
345}
346
347impl ToSql for Duration {
348 fn fmt_sql(&self, f: &mut String, _fmt: SqlFormat) {
349 self.fmt_sql_internal(f);
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_duration_from_str() {
359 assert_eq!(Duration::from_str("1s").unwrap(), Duration::from_secs(1));
361 assert_eq!(Duration::from_str("1m").unwrap(), Duration::from_mins(1).unwrap());
362 assert_eq!(Duration::from_str("1h").unwrap(), Duration::from_hours(1).unwrap());
363 assert_eq!(Duration::from_str("1d").unwrap(), Duration::from_days(1).unwrap());
364 assert_eq!(Duration::from_str("1w").unwrap(), Duration::from_weeks(1).unwrap());
365 assert_eq!(Duration::from_str("1y").unwrap(), Duration::new(365 * 24 * 60 * 60, 0));
366
367 assert_eq!(Duration::from_str("1000ns").unwrap(), Duration::from_nanos(1000));
369 assert_eq!(Duration::from_str("1000ms").unwrap(), Duration::from_millis(1000));
370
371 assert_eq!(Duration::from_str("0ns").unwrap(), Duration::new(0, 0));
373 assert_eq!(Duration::from_str("0").unwrap(), Duration::new(0, 0));
374
375 let combined = Duration::from_str("1h30m15s500ms").unwrap();
377 let expected = Duration::from_hours(1).unwrap().0
378 + Duration::from_mins(30).unwrap().0
379 + Duration::from_secs(15).0
380 + Duration::from_millis(500).0;
381 assert_eq!(combined.0, expected);
382
383 assert!(Duration::from_str("invalid").is_err());
385 assert!(Duration::from_str("1x").is_err());
386 assert!(Duration::from_str("").is_err());
387 }
388
389 #[test]
390 fn test_duration_from_str_debug() {
391 println!("Testing '1000us'");
393 match Duration::from_str("1000us") {
394 Ok(duration) => {
395 println!("Successfully parsed: {:?}", duration);
396 assert_eq!(duration, Duration::from_micros(1000));
397 }
398 Err(_) => {
399 println!("Failed to parse '1000us'");
400 panic!("Failed to parse microseconds");
401 }
402 }
403 }
404}