Skip to main content

reifydb_value/value/temporal/parse/
date.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use crate::{
5	error::{Error, TemporalKind, TypeError},
6	fragment::Fragment,
7	value::Date,
8};
9
10pub fn parse_date(fragment: Fragment) -> Result<Date, Error> {
11	let value = fragment.text();
12	let parts: Vec<&str> = value.split('-').collect();
13
14	if parts.len() != 3 {
15		return Err(TypeError::Temporal {
16			kind: TemporalKind::InvalidDateFormat,
17			message: "invalid date format".into(),
18			fragment,
19		}
20		.into());
21	}
22
23	let year_str = parts[0].trim();
24	let month_str = parts[1].trim();
25	let day_str = parts[2].trim();
26
27	let mut offset = 0;
28	if year_str.is_empty() {
29		let year_frag = fragment.sub_fragment(offset, parts[0].len());
30		return Err(TypeError::Temporal {
31			kind: TemporalKind::EmptyDateComponent,
32			message: "empty date component".into(),
33			fragment: year_frag,
34		}
35		.into());
36	}
37	offset += parts[0].len() + 1;
38
39	if month_str.is_empty() {
40		let month_frag = fragment.sub_fragment(offset, parts[1].len());
41		return Err(TypeError::Temporal {
42			kind: TemporalKind::EmptyDateComponent,
43			message: "empty date component".into(),
44			fragment: month_frag,
45		}
46		.into());
47	}
48	offset += parts[1].len() + 1;
49
50	if day_str.is_empty() {
51		let day_frag = fragment.sub_fragment(offset, parts[2].len());
52		return Err(TypeError::Temporal {
53			kind: TemporalKind::EmptyDateComponent,
54			message: "empty date component".into(),
55			fragment: day_frag,
56		}
57		.into());
58	}
59
60	offset = 0;
61
62	if year_str.len() != 4 {
63		let year_frag = fragment.sub_fragment(offset, parts[0].len());
64		return Err(TypeError::Temporal {
65			kind: TemporalKind::InvalidYear,
66			message: format!("invalid year value '{}'", year_frag.text()),
67			fragment: year_frag,
68		}
69		.into());
70	}
71
72	let year = year_str.parse::<i32>().map_err(|_| {
73		let year_frag = fragment.sub_fragment(offset, parts[0].len());
74		Error::from(TypeError::Temporal {
75			kind: TemporalKind::InvalidYear,
76			message: format!("invalid year value '{}'", year_frag.text()),
77			fragment: year_frag,
78		})
79	})?;
80	offset += parts[0].len() + 1;
81
82	if month_str.len() != 2 {
83		let month_frag = fragment.sub_fragment(offset, parts[1].len());
84		return Err(TypeError::Temporal {
85			kind: TemporalKind::InvalidMonth,
86			message: format!("invalid month value '{}'", month_frag.text()),
87			fragment: month_frag,
88		}
89		.into());
90	}
91
92	let month = month_str.parse::<u32>().map_err(|_| {
93		let month_frag = fragment.sub_fragment(offset, parts[1].len());
94		Error::from(TypeError::Temporal {
95			kind: TemporalKind::InvalidMonth,
96			message: format!("invalid month value '{}'", month_frag.text()),
97			fragment: month_frag,
98		})
99	})?;
100	offset += parts[1].len() + 1;
101
102	if day_str.len() != 2 {
103		let day_frag = fragment.sub_fragment(offset, parts[2].len());
104		return Err(TypeError::Temporal {
105			kind: TemporalKind::InvalidDay,
106			message: format!("invalid day value '{}'", day_frag.text()),
107			fragment: day_frag,
108		}
109		.into());
110	}
111
112	let day = day_str.parse::<u32>().map_err(|_| {
113		let day_frag = fragment.sub_fragment(offset, parts[2].len());
114		Error::from(TypeError::Temporal {
115			kind: TemporalKind::InvalidDay,
116			message: format!("invalid day value '{}'", day_frag.text()),
117			fragment: day_frag,
118		})
119	})?;
120
121	Date::new(year, month, day).ok_or_else(|| {
122		Error::from(TypeError::Temporal {
123			kind: TemporalKind::InvalidDateValues,
124			message: "invalid date values".into(),
125			fragment,
126		})
127	})
128}
129
130#[cfg(test)]
131pub mod tests {
132	use super::parse_date;
133	use crate::fragment::Fragment;
134
135	#[test]
136	fn test_basic() {
137		let fragment = Fragment::testing("2024-03-15");
138		let date = parse_date(fragment).unwrap();
139		assert_eq!(date.to_string(), "2024-03-15");
140	}
141
142	#[test]
143	fn test_leap_year() {
144		let fragment = Fragment::testing("2024-02-29");
145		let date = parse_date(fragment).unwrap();
146		assert_eq!(date.to_string(), "2024-02-29");
147	}
148
149	#[test]
150	fn test_boundaries() {
151		let fragment = Fragment::testing("2000-01-01");
152		let date = parse_date(fragment).unwrap();
153		assert_eq!(date.to_string(), "2000-01-01");
154
155		let fragment = Fragment::testing("2024-12-31");
156		let date = parse_date(fragment).unwrap();
157		assert_eq!(date.to_string(), "2024-12-31");
158	}
159
160	#[test]
161	fn test_invalid_format() {
162		let fragment = Fragment::testing("2024-03");
163		let err = parse_date(fragment).unwrap_err();
164		assert_eq!(err.0.code, "TEMPORAL_001");
165	}
166
167	#[test]
168	fn test_invalid_year() {
169		let fragment = Fragment::testing("abcd-03-15");
170		let err = parse_date(fragment).unwrap_err();
171		assert_eq!(err.0.code, "TEMPORAL_005");
172	}
173
174	#[test]
175	fn test_invalid_month() {
176		let fragment = Fragment::testing("2024-invalid-15");
177		let err = parse_date(fragment).unwrap_err();
178		assert_eq!(err.0.code, "TEMPORAL_006");
179	}
180
181	#[test]
182	fn test_invalid_day() {
183		let fragment = Fragment::testing("2024-03-invalid");
184		let err = parse_date(fragment).unwrap_err();
185		assert_eq!(err.0.code, "TEMPORAL_007");
186	}
187
188	#[test]
189	fn test_invalid_date_values() {
190		let fragment = Fragment::testing("2024-13-32");
191		let err = parse_date(fragment).unwrap_err();
192		assert_eq!(err.0.code, "TEMPORAL_012");
193	}
194
195	#[test]
196	fn test_four_digit_year() {
197		// The year field is fixed at four digits, so anything shorter or longer is rejected
198		// rather than padded or truncated; leading zeros are how a small year is written.
199		let fragment = Fragment::testing("24-03-15");
200		let err = parse_date(fragment).unwrap_err();
201		assert_eq!(err.0.code, "TEMPORAL_005");
202
203		let fragment = Fragment::testing("024-03-15");
204		let err = parse_date(fragment).unwrap_err();
205		assert_eq!(err.0.code, "TEMPORAL_005");
206
207		let fragment = Fragment::testing("20240-03-15");
208		let err = parse_date(fragment).unwrap_err();
209		assert_eq!(err.0.code, "TEMPORAL_005");
210
211		let fragment = Fragment::testing("0024-03-15");
212		let date = parse_date(fragment).unwrap();
213		assert_eq!(date.to_string(), "0024-03-15");
214	}
215
216	#[test]
217	fn test_two_digit_month() {
218		// The month field is fixed at two digits, and the width check runs on characters, so a
219		// space or letter is a width error rather than a value error.
220		let fragment = Fragment::testing("2024-3-15");
221		let err = parse_date(fragment).unwrap_err();
222		assert_eq!(err.0.code, "TEMPORAL_006");
223
224		let fragment = Fragment::testing("2024-003-15");
225		let err = parse_date(fragment).unwrap_err();
226		assert_eq!(err.0.code, "TEMPORAL_006");
227
228		let fragment = Fragment::testing("2024-03-15");
229		let date = parse_date(fragment).unwrap();
230		assert_eq!(date.to_string(), "2024-03-15");
231
232		let fragment = Fragment::testing("2024-0a-15");
233		let err = parse_date(fragment).unwrap_err();
234		assert_eq!(err.0.code, "TEMPORAL_006");
235
236		let fragment = Fragment::testing("2024- 3-15");
237		let err = parse_date(fragment).unwrap_err();
238		assert_eq!(err.0.code, "TEMPORAL_006");
239	}
240
241	#[test]
242	fn test_two_digit_day() {
243		// The day field is fixed at two digits on the same rules as the month.
244		let fragment = Fragment::testing("2024-03-5");
245		let err = parse_date(fragment).unwrap_err();
246		assert_eq!(err.0.code, "TEMPORAL_007");
247
248		let fragment = Fragment::testing("2024-03-015");
249		let err = parse_date(fragment).unwrap_err();
250		assert_eq!(err.0.code, "TEMPORAL_007");
251
252		let fragment = Fragment::testing("2024-03-05");
253		let date = parse_date(fragment).unwrap();
254		assert_eq!(date.to_string(), "2024-03-05");
255
256		let fragment = Fragment::testing("2024-03-1a");
257		let err = parse_date(fragment).unwrap_err();
258		assert_eq!(err.0.code, "TEMPORAL_007");
259
260		let fragment = Fragment::testing("2024-03- 5");
261		let err = parse_date(fragment).unwrap_err();
262		assert_eq!(err.0.code, "TEMPORAL_007");
263	}
264}