Skip to main content

reifydb_value/value/
date.rs

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