Skip to main content

reifydb_type/value/
date.rs

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