reifydb_type/value/
date.rs

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