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}