Skip to main content

reifydb_value/value/temporal/parse/
time.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 ReifyDB
3
4use crate::{
5	error::{Error, TemporalKind, TypeError},
6	fragment::Fragment,
7	value::Time,
8};
9
10pub fn parse_time(fragment: Fragment) -> Result<Time, Error> {
11	let fragment_value = fragment.text();
12	let mut time_str = fragment_value;
13
14	if time_str.ends_with('z') {
15		let parts: Vec<&str> = time_str.split(':').collect();
16		if parts.len() == 3 {
17			let hours_len = parts[0].len();
18			let minutes_len = parts[1].len();
19			let offset = hours_len + 1 + minutes_len + 1;
20			let second_len = parts[2].len();
21			let sub_frag = fragment.sub_fragment(offset, second_len);
22			return Err(TypeError::Temporal {
23				kind: TemporalKind::InvalidSecond,
24				message: format!("invalid second value '{}'", sub_frag.text()),
25				fragment: sub_frag,
26			}
27			.into());
28		}
29	}
30
31	if time_str.ends_with('Z') {
32		time_str = &time_str[..time_str.len() - 1];
33	}
34
35	let time_fragment_parts: Vec<&str> = time_str.split(':').collect();
36
37	if time_fragment_parts.len() != 3 {
38		return Err(TypeError::Temporal {
39			kind: TemporalKind::InvalidTimeFormat,
40			message: "invalid time format".into(),
41			fragment,
42		}
43		.into());
44	}
45
46	let mut offset = 0;
47	if time_fragment_parts[0].trim().is_empty() {
48		let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[0].len());
49		return Err(TypeError::Temporal {
50			kind: TemporalKind::EmptyTimeComponent,
51			message: "empty time component".into(),
52			fragment: sub_frag,
53		}
54		.into());
55	}
56	offset += time_fragment_parts[0].len() + 1;
57
58	if time_fragment_parts[1].trim().is_empty() {
59		let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[1].len());
60		return Err(TypeError::Temporal {
61			kind: TemporalKind::EmptyTimeComponent,
62			message: "empty time component".into(),
63			fragment: sub_frag,
64		}
65		.into());
66	}
67	offset += time_fragment_parts[1].len() + 1;
68
69	if time_fragment_parts[2].trim().is_empty() {
70		let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
71		return Err(TypeError::Temporal {
72			kind: TemporalKind::EmptyTimeComponent,
73			message: "empty time component".into(),
74			fragment: sub_frag,
75		}
76		.into());
77	}
78
79	offset = 0;
80
81	if time_fragment_parts[0].len() != 2 {
82		let frag = fragment.sub_fragment(offset, time_fragment_parts[0].len());
83		return Err(TypeError::Temporal {
84			kind: TemporalKind::InvalidTimeComponentFormat {
85				component: "hour".to_string(),
86			},
87			message: format!("invalid {} format '{}'", "hour", frag.text()),
88			fragment: frag,
89		}
90		.into());
91	}
92	offset += time_fragment_parts[0].len() + 1;
93
94	if time_fragment_parts[1].len() != 2 {
95		let frag = fragment.sub_fragment(offset, time_fragment_parts[1].len());
96		return Err(TypeError::Temporal {
97			kind: TemporalKind::InvalidTimeComponentFormat {
98				component: "minute".to_string(),
99			},
100			message: format!("invalid {} format '{}'", "minute", frag.text()),
101			fragment: frag,
102		}
103		.into());
104	}
105	offset += time_fragment_parts[1].len() + 1;
106
107	let second_base = time_fragment_parts[2].split('.').next().unwrap();
108	if second_base.len() != 2 {
109		let frag = fragment.sub_fragment(offset, second_base.len());
110		return Err(TypeError::Temporal {
111			kind: TemporalKind::InvalidTimeComponentFormat {
112				component: "second".to_string(),
113			},
114			message: format!("invalid {} format '{}'", "second", frag.text()),
115			fragment: frag,
116		}
117		.into());
118	}
119
120	offset = 0;
121	let hour = time_fragment_parts[0].trim().parse::<u32>().map_err(|_| {
122		let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[0].len());
123		let err: Error = TypeError::Temporal {
124			kind: TemporalKind::InvalidHour,
125			message: format!("invalid hour value '{}'", sub_frag.text()),
126			fragment: sub_frag,
127		}
128		.into();
129		err
130	})?;
131	offset += time_fragment_parts[0].len() + 1;
132
133	let minute = time_fragment_parts[1].trim().parse::<u32>().map_err(|_| {
134		let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[1].len());
135		let err: Error = TypeError::Temporal {
136			kind: TemporalKind::InvalidMinute,
137			message: format!("invalid minute value '{}'", sub_frag.text()),
138			fragment: sub_frag,
139		}
140		.into();
141		err
142	})?;
143	offset += time_fragment_parts[1].len() + 1;
144
145	let seconds_with_fraction = time_fragment_parts[2].trim();
146	let (second, nanosecond) = if seconds_with_fraction.contains('.') {
147		let second_parts: Vec<&str> = seconds_with_fraction.split('.').collect();
148		if second_parts.len() != 2 {
149			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
150			return Err(TypeError::Temporal {
151				kind: TemporalKind::InvalidFractionalSeconds,
152				message: format!("invalid fractional seconds value '{}'", sub_frag.text()),
153				fragment: sub_frag,
154			}
155			.into());
156		}
157
158		let second = second_parts[0].parse::<u32>().map_err(|_| {
159			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
160			let err: Error = TypeError::Temporal {
161				kind: TemporalKind::InvalidSecond,
162				message: format!("invalid second value '{}'", sub_frag.text()),
163				fragment: sub_frag,
164			}
165			.into();
166			err
167		})?;
168		let fraction_str = second_parts[1];
169
170		let padded_fraction = if fraction_str.len() < 9 {
171			format!("{:0<9}", fraction_str)
172		} else {
173			fraction_str[..9].to_string()
174		};
175
176		let nanosecond = padded_fraction.parse::<u32>().map_err(|_| {
177			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
178			let err: Error = TypeError::Temporal {
179				kind: TemporalKind::InvalidFractionalSeconds,
180				message: format!("invalid fractional seconds value '{}'", sub_frag.text()),
181				fragment: sub_frag,
182			}
183			.into();
184			err
185		})?;
186		(second, nanosecond)
187	} else {
188		let second = seconds_with_fraction.parse::<u32>().map_err(|_| {
189			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
190			let err: Error = TypeError::Temporal {
191				kind: TemporalKind::InvalidSecond,
192				message: format!("invalid second value '{}'", sub_frag.text()),
193				fragment: sub_frag,
194			}
195			.into();
196			err
197		})?;
198		(second, 0)
199	};
200
201	Time::new(hour, minute, second, nanosecond).ok_or_else(|| {
202		let err: Error = TypeError::Temporal {
203			kind: TemporalKind::InvalidTimeValues,
204			message: "invalid time values".into(),
205			fragment,
206		}
207		.into();
208		err
209	})
210}
211
212#[cfg(test)]
213pub mod tests {
214	use super::parse_time;
215	use crate::fragment::Fragment;
216
217	#[test]
218	fn test_basic() {
219		let fragment = Fragment::testing("14:30:00");
220		let time = parse_time(fragment).unwrap();
221		assert_eq!(time.to_string(), "14:30:00.000000000");
222	}
223
224	#[test]
225	fn test_with_timezone_z() {
226		let fragment = Fragment::testing("14:30:00Z");
227		let time = parse_time(fragment).unwrap();
228		assert_eq!(time.to_string(), "14:30:00.000000000");
229	}
230
231	#[test]
232	fn test_with_milliseconds() {
233		let fragment = Fragment::testing("14:30:00.123");
234		let time = parse_time(fragment).unwrap();
235		assert_eq!(time.to_string(), "14:30:00.123000000");
236	}
237
238	#[test]
239	fn test_with_microseconds() {
240		let fragment = Fragment::testing("14:30:00.123456");
241		let time = parse_time(fragment).unwrap();
242		assert_eq!(time.to_string(), "14:30:00.123456000");
243	}
244
245	#[test]
246	fn test_with_nanoseconds() {
247		let fragment = Fragment::testing("14:30:00.123456789");
248		let time = parse_time(fragment).unwrap();
249		assert_eq!(time.to_string(), "14:30:00.123456789");
250	}
251
252	#[test]
253	fn test_with_utc_timezone() {
254		let fragment = Fragment::testing("14:30:00Z");
255		let time = parse_time(fragment).unwrap();
256		assert_eq!(time.to_string(), "14:30:00.000000000");
257	}
258
259	#[test]
260	fn test_boundaries() {
261		let fragment = Fragment::testing("00:00:00");
262		let time = parse_time(fragment).unwrap();
263		assert_eq!(time.to_string(), "00:00:00.000000000");
264
265		let fragment = Fragment::testing("23:59:59");
266		let time = parse_time(fragment).unwrap();
267		assert_eq!(time.to_string(), "23:59:59.000000000");
268	}
269
270	#[test]
271	fn test_invalid_format() {
272		let fragment = Fragment::testing("14:30");
273		let err = parse_time(fragment).unwrap_err();
274		assert_eq!(err.0.code, "TEMPORAL_003");
275	}
276
277	#[test]
278	fn test_invalid_hour() {
279		let fragment = Fragment::testing("invalid:30:00");
280		let result = parse_time(fragment);
281		assert!(result.is_err());
282		let err = result.unwrap_err();
283		assert_eq!(err.0.code, "TEMPORAL_005");
284	}
285
286	#[test]
287	fn test_invalid_minute() {
288		let fragment = Fragment::testing("14:invalid:00");
289		let result = parse_time(fragment);
290		assert!(result.is_err());
291		let err = result.unwrap_err();
292		assert_eq!(err.0.code, "TEMPORAL_005");
293	}
294
295	#[test]
296	fn test_invalid_second() {
297		let fragment = Fragment::testing("14:30:invalid");
298		let result = parse_time(fragment);
299		assert!(result.is_err());
300		let err = result.unwrap_err();
301		assert_eq!(err.0.code, "TEMPORAL_005");
302	}
303
304	#[test]
305	fn test_invalid_time_values() {
306		let fragment = Fragment::testing("25:70:80");
307		let result = parse_time(fragment);
308		assert!(result.is_err());
309		let err = result.unwrap_err();
310		assert_eq!(err.0.code, "TEMPORAL_013");
311	}
312
313	#[test]
314	fn test_invalid_fractional_seconds() {
315		let fragment = Fragment::testing("14:30:00.123.456");
316		let result = parse_time(fragment);
317		assert!(result.is_err());
318		let err = result.unwrap_err();
319		assert_eq!(err.0.code, "TEMPORAL_011");
320	}
321
322	#[test]
323	fn test_lowercase_z_rejected() {
324		let fragment = Fragment::testing("14:30:00z");
325		let result = parse_time(fragment);
326		assert!(result.is_err());
327		let err = result.unwrap_err();
328		assert_eq!(err.0.code, "TEMPORAL_010"); // invalid_second
329	}
330}