proto_types/common/
date.rs

1use std::{
2  cmp::{Ord, Ordering, PartialOrd},
3  convert::TryFrom,
4  fmt::Display,
5};
6
7use ::prost::alloc::string::String;
8use thiserror::Error;
9
10use crate::common::Date;
11
12/// Errors that can occur during the creation, conversion or validation of a [`Date`].
13#[derive(Debug, Error, PartialEq, Eq, Clone)]
14pub enum DateError {
15  #[error("{0}")]
16  InvalidYear(String),
17  #[error("{0}")]
18  InvalidMonth(String),
19  #[error("{0}")]
20  InvalidDay(String),
21  #[error("Date conversion error: {0}")]
22  ConversionError(String),
23}
24
25impl Display for Date {
26  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27    match self.kind() {
28      DateKind::Full => write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day),
29      DateKind::YearAndMonth => write!(f, "{:04}-{:02}", self.year, self.month),
30      DateKind::YearOnly => write!(f, "{:04}", self.year),
31      DateKind::MonthAndDay => write!(f, "{:02}-{:02}", self.month, self.day),
32    }
33  }
34}
35
36/// The kind of combinations that a [`Date`] can contain.
37#[derive(Debug, Clone, Eq, PartialEq, Copy)]
38pub enum DateKind {
39  /// A full date, with non-zero year, month, and day values
40  Full,
41  /// A year on its own, with zero month and day values
42  YearOnly,
43  /// A year and month value, with a zero day, such as a credit card expiration
44  YearAndMonth,
45  /// A month and day value, with a zero year, such as an anniversary
46  MonthAndDay,
47}
48
49fn validate_date(year: i32, month: i32, day: i32) -> Result<(), DateError> {
50  if !(0..=9999).contains(&year) {
51    return Err(DateError::InvalidYear(
52      "Invalid year value (must be within 0 (to indicate a date without a specific year) and 9999)"
53        .to_string(),
54    ));
55  }
56
57  if !(0..=12).contains(&month) {
58    return Err(DateError::InvalidMonth(
59      "Invalid month value (must be within 0 (if only the year is specified) and 12)".to_string(),
60    ));
61  }
62
63  if !(0..=31).contains(&day) {
64    return Err(DateError::InvalidDay(
65      "Invalid day value (must be within 0 (if only the year is specified) and 31)".to_string(),
66    ));
67  }
68
69  if year == 0 {
70    if month == 0 {
71      return Err(DateError::InvalidMonth(
72        "The month cannot be set to 0 if the year is also set to 0".to_string(),
73      ));
74    }
75
76    if day == 0 {
77      return Err(DateError::InvalidDay(
78        "The day cannot be set to 0 if the year is also set to 0".to_string(),
79      ));
80    }
81  } else if month == 0 {
82    return Err(DateError::InvalidMonth(
83      "The month cannot be set to 0 if the year is non-zero".to_string(),
84    ));
85  }
86
87  Ok(())
88}
89
90impl Date {
91  /// Creates a new [`Date`] instance with validation.
92  /// Allows `year: 0`, `month: 0`, `day: 0` as special cases described in the proto spec.
93  /// Returns an error if any component is out of range or date is invalid (e.g., February 30th).
94  pub fn new(year: i32, month: i32, day: i32) -> Result<Self, DateError> {
95    validate_date(year, month, day)?;
96
97    Ok(Date { year, month, day })
98  }
99
100  /// Returns the kind of values combination for this [`Date`]
101  pub fn kind(&self) -> DateKind {
102    if self.year != 0 && self.month == 0 && self.day == 0 {
103      DateKind::YearOnly
104    } else if self.year != 0 && self.month != 0 && self.day == 0 {
105      DateKind::YearAndMonth
106    } else if self.year == 0 && self.month != 0 && self.day != 0 {
107      DateKind::MonthAndDay
108    } else {
109      DateKind::Full
110    }
111  }
112
113  /// Checks if this [`Date`] instance represents a valid date according to its constraints.
114  pub fn is_valid(&self) -> bool {
115    validate_date(self.year, self.month, self.day).is_ok()
116  }
117
118  pub fn has_year(&self) -> bool {
119    self.year != 0
120  }
121
122  /// Returns `true` if this [`Date`] only indicates a year.
123  pub fn is_year_only(&self) -> bool {
124    self.year != 0 && (self.month == 0 && self.day == 0)
125  }
126
127  /// Returns `true` if this [`Date`] only indicates a year and a month (i.e. for a credit card expiration date).
128  pub fn is_year_and_month(&self) -> bool {
129    self.year != 0 && self.month != 0 && self.day == 0
130  }
131
132  /// Returns `true` if this [`Date`] only indicates a month and a day, with no specific year.
133  pub fn is_month_and_day(&self) -> bool {
134    self.year == 0 && self.month != 0 && self.day != 0
135  }
136
137  /// Converts this [`Date`] to [`chrono::NaiveDate`]. It fails if the year, month or day are set to zero.
138  #[cfg(feature = "chrono")]
139  pub fn to_naive_date(self) -> Result<chrono::NaiveDate, DateError> {
140    self.try_into()
141  }
142}
143
144impl PartialOrd for Date {
145  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
146    if !(self.is_valid() && other.is_valid()) {
147      return None;
148    }
149
150    let self_kind = self.kind();
151    let other_kind = other.kind();
152
153    if self_kind != other_kind {
154      return None;
155    }
156
157    Some(
158      self
159        .year
160        .cmp(&other.year)
161        .then_with(|| self.month.cmp(&other.month))
162        .then_with(|| self.day.cmp(&other.day)),
163    )
164  }
165}
166
167#[cfg(feature = "chrono")]
168impl TryFrom<Date> for chrono::NaiveDate {
169  type Error = DateError;
170
171  fn try_from(date: Date) -> Result<Self, Self::Error> {
172    if date.year == 0 || date.month == 0 || date.day == 0 {
173      return Err(DateError::ConversionError(
174        "Cannot convert Date with year=0, month=0, or day=0 to NaiveDate".to_string(),
175      ));
176    }
177
178    validate_date(date.year, date.month, date.day)?;
179
180    // Safe castings after validation
181    chrono::NaiveDate::from_ymd_opt(date.year, date.month as u32, date.day as u32).ok_or_else(
182      || {
183        DateError::ConversionError(format!(
184          "Invalid date components for NaiveDate: Y:{}, M:{}, D:{}",
185          date.year, date.month, date.day
186        ))
187      },
188    )
189  }
190}
191
192#[cfg(feature = "chrono")]
193impl From<chrono::NaiveDate> for Date {
194  fn from(naive_date: chrono::NaiveDate) -> Self {
195    use chrono::Datelike;
196    // Casting is safe due to chrono's costructor API
197    Date {
198      year: naive_date.year(),
199      month: naive_date.month() as i32,
200      day: naive_date.day() as i32,
201    }
202  }
203}