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