Skip to main content

surrealdb_types/value/
duration.rs

1use 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/// Represents a duration value in SurrealDB
19///
20/// A duration represents a span of time, typically used for time-based calculations and
21/// comparisons. This type wraps the standard `std::time::Duration` type.
22
23#[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	/// The maximum duration
31	pub const MAX: Duration = Duration(std::time::Duration::MAX);
32	/// The zero duration
33	pub const ZERO: Duration = Duration(std::time::Duration::ZERO);
34
35	/// Create a duration from both seconds and nanoseconds components
36	pub fn new(secs: u64, nanos: u32) -> Duration {
37		std::time::Duration::new(secs, nanos).into()
38	}
39
40	/// Create a duration from std::time::Duration
41	pub fn from_std(d: std::time::Duration) -> Self {
42		Self(d)
43	}
44
45	/// Convert into the inner std::time::Duration
46	pub fn into_inner(self) -> std::time::Duration {
47		self.0
48	}
49
50	/// Get the total number of nanoseconds
51	pub fn nanos(&self) -> u128 {
52		self.0.as_nanos()
53	}
54	/// Get the total number of microseconds
55	pub fn micros(&self) -> u128 {
56		self.0.as_micros()
57	}
58	/// Get the total number of milliseconds
59	pub fn millis(&self) -> u128 {
60		self.0.as_millis()
61	}
62	/// Get the total number of seconds
63	pub fn secs(&self) -> u64 {
64		self.0.as_secs()
65	}
66	/// Get the total number of minutes
67	pub fn mins(&self) -> u64 {
68		self.0.as_secs() / SECONDS_PER_MINUTE
69	}
70	/// Get the total number of hours
71	pub fn hours(&self) -> u64 {
72		self.0.as_secs() / SECONDS_PER_HOUR
73	}
74	/// Get the total number of dats
75	pub fn days(&self) -> u64 {
76		self.0.as_secs() / SECONDS_PER_DAY
77	}
78	/// Get the total number of months
79	pub fn weeks(&self) -> u64 {
80		self.0.as_secs() / SECONDS_PER_WEEK
81	}
82	/// Get the total number of years
83	pub fn years(&self) -> u64 {
84		self.0.as_secs() / SECONDS_PER_YEAR
85	}
86	/// Create a duration from nanoseconds
87	pub fn from_nanos(nanos: u64) -> Duration {
88		std::time::Duration::from_nanos(nanos).into()
89	}
90	/// Create a duration from microseconds
91	pub fn from_micros(micros: u64) -> Duration {
92		std::time::Duration::from_micros(micros).into()
93	}
94	/// Create a duration from milliseconds
95	pub fn from_millis(millis: u64) -> Duration {
96		std::time::Duration::from_millis(millis).into()
97	}
98	/// Create a duration from seconds
99	pub fn from_secs(secs: u64) -> Duration {
100		std::time::Duration::from_secs(secs).into()
101	}
102	/// Create a duration from minutes
103	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	/// Create a duration from hours
107	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	/// Create a duration from days
111	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	/// Create a duration from weeks
115	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		// Split up the duration
121		let secs = self.0.as_secs();
122		let nano = self.0.subsec_nanos();
123		// Ensure no empty output
124		if secs == 0 && nano == 0 {
125			return f.push_str("0ns");
126		}
127		// Calculate the total years
128		let year = secs / SECONDS_PER_YEAR;
129		let secs = secs % SECONDS_PER_YEAR;
130		// Calculate the total weeks
131		let week = secs / SECONDS_PER_WEEK;
132		let secs = secs % SECONDS_PER_WEEK;
133		// Calculate the total days
134		let days = secs / SECONDS_PER_DAY;
135		let secs = secs % SECONDS_PER_DAY;
136		// Calculate the total hours
137		let hour = secs / SECONDS_PER_HOUR;
138		let secs = secs % SECONDS_PER_HOUR;
139		// Calculate the total minutes
140		let mins = secs / SECONDS_PER_MINUTE;
141		let secs = secs % SECONDS_PER_MINUTE;
142		// Calculate the total milliseconds
143		let msec = nano / NANOSECONDS_PER_MILLISECOND;
144		let nano = nano % NANOSECONDS_PER_MILLISECOND;
145		// Calculate the total microseconds
146		let usec = nano / NANOSECONDS_PER_MICROSECOND;
147		let nano = nano % NANOSECONDS_PER_MICROSECOND;
148		// Write the different parts
149		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		// Handle empty string
197		if remaining.is_empty() {
198			return Err(anyhow!("Invalid duration string: {s}, empty string"));
199		}
200
201		// Handle special case for zero duration
202		if remaining == "0ns" || remaining == "0" {
203			return Ok(Duration::new(0, 0));
204		}
205
206		while !remaining.is_empty() {
207			// Find the end of the number part
208			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			// Parse the unit - check longer units first to avoid partial matches
229			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			// Convert to seconds and nanoseconds based on unit
266			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		// Handle nanosecond overflow
312		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		// Test basic units
360		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		// Test nanosecond units
368		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		// Test zero duration
372		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		// Test combined units
376		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		// Test invalid input
384		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		// Debug test for microseconds
392		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}