1use crate::error::{Result, RosettaError};
4use crate::timezone::TzOffset;
5
6#[derive(Debug, Clone)]
9pub enum RosettaDateTime {
10 #[cfg(feature = "time-backend")]
11 Time(time::OffsetDateTime),
12
13 #[cfg(feature = "chrono-backend")]
14 Chrono(chrono::DateTime<chrono::FixedOffset>),
15}
16
17impl PartialEq for RosettaDateTime {
18 fn eq(&self, other: &Self) -> bool {
19 self.timestamp() == other.timestamp()
20 }
21}
22impl Eq for RosettaDateTime {}
23
24impl PartialOrd for RosettaDateTime {
25 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
26 Some(self.cmp(other))
27 }
28}
29impl Ord for RosettaDateTime {
30 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
31 self.timestamp().cmp(&other.timestamp())
32 }
33}
34
35impl RosettaDateTime {
38 pub fn from_components(
40 year: i32,
41 month: u8,
42 day: u8,
43 hour: u8,
44 minute: u8,
45 second: u8,
46 offset: TzOffset,
47 ) -> Result<Self> {
48 #[cfg(feature = "time-backend")]
49 {
50 let date = time::Date::from_calendar_date(
51 year,
52 time::Month::try_from(month)
53 .map_err(|e| RosettaError::ParseError(e.to_string()))?,
54 day,
55 )
56 .map_err(|e| RosettaError::ParseError(e.to_string()))?;
57
58 let time_val = time::Time::from_hms(hour, minute, second)
59 .map_err(|e| RosettaError::ParseError(e.to_string()))?;
60
61 let tz = time::UtcOffset::from_whole_seconds(offset.total_seconds)
62 .map_err(|e| RosettaError::TimezoneError(e.to_string()))?;
63
64 Ok(Self::Time(
65 time::PrimitiveDateTime::new(date, time_val).assume_offset(tz),
66 ))
67 }
68
69 #[cfg(all(feature = "chrono-backend", not(feature = "time-backend")))]
70 {
71 use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime};
72 let nd = NaiveDate::from_ymd_opt(year, month as u32, day as u32)
73 .ok_or_else(|| RosettaError::ParseError("Invalid date".into()))?;
74 let nt = NaiveTime::from_hms_opt(hour as u32, minute as u32, second as u32)
75 .ok_or_else(|| RosettaError::ParseError("Invalid time".into()))?;
76 let ndt = NaiveDateTime::new(nd, nt);
77 let fo = FixedOffset::east_opt(offset.total_seconds)
78 .ok_or_else(|| RosettaError::TimezoneError("Invalid offset".into()))?;
79 Ok(Self::Chrono(
80 DateTime::<FixedOffset>::from_naive_utc_and_offset(ndt - fo, fo),
81 ))
82 }
83
84 #[cfg(not(any(feature = "time-backend", feature = "chrono-backend")))]
85 {
86 let _ = (year, month, day, hour, minute, second, offset);
87 Err(RosettaError::ParseError(
88 "No backend feature enabled".into(),
89 ))
90 }
91 }
92
93 pub fn now_utc() -> Self {
95 #[cfg(feature = "time-backend")]
96 {
97 Self::Time(time::OffsetDateTime::now_utc())
98 }
99
100 #[cfg(all(feature = "chrono-backend", not(feature = "time-backend")))]
101 {
102 Self::Chrono(chrono::Utc::now().fixed_offset())
103 }
104
105 #[cfg(not(any(feature = "time-backend", feature = "chrono-backend")))]
106 {
107 panic!("No backend feature enabled")
108 }
109 }
110}
111
112impl RosettaDateTime {
115 pub fn timestamp(&self) -> i64 {
117 match self {
118 #[cfg(feature = "time-backend")]
119 Self::Time(dt) => dt.unix_timestamp(),
120 #[cfg(feature = "chrono-backend")]
121 Self::Chrono(dt) => dt.timestamp(),
122 }
123 }
124
125 pub fn year(&self) -> i32 {
126 match self {
127 #[cfg(feature = "time-backend")]
128 Self::Time(dt) => dt.year(),
129 #[cfg(feature = "chrono-backend")]
130 Self::Chrono(dt) => chrono::Datelike::year(dt),
131 }
132 }
133
134 pub fn month(&self) -> u8 {
136 match self {
137 #[cfg(feature = "time-backend")]
138 Self::Time(dt) => dt.month() as u8,
139 #[cfg(feature = "chrono-backend")]
140 Self::Chrono(dt) => chrono::Datelike::month(dt) as u8,
141 }
142 }
143
144 pub fn day(&self) -> u8 {
146 match self {
147 #[cfg(feature = "time-backend")]
148 Self::Time(dt) => dt.day(),
149 #[cfg(feature = "chrono-backend")]
150 Self::Chrono(dt) => chrono::Datelike::day(dt) as u8,
151 }
152 }
153
154 pub fn hour(&self) -> u8 {
155 match self {
156 #[cfg(feature = "time-backend")]
157 Self::Time(dt) => dt.hour(),
158 #[cfg(feature = "chrono-backend")]
159 Self::Chrono(dt) => chrono::Timelike::hour(dt) as u8,
160 }
161 }
162
163 pub fn minute(&self) -> u8 {
164 match self {
165 #[cfg(feature = "time-backend")]
166 Self::Time(dt) => dt.minute(),
167 #[cfg(feature = "chrono-backend")]
168 Self::Chrono(dt) => chrono::Timelike::minute(dt) as u8,
169 }
170 }
171
172 pub fn second(&self) -> u8 {
173 match self {
174 #[cfg(feature = "time-backend")]
175 Self::Time(dt) => dt.second(),
176 #[cfg(feature = "chrono-backend")]
177 Self::Chrono(dt) => chrono::Timelike::second(dt) as u8,
178 }
179 }
180
181 pub fn weekday(&self) -> u8 {
183 match self {
184 #[cfg(feature = "time-backend")]
185 Self::Time(dt) => dt.weekday().number_days_from_monday(),
186 #[cfg(feature = "chrono-backend")]
187 Self::Chrono(dt) => chrono::Datelike::weekday(dt).num_days_from_monday() as u8,
188 }
189 }
190
191 pub fn offset_seconds(&self) -> i32 {
193 match self {
194 #[cfg(feature = "time-backend")]
195 Self::Time(dt) => dt.offset().whole_seconds(),
196 #[cfg(feature = "chrono-backend")]
197 Self::Chrono(dt) => {
198 use chrono::Offset;
199 dt.offset().fix().local_minus_utc()
200 }
201 }
202 }
203
204 pub fn offset(&self) -> TzOffset {
205 TzOffset {
206 total_seconds: self.offset_seconds(),
207 }
208 }
209}
210
211impl RosettaDateTime {
214 pub fn add_seconds(self, secs: i64) -> Self {
216 match self {
217 #[cfg(feature = "time-backend")]
218 Self::Time(dt) => Self::Time(dt + time::Duration::seconds(secs)),
219 #[cfg(feature = "chrono-backend")]
220 Self::Chrono(dt) => Self::Chrono(dt + chrono::Duration::seconds(secs)),
221 }
222 }
223
224 pub fn add_minutes(self, mins: i64) -> Self {
226 self.add_seconds(mins * 60)
227 }
228
229 pub fn add_hours(self, hrs: i64) -> Self {
231 self.add_seconds(hrs * 3600)
232 }
233
234 pub fn add_days(self, days: i64) -> Self {
236 self.add_seconds(days * 86400)
237 }
238
239 pub fn add_weeks(self, weeks: i64) -> Self {
241 self.add_days(weeks * 7)
242 }
243
244 pub fn add_months_approx(self, months: i64) -> Self {
246 self.add_days(months * 30)
247 }
248
249 pub fn add_years_approx(self, years: i64) -> Self {
251 self.add_days(years * 365)
252 }
253
254 pub fn to_offset(self, new_offset: TzOffset) -> Result<Self> {
256 match self {
257 #[cfg(feature = "time-backend")]
258 Self::Time(dt) => {
259 let tz = time::UtcOffset::from_whole_seconds(new_offset.total_seconds)
260 .map_err(|e| RosettaError::TimezoneError(e.to_string()))?;
261 Ok(Self::Time(dt.to_offset(tz)))
262 }
263 #[cfg(feature = "chrono-backend")]
264 Self::Chrono(dt) => {
265 let fo = chrono::FixedOffset::east_opt(new_offset.total_seconds)
266 .ok_or_else(|| RosettaError::TimezoneError("Invalid offset".into()))?;
267 Ok(Self::Chrono(dt.with_timezone(&fo)))
268 }
269 }
270 }
271}
272
273impl std::fmt::Display for RosettaDateTime {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 write!(
279 f,
280 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}",
281 self.year(),
282 self.month(),
283 self.day(),
284 self.hour(),
285 self.minute(),
286 self.second(),
287 self.offset(),
288 )
289 }
290}