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