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