dcbor/
date.rs

1import_stdlib!();
2
3#[cfg(not(feature = "std"))]
4use core::ops::{Add, Sub};
5#[cfg(feature = "std")]
6use std::ops::{Add, Sub};
7
8use chrono::{
9    DateTime, NaiveDate, NaiveDateTime, SecondsFormat, TimeZone, Timelike, Utc,
10};
11
12use crate::{
13    CBOR, CBORTagged, CBORTaggedDecodable, CBORTaggedEncodable, Error, Result,
14    TAG_DATE, Tag, tags_for_values,
15};
16
17/// A CBOR-friendly representation of a date and time.
18///
19/// The `Date` type provides a wrapper around `chrono::DateTime<Utc>` that
20/// supports encoding and decoding to/from CBOR with tag 1, following the CBOR
21/// date/time standard specified in [RFC 8949](https://www.rfc-editor.org/rfc/rfc8949.html#name-date-and-time-tag-1-and-co).
22///
23/// When encoded to CBOR, dates are represented as tag 1 followed by a numeric
24/// value representing the number of seconds since (or before) the Unix epoch
25/// (1970-01-01T00:00:00Z). The numeric value can be a positive or negative
26/// integer, or a floating-point value for dates with fractional seconds.
27///
28/// # Features
29///
30/// - Supports UTC dates with optional fractional seconds
31/// - Provides convenient constructors for common date creation patterns
32/// - Implements the [`CBORTagged`], [`CBORTaggedEncodable`], and
33///   [`CBORTaggedDecodable`] traits
34/// - Supports arithmetic operations with durations and between dates
35///
36/// # Examples
37///
38/// ```
39/// use dcbor::{Date, prelude::*};
40///
41/// // Create a date from a timestamp (seconds since Unix epoch)
42/// let date = Date::from_timestamp(1675854714.0);
43///
44/// // Create a date from year, month, day
45/// let date = Date::from_ymd(2023, 2, 8);
46///
47/// // Convert to CBOR
48/// let cbor = CBOR::from(date);
49///
50/// // Decode from CBOR
51/// let decoded_date: Date = cbor.try_into().unwrap();
52/// ```
53#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
54pub struct Date(DateTime<Utc>);
55
56impl Date {
57    /// Creates a new `Date` from the given chrono `DateTime`.
58    ///
59    /// This method creates a new `Date` instance by wrapping a
60    /// `chrono::DateTime<Utc>`.
61    ///
62    /// # Arguments
63    ///
64    /// * `date_time` - A `DateTime<Utc>` instance to wrap
65    ///
66    /// # Returns
67    ///
68    /// A new `Date` instance
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use chrono::{DateTime, Utc};
74    /// use dcbor::{Date, prelude::*};
75    ///
76    /// let datetime = Utc::now();
77    /// let date = Date::from_datetime(datetime);
78    /// ```
79    pub fn from_datetime(date_time: DateTime<Utc>) -> Self { Date(date_time) }
80
81    /// Creates a new `Date` from year, month, and day components.
82    ///
83    /// This method creates a new `Date` with the time set to 00:00:00 UTC.
84    ///
85    /// # Arguments
86    ///
87    /// * `year` - The year component (e.g., 2023)
88    /// * `month` - The month component (1-12)
89    /// * `day` - The day component (1-31)
90    ///
91    /// # Returns
92    ///
93    /// A new `Date` instance
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use dcbor::{Date, prelude::*};
99    ///
100    /// // Create February 8, 2023
101    /// let date = Date::from_ymd(2023, 2, 8);
102    /// ```
103    ///
104    /// # Panics
105    ///
106    /// This method panics if the provided components do not form a valid date.
107    pub fn from_ymd(year: i32, month: u32, day: u32) -> Self {
108        let dt = Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap();
109        Self::from_datetime(dt)
110    }
111
112    /// Creates a new `Date` from year, month, day, hour, minute, and second
113    /// components.
114    ///
115    /// # Arguments
116    ///
117    /// * `year` - The year component (e.g., 2023)
118    /// * `month` - The month component (1-12)
119    /// * `day` - The day component (1-31)
120    /// * `hour` - The hour component (0-23)
121    /// * `minute` - The minute component (0-59)
122    /// * `second` - The second component (0-59)
123    ///
124    /// # Returns
125    ///
126    /// A new `Date` instance
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use dcbor::{Date, prelude::*};
132    ///
133    /// // Create February 8, 2023, 15:30:45 UTC
134    /// let date = Date::from_ymd_hms(2023, 2, 8, 15, 30, 45);
135    /// ```
136    ///
137    /// # Panics
138    ///
139    /// This method panics if the provided components do not form a valid date
140    /// and time.
141    pub fn from_ymd_hms(
142        year: i32,
143        month: u32,
144        day: u32,
145        hour: u32,
146        minute: u32,
147        second: u32,
148    ) -> Self {
149        let dt = Utc
150            .with_ymd_and_hms(year, month, day, hour, minute, second)
151            .unwrap();
152        Self::from_datetime(dt)
153    }
154
155    /// Creates a new `Date` from seconds since (or before) the Unix epoch.
156    ///
157    /// This method creates a new `Date` representing the specified number of
158    /// seconds since the Unix epoch (1970-01-01T00:00:00Z). Negative values
159    /// represent times before the epoch.
160    ///
161    /// # Arguments
162    ///
163    /// * `seconds_since_unix_epoch` - Seconds from the Unix epoch (positive or
164    ///   negative), which can include a fractional part for sub-second
165    ///   precision
166    ///
167    /// # Returns
168    ///
169    /// A new `Date` instance
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use dcbor::{Date, prelude::*};
175    ///
176    /// // Create a date from a timestamp
177    /// let date = Date::from_timestamp(1675854714.0);
178    ///
179    /// // Create a date one second before the Unix epoch
180    /// let before_epoch = Date::from_timestamp(-1.0);
181    ///
182    /// // Create a date with fractional seconds
183    /// let with_fraction = Date::from_timestamp(1675854714.5);
184    /// ```
185    pub fn from_timestamp(seconds_since_unix_epoch: f64) -> Self {
186        let whole_seconds_since_unix_epoch =
187            seconds_since_unix_epoch.trunc() as i64;
188        let nsecs = (seconds_since_unix_epoch.fract() * 1_000_000_000.0) as u32;
189        Self::from_datetime(
190            Utc.timestamp_opt(whole_seconds_since_unix_epoch, nsecs)
191                .unwrap(),
192        )
193    }
194
195    /// Creates a new `Date` from a string containing an ISO-8601 (RFC-3339)
196    /// date (with or without time).
197    ///
198    /// This method parses a string representation of a date or date-time in
199    /// ISO-8601/RFC-3339 format and creates a new `Date` instance. It
200    /// supports both full date-time strings (e.g., "2023-02-08T15:30:45Z")
201    /// and date-only strings (e.g., "2023-02-08").
202    ///
203    /// # Arguments
204    ///
205    /// * `value` - A string containing a date or date-time in ISO-8601/RFC-3339
206    ///   format
207    ///
208    /// # Returns
209    ///
210    /// * `Ok(Date)` - A new `Date` instance if parsing succeeds
211    /// * `Err` - If the string cannot be parsed as a valid date or date-time
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use dcbor::{Date, prelude::*};
217    ///
218    /// // Parse a date-time string
219    /// let date = Date::from_string("2023-02-08T15:30:45Z").unwrap();
220    ///
221    /// // Parse a date-only string (time will be set to 00:00:00)
222    /// let date = Date::from_string("2023-02-08").unwrap();
223    /// ```
224    pub fn from_string(value: impl Into<String>) -> Result<Self> {
225        let value = value.into();
226        // try parsing as DateTime
227        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
228            return Ok(Self::from_datetime(dt.with_timezone(&Utc)));
229        }
230
231        // try parsing as just a date (with assumed zero time)
232        if let Ok(d) = NaiveDate::parse_from_str(&value, "%Y-%m-%d") {
233            let dt = NaiveDateTime::new(
234                d,
235                chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
236            );
237            return Ok(Self::from_datetime(
238                DateTime::from_naive_utc_and_offset(dt, Utc),
239            ));
240        }
241
242        Err(Error::InvalidDate("Invalid date string".into()))
243    }
244
245    /// Creates a new `Date` containing the current date and time.
246    ///
247    /// # Returns
248    ///
249    /// A new `Date` instance representing the current UTC date and time
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use dcbor::{Date, prelude::*};
255    ///
256    /// let now = Date::now();
257    /// ```
258    pub fn now() -> Self { Self::from_datetime(Utc::now()) }
259
260    /// Creates a new `Date` containing the current date and time plus the given
261    /// duration.
262    ///
263    /// # Arguments
264    ///
265    /// * `duration` - The duration to add to the current time
266    ///
267    /// # Returns
268    ///
269    /// A new `Date` instance representing the current UTC date and time plus
270    /// the duration
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use std::time::Duration;
276    ///
277    /// use dcbor::{Date, prelude::*};
278    ///
279    /// // Get a date 1 hour from now
280    /// let one_hour_later =
281    ///     Date::with_duration_from_now(Duration::from_secs(3600));
282    /// ```
283    pub fn with_duration_from_now(duration: Duration) -> Self {
284        Self::now() + duration
285    }
286
287    /// Returns the underlying chrono `DateTime` struct.
288    ///
289    /// This method provides access to the wrapped `chrono::DateTime<Utc>`
290    /// instance.
291    ///
292    /// # Returns
293    ///
294    /// The wrapped `DateTime<Utc>` instance
295    ///
296    /// # Examples
297    ///
298    /// ```
299    /// use chrono::Datelike;
300    /// use dcbor::{Date, prelude::*};
301    ///
302    /// let date = Date::now();
303    /// let datetime = date.datetime();
304    /// let year = datetime.year();
305    /// ```
306    pub fn datetime(&self) -> DateTime<Utc> { self.0 }
307
308    /// Returns the `Date` as the number of seconds since the Unix epoch.
309    ///
310    /// This method converts the date to a floating-point number representing
311    /// the number of seconds since the Unix epoch (1970-01-01T00:00:00Z).
312    /// Negative values represent times before the epoch. The fractional
313    /// part represents sub-second precision.
314    ///
315    /// # Returns
316    ///
317    /// Seconds since the Unix epoch as a `f64`
318    ///
319    /// # Examples
320    ///
321    /// ```
322    /// use dcbor::{Date, prelude::*};
323    ///
324    /// let date = Date::from_ymd(2023, 2, 8);
325    /// let timestamp = date.timestamp();
326    /// ```
327    pub fn timestamp(&self) -> f64 {
328        let d = self.datetime();
329        let whole_seconds_since_unix_epoch = d.timestamp();
330        let nsecs = d.nanosecond();
331        (whole_seconds_since_unix_epoch as f64)
332            + (nsecs as f64) / 1_000_000_000.0
333    }
334}
335
336// Support adding seconds as f64
337impl Add<f64> for Date {
338    type Output = Self;
339
340    fn add(self, rhs: f64) -> Self::Output {
341        Self::from_timestamp(self.timestamp() + rhs)
342    }
343}
344
345// Support subtracting seconds as f64
346impl Sub<f64> for Date {
347    type Output = Self;
348
349    fn sub(self, rhs: f64) -> Self::Output {
350        Self::from_timestamp(self.timestamp() - rhs)
351    }
352}
353
354// Support adding a duration
355impl Add<Duration> for Date {
356    type Output = Self;
357
358    fn add(self, rhs: Duration) -> Self::Output {
359        Self::from_timestamp(self.timestamp() + rhs.as_secs_f64())
360    }
361}
362
363// Support subtracting a duration
364impl Sub<Duration> for Date {
365    type Output = Self;
366
367    fn sub(self, rhs: Duration) -> Self::Output {
368        Self::from_timestamp(self.timestamp() - rhs.as_secs_f64())
369    }
370}
371
372// Support subtracting another date and returning the number of seconds as f64
373impl Sub for Date {
374    type Output = f64;
375
376    fn sub(self, rhs: Self) -> Self::Output {
377        self.timestamp() - rhs.timestamp()
378    }
379}
380
381impl Default for Date {
382    fn default() -> Self { Self::now() }
383}
384
385impl TryFrom<&str> for Date {
386    type Error = Error;
387
388    fn try_from(value: &str) -> Result<Self> { Self::from_string(value) }
389}
390
391impl From<DateTime<Utc>> for Date {
392    fn from(value: DateTime<Utc>) -> Self { Self::from_datetime(value) }
393}
394
395impl From<Date> for CBOR {
396    fn from(value: Date) -> Self { value.tagged_cbor() }
397}
398
399impl AsRef<Date> for Date {
400    fn as_ref(&self) -> &Self { self }
401}
402
403impl TryFrom<CBOR> for Date {
404    type Error = Error;
405
406    fn try_from(cbor: CBOR) -> Result<Self> { Self::from_tagged_cbor(cbor) }
407}
408
409/// Implementation of the `CBORTagged` trait for `Date`.
410///
411/// This implementation specifies that `Date` values are tagged with CBOR tag 1,
412/// which is the standard CBOR tag for date/time values represented as seconds
413/// since the Unix epoch per RFC 8949.
414impl CBORTagged for Date {
415    /// Returns the CBOR tags associated with the `Date` type.
416    ///
417    /// For dates, this is always tag 1, which is the standard CBOR tag for
418    /// date/time values represented as seconds since the Unix epoch.
419    ///
420    /// # Returns
421    ///
422    /// A vector containing tag 1
423    fn cbor_tags() -> Vec<Tag> { tags_for_values(&[TAG_DATE]) }
424}
425
426/// Implementation of the `CBORTaggedEncodable` trait for `Date`.
427///
428/// This implementation converts a `Date` to an untagged CBOR value
429/// representing the number of seconds since the Unix epoch.
430impl CBORTaggedEncodable for Date {
431    /// Converts this `Date` to an untagged CBOR value.
432    ///
433    /// The date is converted to a numeric value representing the number of
434    /// seconds since the Unix epoch. This value may be an integer or a
435    /// floating-point number, depending on whether the date has fractional
436    /// seconds.
437    ///
438    /// # Returns
439    ///
440    /// A CBOR value representing the timestamp
441    fn untagged_cbor(&self) -> CBOR { self.timestamp().into() }
442}
443
444/// Implementation of the `CBORTaggedDecodable` trait for `Date`.
445///
446/// This implementation creates a `Date` from an untagged CBOR value
447/// representing seconds since the Unix epoch.
448impl CBORTaggedDecodable for Date {
449    /// Creates a `Date` from an untagged CBOR value.
450    ///
451    /// The CBOR value must be a numeric value (integer or floating-point)
452    /// representing the number of seconds since the Unix epoch.
453    ///
454    /// # Arguments
455    ///
456    /// * `cbor` - The untagged CBOR value
457    ///
458    /// # Returns
459    ///
460    /// * `Ok(Date)` - A new `Date` instance if decoding succeeds
461    /// * `Err` - If the CBOR value is not a valid timestamp
462    fn from_untagged_cbor(cbor: CBOR) -> Result<Self> {
463        let n = cbor.clone().try_into()?;
464        Ok(Date::from_timestamp(n))
465    }
466}
467
468/// Implementation of the `Display` trait for `Date`.
469///
470/// This implementation provides a string representation of a `Date` in ISO-8601
471/// format. For dates with time exactly at midnight (00:00:00), only the date
472/// part is shown. For other times, a full date-time string is shown.
473impl fmt::Display for Date {
474    /// Formats the `Date` as a string in ISO-8601 format.
475    ///
476    /// # Examples
477    ///
478    /// ```
479    /// use dcbor::{Date, prelude::*};
480    ///
481    /// // A date at midnight will display as just the date
482    /// let date = Date::from_ymd(2023, 2, 8);
483    /// assert_eq!(date.to_string(), "2023-02-08");
484    ///
485    /// // A date with time will display as date and time
486    /// let date = Date::from_ymd_hms(2023, 2, 8, 15, 30, 45);
487    /// assert_eq!(date.to_string(), "2023-02-08T15:30:45Z");
488    /// ```
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        let dt = self.datetime();
491        if dt.hour() == 0 && dt.minute() == 0 && dt.second() == 0 {
492            f.write_str(dt.date_naive().to_string().as_str())
493        } else {
494            f.write_str(dt.to_rfc3339_opts(SecondsFormat::Secs, true).as_str())
495        }
496    }
497}