qubit_common/serde/duration_with_unit.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! Serde adapter for [`std::time::Duration`] as a string with a time unit.
10//!
11//! Serialization emits whole milliseconds with an `ms` suffix. Deserialization
12//! accepts strings with `ns`, `us`, `µs`, `ms`, `s`, `m`, `h`, or `d` suffixes,
13//! and also accepts a bare integer as milliseconds for lenient configuration
14//! input.
15
16use std::time::Duration;
17
18use serde::de::Error;
19use serde::{
20 Deserialize,
21 Deserializer,
22 Serializer,
23};
24
25use super::duration_millis::as_millis_u64;
26
27/// Serializes a [`Duration`] as a string such as `"500ms"`.
28///
29/// # Parameters
30/// - `duration`: Duration to serialize.
31/// - `serializer`: Serde serializer receiving the formatted string.
32///
33/// # Returns
34/// The serializer result.
35///
36/// # Errors
37/// Returns the serializer error if writing the string value fails.
38pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
39where
40 S: Serializer,
41{
42 serializer.serialize_str(&format(duration))
43}
44
45/// Deserializes a [`Duration`] from a string with a unit, or a bare millisecond
46/// integer.
47///
48/// # Parameters
49/// - `deserializer`: Serde deserializer providing a string or integer value.
50///
51/// # Returns
52/// The parsed [`Duration`].
53///
54/// # Errors
55/// Returns the deserializer error when the input has an unsupported unit,
56/// invalid number, fractional value, or overflows [`Duration`].
57pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
58where
59 D: Deserializer<'de>,
60{
61 let value = serde_json::Value::deserialize(deserializer)?;
62 match value {
63 serde_json::Value::Number(number) => {
64 let millis = number
65 .as_u64()
66 .ok_or_else(|| D::Error::custom("duration integer must be a non-negative u64"))?;
67 Ok(Duration::from_millis(millis))
68 }
69 serde_json::Value::String(text) => parse(&text).map_err(D::Error::custom),
70 _ => Err(D::Error::custom(
71 "duration must be a string with unit or a millisecond integer",
72 )),
73 }
74}
75
76/// Formats a [`Duration`] as saturated whole milliseconds with an `ms` suffix.
77///
78/// # Parameters
79/// - `duration`: Duration to format.
80///
81/// # Returns
82/// A string in the form `<millis>ms`.
83#[inline]
84pub fn format(duration: &Duration) -> String {
85 format!("{}ms", as_millis_u64(duration))
86}
87
88/// Parses a [`Duration`] from a string with a supported unit.
89///
90/// Bare integers are treated as milliseconds. Supported suffixes are `ns`,
91/// `us`, `µs`, `ms`, `s`, `m`, `h`, and `d`.
92///
93/// # Parameters
94/// - `text`: Duration text to parse.
95///
96/// # Returns
97/// The parsed [`Duration`].
98///
99/// # Errors
100/// Returns a message describing invalid syntax, unsupported units, or overflow.
101pub fn parse(text: &str) -> Result<Duration, String> {
102 let trimmed = text.trim();
103 if trimmed.is_empty() {
104 return Err("duration must not be empty".to_string());
105 }
106 if let Ok(millis) = trimmed.parse::<u64>() {
107 return Ok(Duration::from_millis(millis));
108 }
109
110 let (number, unit) = split_number_and_unit(trimmed)?;
111 let value = number.parse::<u64>().map_err(|_| {
112 format!("invalid duration value `{number}`: expected a non-negative integer")
113 })?;
114
115 duration_from_unit(value, unit)
116}
117
118/// Splits duration text into integer and unit parts.
119///
120/// # Parameters
121/// - `text`: Non-empty duration text.
122///
123/// # Returns
124/// A tuple containing the number text and unit text.
125///
126/// # Errors
127/// Returns an error when either part is missing.
128fn split_number_and_unit(text: &str) -> Result<(&str, &str), String> {
129 let split_at = text
130 .find(|ch: char| !ch.is_ascii_digit())
131 .ok_or_else(|| "duration unit is missing".to_string())?;
132 let (number, unit) = text.split_at(split_at);
133 if number.is_empty() {
134 return Err("duration value is missing".to_string());
135 }
136 Ok((number, unit))
137}
138
139/// Converts an integer value and unit suffix to a [`Duration`].
140///
141/// # Parameters
142/// - `value`: Non-negative integer duration value.
143/// - `unit`: Supported unit suffix.
144///
145/// # Returns
146/// The corresponding [`Duration`].
147///
148/// # Errors
149/// Returns an error when the unit is unsupported or the conversion overflows.
150fn duration_from_unit(value: u64, unit: &str) -> Result<Duration, String> {
151 match unit {
152 "ns" => Ok(Duration::from_nanos(value)),
153 "us" | "µs" => Ok(Duration::from_micros(value)),
154 "ms" => Ok(Duration::from_millis(value)),
155 "s" => Ok(Duration::from_secs(value)),
156 "m" => value
157 .checked_mul(60)
158 .map(Duration::from_secs)
159 .ok_or_else(|| "duration minutes overflow u64 seconds".to_string()),
160 "h" => value
161 .checked_mul(60 * 60)
162 .map(Duration::from_secs)
163 .ok_or_else(|| "duration hours overflow u64 seconds".to_string()),
164 "d" => value
165 .checked_mul(24 * 60 * 60)
166 .map(Duration::from_secs)
167 .ok_or_else(|| "duration days overflow u64 seconds".to_string()),
168 _ => Err(format!("unsupported duration unit `{unit}`")),
169 }
170}