tea_time/
datetime.rs

1use std::cmp::Ordering;
2use std::hash::Hash;
3use std::marker::PhantomData;
4
5use chrono::{
6    DateTime as CrDateTime, Datelike, DurationRound, Months, NaiveDate, NaiveDateTime, NaiveTime,
7    Timelike, Utc,
8};
9use tea_error::{tbail, TResult};
10
11use super::timeunit::*;
12use crate::TimeDelta;
13
14#[derive(Clone, Copy, Hash, Eq, PartialEq, PartialOrd, Ord)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[repr(transparent)]
17/// Represents a date and time with a specific time unit precision.
18///
19/// # Type Parameters
20///
21/// * `U`: The time unit precision, defaulting to `Nanosecond`. Must implement `TimeUnitTrait`.
22///
23/// # Fields
24///
25/// * `0`: An `i64` representing the timestamp in the specified time unit.
26/// * `PhantomData<U>`: A zero-sized type used to "mark" the time unit without affecting the struct's memory layout.
27pub struct DateTime<U: TimeUnitTrait = Nanosecond>(pub i64, PhantomData<U>);
28
29impl<U: TimeUnitTrait> std::fmt::Debug for DateTime<U>
30where
31    Self: TryInto<CrDateTime<Utc>>,
32    <Self as TryInto<CrDateTime<Utc>>>::Error: std::fmt::Debug,
33{
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        if self.is_nat() {
36            write!(f, "NaT")
37        } else {
38            write!(f, "{}", self.strftime(None))
39        }
40    }
41}
42
43unsafe impl<U: TimeUnitTrait> Send for DateTime<U> {}
44unsafe impl<U: TimeUnitTrait> Sync for DateTime<U> {}
45
46const TIME_RULE_VEC: [&str; 11] = [
47    "%Y-%m-%d %H:%M:%S",
48    "%Y-%m-%d %H:%M:%S.%f",
49    "%Y-%m-%d",
50    "%Y%m%d",
51    "%Y%m%d %H%M%S",
52    "%d/%m/%Y",
53    "%d/%m/%Y H%M%S",
54    "%Y%m%d%H%M%S",
55    "%d/%m/%YH%M%S",
56    "%Y/%m/%d",
57    "%Y/%m/%d %H:%M:%S",
58];
59
60impl<U: TimeUnitTrait> DateTime<U> {
61    /// Creates a new `DateTime` instance with the given timestamp.
62    ///
63    /// # Arguments
64    ///
65    /// * `dt` - An `i64` representing the timestamp in the specified time unit.
66    ///
67    /// # Returns
68    ///
69    /// A new `DateTime<U>` instance.
70    #[inline]
71    pub const fn new(dt: i64) -> Self {
72        Self(dt, PhantomData)
73    }
74
75    /// Checks if the `DateTime` instance represents "Not-a-Time" (NaT).
76    ///
77    /// # Returns
78    ///
79    /// `true` if the instance is NaT, `false` otherwise.
80    #[inline]
81    pub const fn is_nat(&self) -> bool {
82        self.0 == i64::MIN
83    }
84
85    /// Checks if the `DateTime` instance represents a valid time (not NaT).
86    ///
87    /// # Returns
88    ///
89    /// `true` if the instance is not NaT, `false` otherwise.
90    #[inline]
91    pub const fn is_not_nat(&self) -> bool {
92        self.0 != i64::MIN
93    }
94
95    /// Creates a new `DateTime` instance representing "Not-a-Time" (NaT).
96    ///
97    /// # Returns
98    ///
99    /// A new `DateTime<U>` instance representing NaT.
100    #[inline]
101    pub const fn nat() -> Self {
102        Self(i64::MIN, PhantomData)
103    }
104
105    /// Converts the `DateTime` instance to its underlying `i64` timestamp.
106    ///
107    /// # Returns
108    ///
109    /// The `i64` timestamp value.
110    #[inline]
111    pub const fn into_i64(self) -> i64 {
112        self.0
113    }
114
115    /// Creates a `DateTime` instance from an optional `i64` timestamp.
116    ///
117    /// # Arguments
118    ///
119    /// * `v` - An `Option<i64>` representing the timestamp.
120    ///
121    /// # Returns
122    ///
123    /// A new `DateTime<U>` instance. If `v` is `None`, returns NaT.
124    #[inline]
125    pub const fn from_opt_i64(v: Option<i64>) -> Self {
126        if let Some(v) = v {
127            Self::new(v)
128        } else {
129            Self::nat()
130        }
131    }
132
133    /// Converts the `DateTime` instance to an optional `i64` timestamp.
134    ///
135    /// # Returns
136    ///
137    /// `Some(i64)` if the instance is not NaT, `None` otherwise.
138    #[inline]
139    pub const fn into_opt_i64(self) -> Option<i64> {
140        if self.is_nat() {
141            None
142        } else {
143            Some(self.0)
144        }
145    }
146
147    /// Converts the `DateTime` instance to a `chrono::DateTime<Utc>`.
148    ///
149    /// # Returns
150    ///
151    /// `Some(CrDateTime<Utc>)` if the conversion is successful, `None` if the instance is NaT.
152    #[inline]
153    #[deprecated(since = "0.5.0", note = "use `as_cr` instead")]
154    pub fn to_cr(&self) -> Option<CrDateTime<Utc>>
155    where
156        Self: TryInto<CrDateTime<Utc>>,
157    {
158        self.as_cr()
159    }
160
161    #[inline]
162    pub fn as_cr(&self) -> Option<CrDateTime<Utc>>
163    where
164        Self: TryInto<CrDateTime<Utc>>,
165    {
166        if self.is_nat() {
167            None
168        } else {
169            (*self).try_into().ok()
170        }
171    }
172
173    /// Parses a string into a `DateTime` instance.
174    ///
175    /// # Arguments
176    ///
177    /// * `s` - The string to parse.
178    /// * `fmt` - An optional format string. If `None`, tries multiple common formats.
179    ///
180    /// # Returns
181    ///
182    /// A `TResult<Self>` containing the parsed `DateTime` or an error.
183    #[inline(always)]
184    pub fn parse(s: &str, fmt: Option<&str>) -> TResult<Self>
185    where
186        Self: From<CrDateTime<Utc>>,
187    {
188        if let Some(fmt) = fmt {
189            if let Ok(cr_dt) = NaiveDateTime::parse_from_str(s, fmt) {
190                Ok(cr_dt.into())
191            } else if let Ok(cr_date) = NaiveDate::parse_from_str(s, fmt) {
192                Ok(cr_date.into())
193            } else {
194                tbail!(ParseError:"Failed to parse datetime from string: {}", s)
195            }
196        } else {
197            for fmt in TIME_RULE_VEC.iter() {
198                if let Ok(cr_dt) = NaiveDateTime::parse_from_str(s, fmt) {
199                    return Ok(cr_dt.into());
200                } else if let Ok(cr_date) = NaiveDate::parse_from_str(s, fmt) {
201                    return Ok(cr_date.into());
202                }
203            }
204            tbail!(ParseError:"Failed to parse datetime from string: {}", s)
205        }
206    }
207
208    /// Formats the `DateTime` instance as a string.
209    ///
210    /// # Arguments
211    ///
212    /// * `fmt` - An optional format string. If `None`, uses "%Y-%m-%d %H:%M:%S.%f".
213    ///
214    /// # Returns
215    ///
216    /// A formatted string representation of the `DateTime`.
217    #[inline]
218    pub fn strftime(&self, fmt: Option<&str>) -> String
219    where
220        Self: TryInto<CrDateTime<Utc>>,
221        <Self as TryInto<CrDateTime<Utc>>>::Error: std::fmt::Debug,
222    {
223        if self.is_nat() {
224            "NaT".to_string()
225        } else {
226            let fmt = fmt.unwrap_or("%Y-%m-%d %H:%M:%S.%f");
227            self.as_cr().unwrap().format(fmt).to_string()
228        }
229    }
230
231    /// Truncates the `DateTime` to a specified duration.
232    ///
233    /// # Arguments
234    ///
235    /// * `duration` - A `TimeDelta` specifying the truncation interval.
236    ///
237    /// # Returns
238    ///
239    /// A new `DateTime<U>` instance truncated to the specified duration.
240    pub fn duration_trunc(self, duration: TimeDelta) -> Self
241    where
242        Self: TryInto<CrDateTime<Utc>> + From<CrDateTime<Utc>>,
243        <Self as TryInto<CrDateTime<Utc>>>::Error: std::fmt::Debug,
244    {
245        if self.is_nat() {
246            return self;
247        }
248        let mut dt = self.as_cr().unwrap();
249        let dm = duration.months;
250        if dm != 0 {
251            let (flag, dt_year) = dt.year_ce();
252            if dm < 0 {
253                unimplemented!("not support year before ce or negative month")
254            }
255            let dt_month = if flag {
256                (dt_year * 12 + dt.month()) as i32
257            } else {
258                dt_year as i32 * (-12) + dt.month() as i32
259            };
260            let delta_down = dt_month % dm;
261            dt = match delta_down.cmp(&0) {
262                Ordering::Equal => dt,
263                Ordering::Greater => dt - Months::new(delta_down as u32),
264                Ordering::Less => dt - Months::new((dm - delta_down.abs()) as u32),
265            };
266            if let Some(nd) = duration.inner.num_nanoseconds() {
267                if nd == 0 {
268                    return dt.into();
269                }
270            }
271        }
272        dt.duration_trunc(duration.inner)
273            .expect("Rounding Error")
274            .into()
275    }
276}
277
278impl<U: TimeUnitTrait> DateTime<U>
279where
280    Self: TryInto<CrDateTime<Utc>>,
281{
282    /// Returns the time component of the DateTime as a NaiveTime.
283    ///
284    /// # Returns
285    ///
286    /// `Option<NaiveTime>`: The time component if the DateTime is valid, or None if it's NaT.
287    #[inline(always)]
288    pub fn time(&self) -> Option<NaiveTime> {
289        self.as_cr().map(|dt| dt.time())
290    }
291
292    /// Returns the year.
293    ///
294    /// # Returns
295    ///
296    /// `Option<i32>`: The year if the DateTime is valid, or None if it's NaT.
297    #[inline(always)]
298    pub fn year(&self) -> Option<i32> {
299        self.as_cr().map(|dt| dt.year())
300    }
301
302    /// Returns the day of the month (1-31).
303    ///
304    /// # Returns
305    ///
306    /// `Option<usize>`: The day of the month if the DateTime is valid, or None if it's NaT.
307    #[inline(always)]
308    pub fn day(&self) -> Option<usize> {
309        self.as_cr().map(|dt| dt.day() as usize)
310    }
311
312    /// Returns the month (1-12).
313    ///
314    /// # Returns
315    ///
316    /// `Option<usize>`: The month if the DateTime is valid, or None if it's NaT.
317    #[inline(always)]
318    pub fn month(&self) -> Option<usize> {
319        self.as_cr().map(|dt| dt.month() as usize)
320    }
321
322    /// Returns the hour (0-23).
323    ///
324    /// # Returns
325    ///
326    /// `Option<usize>`: The hour if the DateTime is valid, or None if it's NaT.
327    #[inline(always)]
328    pub fn hour(&self) -> Option<usize> {
329        self.as_cr().map(|dt| dt.hour() as usize)
330    }
331
332    /// Returns the minute (0-59).
333    ///
334    /// # Returns
335    ///
336    /// `Option<usize>`: The minute if the DateTime is valid, or None if it's NaT.
337    #[inline(always)]
338    pub fn minute(&self) -> Option<usize> {
339        self.as_cr().map(|dt| dt.minute() as usize)
340    }
341
342    /// Returns the second (0-59).
343    ///
344    /// # Returns
345    ///
346    /// `Option<usize>`: The second if the DateTime is valid, or None if it's NaT.
347    #[inline(always)]
348    pub fn second(&self) -> Option<usize> {
349        self.as_cr().map(|dt| dt.second() as usize)
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    #[allow(unused_assignments, unused_variables)]
359    fn test_parse_datetime() -> TResult<()> {
360        let mut dt: DateTime = "2020-01-01 00:00:00".parse()?;
361        dt = DateTime::parse("2020-01-01", Some("%Y-%m-%d"))?;
362        dt = "2020-01-01".parse()?;
363        dt = "20220101".parse()?;
364        dt = "2021/02/03".parse()?;
365        Ok(())
366    }
367
368    #[test]
369    fn test_datetime_components() -> TResult<()> {
370        let dt: DateTime = "2023-05-15 14:30:45".parse()?;
371        assert_eq!(dt.year(), Some(2023));
372        assert_eq!(dt.month(), Some(5));
373        assert_eq!(dt.day(), Some(15));
374        assert_eq!(dt.hour(), Some(14));
375        assert_eq!(dt.minute(), Some(30));
376        assert_eq!(dt.second(), Some(45));
377        Ok(())
378    }
379
380    #[test]
381    fn test_nat_datetime() {
382        let nat_dt: DateTime = DateTime::nat();
383        assert!(nat_dt.is_nat());
384        assert_eq!(nat_dt.year(), None);
385        assert_eq!(nat_dt.month(), None);
386        assert_eq!(nat_dt.day(), None);
387        assert_eq!(nat_dt.hour(), None);
388        assert_eq!(nat_dt.minute(), None);
389        assert_eq!(nat_dt.second(), None);
390    }
391
392    #[test]
393    fn test_invalid_datetime_parse() {
394        assert!("invalid date".parse::<DateTime>().is_err());
395    }
396}