proto_types/common/
date.rs1use 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#[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#[derive(Debug, Clone, Eq, PartialEq, Copy)]
38pub enum DateKind {
39 Full,
41 YearOnly,
43 YearAndMonth,
45 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 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 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 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 pub fn is_year_only(&self) -> bool {
124 self.year != 0 && (self.month == 0 && self.day == 0)
125 }
126
127 pub fn is_year_and_month(&self) -> bool {
129 self.year != 0 && self.month != 0 && self.day == 0
130 }
131
132 pub fn is_month_and_day(&self) -> bool {
134 self.year == 0 && self.month != 0 && self.day != 0
135 }
136
137 #[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 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 Date {
198 year: naive_date.year(),
199 month: naive_date.month() as i32,
200 day: naive_date.day() as i32,
201 }
202 }
203}