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}