Skip to main content

reifydb_type/value/
date.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::fmt::{self, Display, Formatter};
5
6use serde::{
7	Deserialize, Deserializer, Serialize, Serializer,
8	de::{self, Visitor},
9};
10
11use crate::{
12	error::{TemporalKind, TypeError},
13	fragment::Fragment,
14};
15
16#[repr(transparent)]
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
18pub struct Date {
19	days_since_epoch: i32,
20}
21
22impl Date {
23	#[inline]
24	pub fn is_leap_year(year: i32) -> bool {
25		(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
26	}
27
28	#[inline]
29	pub fn days_in_month(year: i32, month: u32) -> u32 {
30		match month {
31			1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
32			4 | 6 | 9 | 11 => 30,
33			2 => {
34				if Self::is_leap_year(year) {
35					29
36				} else {
37					28
38				}
39			}
40			_ => 0,
41		}
42	}
43
44	fn ymd_to_days_since_epoch(year: i32, month: u32, day: u32) -> Option<i32> {
45		if !(1..=12).contains(&month) || day < 1 || day > Self::days_in_month(year, month) {
46			return None;
47		}
48
49		let (y, m) = if month <= 2 {
50			(year - 1, month as i32 + 9)
51		} else {
52			(year, month as i32 - 3)
53		};
54
55		let era = if y >= 0 {
56			y
57		} else {
58			y - 399
59		} / 400;
60		let yoe = y - era * 400;
61		let doy = (153 * m + 2) / 5 + day as i32 - 1;
62		let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
63		let days = era * 146097 + doe - 719468;
64
65		Some(days)
66	}
67
68	fn days_since_epoch_to_ymd(days: i32) -> (i32, u32, u32) {
69		let days_since_ce = days + 719468;
70
71		let era = if days_since_ce >= 0 {
72			days_since_ce
73		} else {
74			days_since_ce - 146096
75		} / 146097;
76		let doe = days_since_ce - era * 146097;
77		let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
78		let y = yoe + era * 400;
79		let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
80		let mp = (5 * doy + 2) / 153;
81		let d = doy - (153 * mp + 2) / 5 + 1;
82		let m = if mp < 10 {
83			mp + 3
84		} else {
85			mp - 9
86		};
87		let year = if m <= 2 {
88			y + 1
89		} else {
90			y
91		};
92
93		(year, m as u32, d as u32)
94	}
95}
96
97impl Date {
98	fn overflow_err(message: impl Into<String>) -> TypeError {
99		TypeError::Temporal {
100			kind: TemporalKind::DateOverflow {
101				message: message.into(),
102			},
103			message: "date overflow".to_string(),
104			fragment: Fragment::None,
105		}
106	}
107
108	pub fn new(year: i32, month: u32, day: u32) -> Option<Self> {
109		Self::ymd_to_days_since_epoch(year, month, day).map(|days_since_epoch| Self {
110			days_since_epoch,
111		})
112	}
113
114	pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self, Box<TypeError>> {
115		Self::new(year, month, day).ok_or_else(|| {
116			Box::new(Self::overflow_err(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
117		})
118	}
119
120	pub fn year(&self) -> i32 {
121		Self::days_since_epoch_to_ymd(self.days_since_epoch).0
122	}
123
124	pub fn month(&self) -> u32 {
125		Self::days_since_epoch_to_ymd(self.days_since_epoch).1
126	}
127
128	pub fn day(&self) -> u32 {
129		Self::days_since_epoch_to_ymd(self.days_since_epoch).2
130	}
131
132	pub fn to_days_since_epoch(&self) -> i32 {
133		self.days_since_epoch
134	}
135
136	pub fn from_days_since_epoch(days: i32) -> Option<Self> {
137		if !(-365_250_000..=365_250_000).contains(&days) {
138			return None;
139		}
140		Some(Self {
141			days_since_epoch: days,
142		})
143	}
144}
145
146impl Display for Date {
147	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
148		let (year, month, day) = Self::days_since_epoch_to_ymd(self.days_since_epoch);
149		if year < 0 {
150			write!(f, "-{:04}-{:02}-{:02}", -year, month, day)
151		} else {
152			write!(f, "{:04}-{:02}-{:02}", year, month, day)
153		}
154	}
155}
156
157impl Serialize for Date {
158	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159	where
160		S: Serializer,
161	{
162		serializer.serialize_str(&self.to_string())
163	}
164}
165
166struct DateVisitor;
167
168impl<'de> Visitor<'de> for DateVisitor {
169	type Value = Date;
170
171	fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
172		formatter.write_str("a date in ISO 8601 format (YYYY-MM-DD)")
173	}
174
175	fn visit_str<E>(self, value: &str) -> Result<Date, E>
176	where
177		E: de::Error,
178	{
179		let parts: Vec<&str> = value.split('-').collect();
180
181		if parts.len() != 3 {
182			return Err(E::custom(format!("invalid date format: {}", value)));
183		}
184
185		let (year_str, month_str, day_str) = if parts[0].is_empty() && parts.len() == 4 {
186			(format!("-{}", parts[1]), parts[2], parts[3])
187		} else {
188			(parts[0].to_string(), parts[1], parts[2])
189		};
190
191		let year = year_str.parse::<i32>().map_err(|_| E::custom(format!("invalid year: {}", year_str)))?;
192		let month = month_str.parse::<u32>().map_err(|_| E::custom(format!("invalid month: {}", month_str)))?;
193		let day = day_str.parse::<u32>().map_err(|_| E::custom(format!("invalid day: {}", day_str)))?;
194
195		Date::new(year, month, day)
196			.ok_or_else(|| E::custom(format!("invalid date: {}-{:02}-{:02}", year, month, day)))
197	}
198}
199
200impl<'de> Deserialize<'de> for Date {
201	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202	where
203		D: Deserializer<'de>,
204	{
205		deserializer.deserialize_str(DateVisitor)
206	}
207}
208
209#[cfg(test)]
210pub mod tests {
211	use std::fmt::Debug;
212
213	use serde_json::{from_str, to_string};
214
215	use super::*;
216	use crate::error::{TemporalKind, TypeError};
217
218	#[test]
219	fn test_date_display_standard_dates() {
220		// Standard dates
221		let date = Date::new(2024, 3, 15).unwrap();
222		assert_eq!(format!("{}", date), "2024-03-15");
223
224		let date = Date::new(2000, 1, 1).unwrap();
225		assert_eq!(format!("{}", date), "2000-01-01");
226
227		let date = Date::new(1999, 12, 31).unwrap();
228		assert_eq!(format!("{}", date), "1999-12-31");
229	}
230
231	#[test]
232	fn test_date_display_edge_cases() {
233		// Unix epoch
234		let date = Date::new(1970, 1, 1).unwrap();
235		assert_eq!(format!("{}", date), "1970-01-01");
236
237		// Leap year
238		let date = Date::new(2024, 2, 29).unwrap();
239		assert_eq!(format!("{}", date), "2024-02-29");
240
241		// Single digit day/month
242		let date = Date::new(2024, 1, 9).unwrap();
243		assert_eq!(format!("{}", date), "2024-01-09");
244
245		let date = Date::new(2024, 9, 1).unwrap();
246		assert_eq!(format!("{}", date), "2024-09-01");
247	}
248
249	#[test]
250	fn test_date_display_boundary_dates() {
251		// Very early date
252		let date = Date::new(1, 1, 1).unwrap();
253		assert_eq!(format!("{}", date), "0001-01-01");
254
255		// Far future date
256		let date = Date::new(9999, 12, 31).unwrap();
257		assert_eq!(format!("{}", date), "9999-12-31");
258
259		// Century boundaries
260		let date = Date::new(1900, 1, 1).unwrap();
261		assert_eq!(format!("{}", date), "1900-01-01");
262
263		let date = Date::new(2000, 1, 1).unwrap();
264		assert_eq!(format!("{}", date), "2000-01-01");
265
266		let date = Date::new(2100, 1, 1).unwrap();
267		assert_eq!(format!("{}", date), "2100-01-01");
268	}
269
270	#[test]
271	fn test_date_display_negative_years() {
272		// Year 0 (1 BC)
273		let date = Date::new(0, 1, 1).unwrap();
274		assert_eq!(format!("{}", date), "0000-01-01");
275
276		// Negative years (BC)
277		let date = Date::new(-1, 1, 1).unwrap();
278		assert_eq!(format!("{}", date), "-0001-01-01");
279
280		let date = Date::new(-100, 12, 31).unwrap();
281		assert_eq!(format!("{}", date), "-0100-12-31");
282	}
283
284	#[test]
285	fn test_date_display_default() {
286		let date = Date::default();
287		assert_eq!(format!("{}", date), "1970-01-01");
288	}
289
290	#[test]
291	fn test_date_display_all_months() {
292		let months = [
293			(1, "01"),
294			(2, "02"),
295			(3, "03"),
296			(4, "04"),
297			(5, "05"),
298			(6, "06"),
299			(7, "07"),
300			(8, "08"),
301			(9, "09"),
302			(10, "10"),
303			(11, "11"),
304			(12, "12"),
305		];
306
307		for (month, expected) in months {
308			let date = Date::new(2024, month, 15).unwrap();
309			assert_eq!(format!("{}", date), format!("2024-{}-15", expected));
310		}
311	}
312
313	#[test]
314	fn test_date_display_days_in_month() {
315		// Test first and last days of various months
316		let test_cases = [
317			(2024, 1, 1, "2024-01-01"),
318			(2024, 1, 31, "2024-01-31"),
319			(2024, 2, 1, "2024-02-01"),
320			(2024, 2, 29, "2024-02-29"), // Leap year
321			(2024, 4, 1, "2024-04-01"),
322			(2024, 4, 30, "2024-04-30"),
323			(2024, 12, 1, "2024-12-01"),
324			(2024, 12, 31, "2024-12-31"),
325		];
326
327		for (year, month, day, expected) in test_cases {
328			let date = Date::new(year, month, day).unwrap();
329			assert_eq!(format!("{}", date), expected);
330		}
331	}
332
333	#[test]
334	fn test_date_roundtrip() {
335		// Test that converting to/from days preserves the date
336		let test_dates = [
337			(1900, 1, 1),
338			(1970, 1, 1),
339			(2000, 2, 29), // Leap year
340			(2024, 12, 31),
341			(2100, 6, 15),
342		];
343
344		for (year, month, day) in test_dates {
345			let date = Date::new(year, month, day).unwrap();
346			let days = date.to_days_since_epoch();
347			let recovered = Date::from_days_since_epoch(days).unwrap();
348
349			assert_eq!(date.year(), recovered.year());
350			assert_eq!(date.month(), recovered.month());
351			assert_eq!(date.day(), recovered.day());
352		}
353	}
354
355	#[test]
356	fn test_leap_year_detection() {
357		assert!(Date::is_leap_year(2000)); // Divisible by 400
358		assert!(Date::is_leap_year(2024)); // Divisible by 4, not by 100
359		assert!(!Date::is_leap_year(1900)); // Divisible by 100, not by 400
360		assert!(!Date::is_leap_year(2023)); // Not divisible by 4
361	}
362
363	#[test]
364	fn test_invalid_dates() {
365		assert!(Date::new(2024, 0, 1).is_none()); // Invalid month
366		assert!(Date::new(2024, 13, 1).is_none()); // Invalid month
367		assert!(Date::new(2024, 1, 0).is_none()); // Invalid day
368		assert!(Date::new(2024, 1, 32).is_none()); // Invalid day
369		assert!(Date::new(2023, 2, 29).is_none()); // Not a leap year
370		assert!(Date::new(2024, 4, 31).is_none()); // April has 30 days
371	}
372
373	#[test]
374	fn test_serde_roundtrip() {
375		let date = Date::new(2024, 3, 15).unwrap();
376		let json = to_string(&date).unwrap();
377		assert_eq!(json, "\"2024-03-15\"");
378
379		let recovered: Date = from_str(&json).unwrap();
380		assert_eq!(date, recovered);
381	}
382
383	fn assert_date_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
384		let err = result.expect_err("expected DateOverflow error");
385		match *err {
386			TypeError::Temporal {
387				kind: TemporalKind::DateOverflow {
388					..
389				},
390				..
391			} => {}
392			other => panic!("expected DateOverflow, got: {:?}", other),
393		}
394	}
395
396	#[test]
397	fn test_from_ymd_invalid_month() {
398		assert_date_overflow(Date::from_ymd(2024, 0, 1));
399		assert_date_overflow(Date::from_ymd(2024, 13, 1));
400	}
401
402	#[test]
403	fn test_from_ymd_invalid_day() {
404		assert_date_overflow(Date::from_ymd(2024, 1, 0));
405		assert_date_overflow(Date::from_ymd(2024, 1, 32));
406	}
407
408	#[test]
409	fn test_from_ymd_non_leap_year() {
410		assert_date_overflow(Date::from_ymd(2023, 2, 29));
411	}
412}