tea_time/timedelta.rs
1use std::hash::Hash;
2use std::str::FromStr;
3
4use chrono::Duration;
5use tea_error::{TError, TResult, tbail, tensure};
6
7use crate::convert::*;
8
9#[cfg(feature = "serde")]
10#[serde_with::serde_as]
11#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
12/// Represents a duration of time with both months and a more precise duration.
13///
14/// This struct combines a number of months with a `chrono::Duration` to represent
15/// time intervals that may include calendar-specific units (months) as well as
16/// fixed-length durations.
17///
18/// # Fields
19///
20/// * `months`: The number of months in the time delta.
21/// * `inner`: A `chrono::Duration` representing the precise duration beyond whole months.
22///
23/// # Serialization
24///
25/// When the "serde" feature is enabled, this struct can be serialized and deserialized.
26pub struct TimeDelta {
27 pub months: i32,
28 // #[cfg_attr(feature = "serde", serde_as(as = "serde_with::DurationSeconds<i64>"))]
29 #[serde_as(as = "serde_with::DurationSeconds<i64>")]
30 pub inner: Duration,
31}
32
33#[cfg(not(feature = "serde"))]
34#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
35/// Represents a duration of time with both months and a more precise duration.
36///
37/// This struct combines a number of months with a `chrono::Duration` to represent
38/// time intervals that may include calendar-specific units (months) as well as
39/// fixed-length durations.
40///
41/// # Fields
42///
43/// * `months`: The number of months in the time delta.
44/// * `inner`: A `chrono::Duration` representing the precise duration beyond whole months.
45///
46/// # Serialization
47///
48/// When the "serde" feature is enabled, this struct can be serialized and deserialized.
49pub struct TimeDelta {
50 pub months: i32,
51 pub inner: Duration,
52}
53
54impl FromStr for TimeDelta {
55 type Err = TError;
56
57 #[inline]
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 TimeDelta::parse(s)
60 }
61}
62
63impl From<&str> for TimeDelta {
64 #[inline]
65 fn from(s: &str) -> Self {
66 TimeDelta::parse(s).unwrap_or_else(|e| panic!("{}", e))
67 }
68}
69
70impl TimeDelta {
71 /// Parse timedelta from string
72 ///
73 /// for example: "2y1mo-3d5h-2m3s"
74 /// Parses a string representation of a duration into a `TimeDelta`.
75 ///
76 /// This function supports a variety of time units and can handle complex duration strings.
77 ///
78 /// # Supported Units
79 ///
80 /// - `ns`: nanoseconds
81 ///
82 /// - `us`: microseconds
83 ///
84 /// - `ms`: milliseconds
85 ///
86 /// - `s`: seconds
87 ///
88 /// - `m`: minutes
89 ///
90 /// - `h`: hours
91 ///
92 /// - `d`: days
93 ///
94 /// - `w`: weeks
95 ///
96 /// - `mo`: months
97 ///
98 /// - `y`: years
99 ///
100 /// # Format
101 /// The duration string should be in the format of `<number><unit>`, and multiple such pairs can be combined.
102 /// For example: "2y1mo-3d5h-2m3s" represents 2 years, 1 month, minus 3 days, 5 hours, minus 2 minutes, and 3 seconds.
103 ///
104 /// # Arguments
105 /// * `duration` - A string slice that holds the duration to be parsed.
106 ///
107 /// # Returns
108 /// * `TResult<Self>` - A Result containing the parsed `TimeDelta` if successful, or an error if parsing fails.
109 ///
110 /// # Examples
111 /// ```
112 /// use tea_time::TimeDelta;
113 ///
114 /// let td = TimeDelta::parse("1y2mo3d4h5m6s").unwrap();
115 /// assert_eq!(td.months, 14); // 1 year and 2 months
116 /// assert_eq!(td.inner, chrono::Duration::seconds(3 * 86400 + 4 * 3600 + 5 * 60 + 6));
117 /// ```
118 pub fn parse(duration: &str) -> TResult<Self> {
119 let mut nsecs = 0;
120 let mut secs = 0;
121 let mut months = 0;
122 let mut iter = duration.char_indices();
123 let mut start = 0;
124 let mut unit = String::with_capacity(2);
125 while let Some((i, mut ch)) = iter.next() {
126 if !ch.is_ascii_digit() && i != 0 {
127 let n = duration[start..i].parse::<i64>().unwrap();
128 loop {
129 if ch.is_ascii_alphabetic() {
130 unit.push(ch)
131 } else {
132 break;
133 }
134 match iter.next() {
135 Some((i, ch_)) => {
136 ch = ch_;
137 start = i
138 },
139 None => {
140 break;
141 },
142 }
143 }
144 tensure!(!unit.is_empty(), ParseError:"expected a unit in the duration string");
145
146 match unit.as_str() {
147 "ns" => nsecs += n,
148 "us" => nsecs += n * NANOS_PER_MICRO,
149 "ms" => nsecs += n * NANOS_PER_MILLI,
150 "s" => secs += n,
151 "m" => secs += n * SECS_PER_MINUTE,
152 "h" => secs += n * SECS_PER_HOUR,
153 "d" => secs += n * SECS_PER_DAY,
154 "w" => secs += n * SECS_PER_WEEK,
155 "mo" => months += n as i32,
156 "y" => months += n as i32 * 12,
157 unit => tbail!(ParseError:"unit: '{}' not supported", unit),
158 }
159 unit.clear();
160 }
161 }
162 let duration = Duration::seconds(secs) + Duration::nanoseconds(nsecs);
163 Ok(TimeDelta {
164 months,
165 inner: duration,
166 })
167 }
168
169 #[inline(always)]
170 pub const fn nat() -> Self {
171 Self {
172 months: i32::MIN,
173 inner: Duration::seconds(0),
174 }
175 }
176
177 #[allow(dead_code)]
178 #[inline(always)]
179 pub const fn is_nat(&self) -> bool {
180 self.months == i32::MIN
181 }
182
183 #[inline(always)]
184 pub const fn is_not_nat(&self) -> bool {
185 self.months != i32::MIN
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_parse_timedelta() {
195 let cases = vec![
196 (
197 "1d",
198 TimeDelta {
199 months: 0,
200 inner: Duration::days(1),
201 },
202 ),
203 (
204 "2w",
205 TimeDelta {
206 months: 0,
207 inner: Duration::weeks(2),
208 },
209 ),
210 (
211 "3mo",
212 TimeDelta {
213 months: 3,
214 inner: Duration::seconds(0),
215 },
216 ),
217 (
218 "1y2mo",
219 TimeDelta {
220 months: 14,
221 inner: Duration::seconds(0),
222 },
223 ),
224 (
225 "1d12h30m",
226 TimeDelta {
227 months: 0,
228 inner: Duration::days(1) + Duration::hours(12) + Duration::minutes(30),
229 },
230 ),
231 (
232 "1h30m45s",
233 TimeDelta {
234 months: 0,
235 inner: Duration::hours(1) + Duration::minutes(30) + Duration::seconds(45),
236 },
237 ),
238 (
239 "500ms",
240 TimeDelta {
241 months: 0,
242 inner: Duration::milliseconds(500),
243 },
244 ),
245 (
246 "1us",
247 TimeDelta {
248 months: 0,
249 inner: Duration::microseconds(1),
250 },
251 ),
252 (
253 "100ns",
254 TimeDelta {
255 months: 0,
256 inner: Duration::nanoseconds(100),
257 },
258 ),
259 ];
260
261 for (input, expected) in cases {
262 let result = TimeDelta::parse(input).unwrap();
263 assert_eq!(result, expected, "Failed for input: {}", input);
264 }
265 }
266
267 #[test]
268 fn test_parse_timedelta_errors() {
269 let error_cases = vec![
270 "1x", // Invalid unit
271 "1.5d", // Fractional values not supported
272 ];
273
274 for input in error_cases {
275 assert!(
276 TimeDelta::parse(input).is_err(),
277 "Expected error for input: {}",
278 input
279 );
280 }
281 }
282
283 #[test]
284 fn test_nat_timedelta() {
285 let nat = TimeDelta::nat();
286 assert!(nat.is_nat());
287 assert!(!nat.is_not_nat());
288
289 let non_nat = TimeDelta::parse("1d").unwrap();
290 assert!(!non_nat.is_nat());
291 assert!(non_nat.is_not_nat());
292 }
293}