Skip to main content

reifydb_value/value/temporal/parse/
time.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::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		Error::from(TypeError::Temporal {
124			kind: TemporalKind::InvalidHour,
125			message: format!("invalid hour value '{}'", sub_frag.text()),
126			fragment: sub_frag,
127		})
128	})?;
129	offset += time_fragment_parts[0].len() + 1;
130
131	let minute = time_fragment_parts[1].trim().parse::<u32>().map_err(|_| {
132		let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[1].len());
133		Error::from(TypeError::Temporal {
134			kind: TemporalKind::InvalidMinute,
135			message: format!("invalid minute value '{}'", sub_frag.text()),
136			fragment: sub_frag,
137		})
138	})?;
139	offset += time_fragment_parts[1].len() + 1;
140
141	let seconds_with_fraction = time_fragment_parts[2].trim();
142	let (second, nanosecond) = if seconds_with_fraction.contains('.') {
143		let second_parts: Vec<&str> = seconds_with_fraction.split('.').collect();
144		if second_parts.len() != 2 {
145			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
146			return Err(TypeError::Temporal {
147				kind: TemporalKind::InvalidFractionalSeconds,
148				message: format!("invalid fractional seconds value '{}'", sub_frag.text()),
149				fragment: sub_frag,
150			}
151			.into());
152		}
153
154		let second = second_parts[0].parse::<u32>().map_err(|_| {
155			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
156			Error::from(TypeError::Temporal {
157				kind: TemporalKind::InvalidSecond,
158				message: format!("invalid second value '{}'", sub_frag.text()),
159				fragment: sub_frag,
160			})
161		})?;
162		let fraction_str = second_parts[1];
163
164		let padded_fraction = if fraction_str.len() < 9 {
165			format!("{:0<9}", fraction_str)
166		} else {
167			fraction_str[..9].to_string()
168		};
169
170		let nanosecond = padded_fraction.parse::<u32>().map_err(|_| {
171			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
172			Error::from(TypeError::Temporal {
173				kind: TemporalKind::InvalidFractionalSeconds,
174				message: format!("invalid fractional seconds value '{}'", sub_frag.text()),
175				fragment: sub_frag,
176			})
177		})?;
178		(second, nanosecond)
179	} else {
180		let second = seconds_with_fraction.parse::<u32>().map_err(|_| {
181			let sub_frag = fragment.sub_fragment(offset, time_fragment_parts[2].len());
182			Error::from(TypeError::Temporal {
183				kind: TemporalKind::InvalidSecond,
184				message: format!("invalid second value '{}'", sub_frag.text()),
185				fragment: sub_frag,
186			})
187		})?;
188		(second, 0)
189	};
190
191	Time::new(hour, minute, second, nanosecond).ok_or_else(|| {
192		Error::from(TypeError::Temporal {
193			kind: TemporalKind::InvalidTimeValues,
194			message: "invalid time values".into(),
195			fragment,
196		})
197	})
198}
199
200#[cfg(test)]
201pub mod tests {
202	use super::parse_time;
203	use crate::fragment::Fragment;
204
205	#[test]
206	fn test_basic() {
207		let fragment = Fragment::testing("14:30:00");
208		let time = parse_time(fragment).unwrap();
209		assert_eq!(time.to_string(), "14:30:00.000000000");
210	}
211
212	#[test]
213	fn test_with_timezone_z() {
214		let fragment = Fragment::testing("14:30:00Z");
215		let time = parse_time(fragment).unwrap();
216		assert_eq!(time.to_string(), "14:30:00.000000000");
217	}
218
219	#[test]
220	fn test_with_milliseconds() {
221		let fragment = Fragment::testing("14:30:00.123");
222		let time = parse_time(fragment).unwrap();
223		assert_eq!(time.to_string(), "14:30:00.123000000");
224	}
225
226	#[test]
227	fn test_with_microseconds() {
228		let fragment = Fragment::testing("14:30:00.123456");
229		let time = parse_time(fragment).unwrap();
230		assert_eq!(time.to_string(), "14:30:00.123456000");
231	}
232
233	#[test]
234	fn test_with_nanoseconds() {
235		let fragment = Fragment::testing("14:30:00.123456789");
236		let time = parse_time(fragment).unwrap();
237		assert_eq!(time.to_string(), "14:30:00.123456789");
238	}
239
240	#[test]
241	fn test_with_utc_timezone() {
242		let fragment = Fragment::testing("14:30:00Z");
243		let time = parse_time(fragment).unwrap();
244		assert_eq!(time.to_string(), "14:30:00.000000000");
245	}
246
247	#[test]
248	fn test_boundaries() {
249		let fragment = Fragment::testing("00:00:00");
250		let time = parse_time(fragment).unwrap();
251		assert_eq!(time.to_string(), "00:00:00.000000000");
252
253		let fragment = Fragment::testing("23:59:59");
254		let time = parse_time(fragment).unwrap();
255		assert_eq!(time.to_string(), "23:59:59.000000000");
256	}
257
258	#[test]
259	fn test_invalid_format() {
260		let fragment = Fragment::testing("14:30");
261		let err = parse_time(fragment).unwrap_err();
262		assert_eq!(err.0.code, "TEMPORAL_003");
263	}
264
265	#[test]
266	fn test_invalid_hour() {
267		let fragment = Fragment::testing("invalid:30:00");
268		let result = parse_time(fragment);
269		assert!(result.is_err());
270		let err = result.unwrap_err();
271		assert_eq!(err.0.code, "TEMPORAL_005");
272	}
273
274	#[test]
275	fn test_invalid_minute() {
276		let fragment = Fragment::testing("14:invalid:00");
277		let result = parse_time(fragment);
278		assert!(result.is_err());
279		let err = result.unwrap_err();
280		assert_eq!(err.0.code, "TEMPORAL_005");
281	}
282
283	#[test]
284	fn test_invalid_second() {
285		let fragment = Fragment::testing("14:30:invalid");
286		let result = parse_time(fragment);
287		assert!(result.is_err());
288		let err = result.unwrap_err();
289		assert_eq!(err.0.code, "TEMPORAL_005");
290	}
291
292	#[test]
293	fn test_invalid_time_values() {
294		let fragment = Fragment::testing("25:70:80");
295		let result = parse_time(fragment);
296		assert!(result.is_err());
297		let err = result.unwrap_err();
298		assert_eq!(err.0.code, "TEMPORAL_013");
299	}
300
301	#[test]
302	fn test_invalid_fractional_seconds() {
303		let fragment = Fragment::testing("14:30:00.123.456");
304		let result = parse_time(fragment);
305		assert!(result.is_err());
306		let err = result.unwrap_err();
307		assert_eq!(err.0.code, "TEMPORAL_011");
308	}
309
310	#[test]
311	fn test_lowercase_z_rejected() {
312		let fragment = Fragment::testing("14:30:00z");
313		let result = parse_time(fragment);
314		assert!(result.is_err());
315		let err = result.unwrap_err();
316		assert_eq!(err.0.code, "TEMPORAL_010"); // invalid_second
317	}
318}