Skip to main content

reifydb_type/value/
date.rs

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