Skip to main content

reifydb_value/value/
time.rs

1// SPDX-License-Identifier: Apache-2.0
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	value::duration::Duration,
15};
16
17#[repr(transparent)]
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
19pub struct Time {
20	nanos_since_midnight: u64,
21}
22
23impl Time {
24	const MAX_NANOS_IN_DAY: u64 = 86_399_999_999_999;
25	const NANOS_PER_SECOND: u64 = 1_000_000_000;
26	const NANOS_PER_MINSVTE: u64 = 60 * Self::NANOS_PER_SECOND;
27	const NANOS_PER_HOUR: u64 = 60 * Self::NANOS_PER_MINSVTE;
28
29	fn overflow_err(message: impl Into<String>) -> TypeError {
30		TypeError::Temporal {
31			kind: TemporalKind::TimeOverflow {
32				message: message.into(),
33			},
34			message: "time overflow".to_string(),
35			fragment: Fragment::None,
36		}
37	}
38
39	pub fn new(hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
40		if hour >= 24 || min >= 60 || sec >= 60 || nano >= Self::NANOS_PER_SECOND as u32 {
41			return None;
42		}
43
44		let nanos = hour as u64 * Self::NANOS_PER_HOUR
45			+ min as u64 * Self::NANOS_PER_MINSVTE
46			+ sec as u64 * Self::NANOS_PER_SECOND
47			+ nano as u64;
48
49		Some(Self {
50			nanos_since_midnight: nanos,
51		})
52	}
53
54	pub fn from_hms(hour: u32, min: u32, sec: u32) -> Result<Self, Box<TypeError>> {
55		Self::new(hour, min, sec, 0).ok_or_else(|| {
56			Box::new(Self::overflow_err(format!("invalid time: {:02}:{:02}:{:02}", hour, min, sec)))
57		})
58	}
59
60	pub fn from_hms_nano(hour: u32, min: u32, sec: u32, nano: u32) -> Result<Self, Box<TypeError>> {
61		Self::new(hour, min, sec, nano).ok_or_else(|| {
62			Box::new(Self::overflow_err(format!(
63				"invalid time: {:02}:{:02}:{:02}.{:09}",
64				hour, min, sec, nano
65			)))
66		})
67	}
68
69	pub fn midnight() -> Self {
70		Self {
71			nanos_since_midnight: 0,
72		}
73	}
74
75	pub fn noon() -> Self {
76		Self {
77			nanos_since_midnight: 12 * Self::NANOS_PER_HOUR,
78		}
79	}
80
81	pub fn hour(&self) -> u32 {
82		(self.nanos_since_midnight / Self::NANOS_PER_HOUR) as u32
83	}
84
85	pub fn minute(&self) -> u32 {
86		((self.nanos_since_midnight % Self::NANOS_PER_HOUR) / Self::NANOS_PER_MINSVTE) as u32
87	}
88
89	pub fn second(&self) -> u32 {
90		((self.nanos_since_midnight % Self::NANOS_PER_MINSVTE) / Self::NANOS_PER_SECOND) as u32
91	}
92
93	pub fn nanosecond(&self) -> u32 {
94		(self.nanos_since_midnight % Self::NANOS_PER_SECOND) as u32
95	}
96
97	pub fn to_nanos_since_midnight(&self) -> u64 {
98		self.nanos_since_midnight
99	}
100
101	pub fn from_nanos_since_midnight(nanos: u64) -> Option<Self> {
102		if nanos > Self::MAX_NANOS_IN_DAY {
103			return None;
104		}
105		Some(Self {
106			nanos_since_midnight: nanos,
107		})
108	}
109
110	pub fn saturating_add(self, rhs: Duration) -> Time {
111		let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
112			i64::MIN
113		} else {
114			i64::MAX
115		});
116		let nanos =
117			(self.nanos_since_midnight as i128 + total as i128).clamp(0, Self::MAX_NANOS_IN_DAY as i128);
118		Self {
119			nanos_since_midnight: nanos as u64,
120		}
121	}
122
123	pub fn saturating_sub(self, rhs: Duration) -> Time {
124		let total = rhs.as_nanos().unwrap_or(if rhs.is_negative() {
125			i64::MIN
126		} else {
127			i64::MAX
128		});
129		let nanos =
130			(self.nanos_since_midnight as i128 - total as i128).clamp(0, Self::MAX_NANOS_IN_DAY as i128);
131		Self {
132			nanos_since_midnight: nanos as u64,
133		}
134	}
135}
136
137impl Display for Time {
138	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
139		let hours = self.hour();
140		let minutes = self.minute();
141		let seconds = self.second();
142		let nanos = self.nanosecond();
143
144		write!(f, "{:02}:{:02}:{:02}.{:09}", hours, minutes, seconds, nanos)
145	}
146}
147
148impl Serialize for Time {
149	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150	where
151		S: Serializer,
152	{
153		serializer.serialize_u64(self.nanos_since_midnight)
154	}
155}
156
157struct TimeVisitor;
158
159impl<'de> Visitor<'de> for TimeVisitor {
160	type Value = Time;
161
162	fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
163		formatter.write_str("a time as nanoseconds since midnight (u64)")
164	}
165
166	fn visit_u64<E>(self, value: u64) -> Result<Time, E>
167	where
168		E: de::Error,
169	{
170		Time::from_nanos_since_midnight(value)
171			.ok_or_else(|| E::custom(format!("time nanoseconds out of range: {}", value)))
172	}
173}
174
175impl<'de> Deserialize<'de> for Time {
176	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177	where
178		D: Deserializer<'de>,
179	{
180		deserializer.deserialize_u64(TimeVisitor)
181	}
182}
183
184#[cfg(test)]
185pub mod tests {
186	use std::fmt::Debug;
187
188	use postcard::{from_bytes, to_allocvec};
189	use serde_json::{from_str, to_string};
190
191	use super::*;
192	use crate::error::{TemporalKind, TypeError};
193
194	#[test]
195	fn test_time_display_standard_format() {
196		let time = Time::new(14, 30, 45, 123456789).unwrap();
197		assert_eq!(format!("{}", time), "14:30:45.123456789");
198
199		let time = Time::new(0, 0, 0, 0).unwrap();
200		assert_eq!(format!("{}", time), "00:00:00.000000000");
201
202		let time = Time::new(23, 59, 59, 999999999).unwrap();
203		assert_eq!(format!("{}", time), "23:59:59.999999999");
204	}
205
206	#[test]
207	fn test_time_display_millisecond_precision() {
208		let time = Time::new(14, 30, 45, 123000000).unwrap();
209		assert_eq!(format!("{}", time), "14:30:45.123000000");
210
211		let time = Time::new(14, 30, 45, 001000000).unwrap();
212		assert_eq!(format!("{}", time), "14:30:45.001000000");
213
214		let time = Time::new(14, 30, 45, 999000000).unwrap();
215		assert_eq!(format!("{}", time), "14:30:45.999000000");
216	}
217
218	#[test]
219	fn test_time_display_microsecond_precision() {
220		let time = Time::new(14, 30, 45, 123456000).unwrap();
221		assert_eq!(format!("{}", time), "14:30:45.123456000");
222
223		let time = Time::new(14, 30, 45, 000001000).unwrap();
224		assert_eq!(format!("{}", time), "14:30:45.000001000");
225
226		let time = Time::new(14, 30, 45, 999999000).unwrap();
227		assert_eq!(format!("{}", time), "14:30:45.999999000");
228	}
229
230	#[test]
231	fn test_time_display_nanosecond_precision() {
232		let time = Time::new(14, 30, 45, 123456789).unwrap();
233		assert_eq!(format!("{}", time), "14:30:45.123456789");
234
235		let time = Time::new(14, 30, 45, 000000001).unwrap();
236		assert_eq!(format!("{}", time), "14:30:45.000000001");
237
238		let time = Time::new(14, 30, 45, 999999999).unwrap();
239		assert_eq!(format!("{}", time), "14:30:45.999999999");
240	}
241
242	#[test]
243	fn test_time_display_zero_fractional_seconds() {
244		let time = Time::new(14, 30, 45, 0).unwrap();
245		assert_eq!(format!("{}", time), "14:30:45.000000000");
246
247		let time = Time::new(0, 0, 0, 0).unwrap();
248		assert_eq!(format!("{}", time), "00:00:00.000000000");
249	}
250
251	#[test]
252	fn test_time_display_edge_times() {
253		let time = Time::new(0, 0, 0, 0).unwrap();
254		assert_eq!(format!("{}", time), "00:00:00.000000000");
255
256		let time = Time::new(23, 59, 59, 999999999).unwrap();
257		assert_eq!(format!("{}", time), "23:59:59.999999999");
258
259		let time = Time::new(12, 0, 0, 0).unwrap();
260		assert_eq!(format!("{}", time), "12:00:00.000000000");
261
262		let time = Time::new(23, 59, 58, 999999999).unwrap();
263		assert_eq!(format!("{}", time), "23:59:58.999999999");
264
265		let time = Time::new(0, 0, 1, 0).unwrap();
266		assert_eq!(format!("{}", time), "00:00:01.000000000");
267	}
268
269	#[test]
270	fn test_time_display_special_times() {
271		let midnight = Time::midnight();
272		assert_eq!(format!("{}", midnight), "00:00:00.000000000");
273
274		let noon = Time::noon();
275		assert_eq!(format!("{}", noon), "12:00:00.000000000");
276
277		let default = Time::default();
278		assert_eq!(format!("{}", default), "00:00:00.000000000");
279	}
280
281	#[test]
282	fn test_time_display_all_hours() {
283		for hour in 0..24 {
284			let time = Time::new(hour, 30, 45, 123456789).unwrap();
285			let expected = format!("{:02}:30:45.123456789", hour);
286			assert_eq!(format!("{}", time), expected);
287		}
288	}
289
290	#[test]
291	fn test_time_display_all_minutes() {
292		for minute in 0..60 {
293			let time = Time::new(14, minute, 45, 123456789).unwrap();
294			let expected = format!("14:{:02}:45.123456789", minute);
295			assert_eq!(format!("{}", time), expected);
296		}
297	}
298
299	#[test]
300	fn test_time_display_all_seconds() {
301		for second in 0..60 {
302			let time = Time::new(14, 30, second, 123456789).unwrap();
303			let expected = format!("14:30:{:02}.123456789", second);
304			assert_eq!(format!("{}", time), expected);
305		}
306	}
307
308	#[test]
309	fn test_time_display_from_hms() {
310		let time = Time::from_hms(14, 30, 45).unwrap();
311		assert_eq!(format!("{}", time), "14:30:45.000000000");
312
313		let time = Time::from_hms(0, 0, 0).unwrap();
314		assert_eq!(format!("{}", time), "00:00:00.000000000");
315
316		let time = Time::from_hms(23, 59, 59).unwrap();
317		assert_eq!(format!("{}", time), "23:59:59.000000000");
318	}
319
320	#[test]
321	fn test_time_display_from_hms_nano() {
322		let time = Time::from_hms_nano(14, 30, 45, 123456789).unwrap();
323		assert_eq!(format!("{}", time), "14:30:45.123456789");
324
325		let time = Time::from_hms_nano(0, 0, 0, 0).unwrap();
326		assert_eq!(format!("{}", time), "00:00:00.000000000");
327
328		let time = Time::from_hms_nano(23, 59, 59, 999999999).unwrap();
329		assert_eq!(format!("{}", time), "23:59:59.999999999");
330	}
331
332	#[test]
333	fn test_time_display_from_nanos_since_midnight() {
334		let time = Time::from_nanos_since_midnight(0).unwrap();
335		assert_eq!(format!("{}", time), "00:00:00.000000000");
336
337		let time = Time::from_nanos_since_midnight(1_000_000_000).unwrap();
338		assert_eq!(format!("{}", time), "00:00:01.000000000");
339
340		let time = Time::from_nanos_since_midnight(60_000_000_000).unwrap();
341		assert_eq!(format!("{}", time), "00:01:00.000000000");
342
343		let time = Time::from_nanos_since_midnight(3_600_000_000_000).unwrap();
344		assert_eq!(format!("{}", time), "01:00:00.000000000");
345
346		let nanos = 14 * 3600 * 1_000_000_000 + 30 * 60 * 1_000_000_000 + 45 * 1_000_000_000 + 123456789;
347		let time = Time::from_nanos_since_midnight(nanos).unwrap();
348		assert_eq!(format!("{}", time), "14:30:45.123456789");
349	}
350
351	#[test]
352	fn test_time_display_boundary_values() {
353		let nanos = 24 * 3600 * 1_000_000_000 - 1;
354		let time = Time::from_nanos_since_midnight(nanos).unwrap();
355		assert_eq!(format!("{}", time), "23:59:59.999999999");
356
357		let time = Time::from_nanos_since_midnight(1).unwrap();
358		assert_eq!(format!("{}", time), "00:00:00.000000001");
359	}
360
361	#[test]
362	fn test_time_display_precision_patterns() {
363		let time = Time::new(14, 30, 45, 100000000).unwrap(); // 0.1 seconds
364		assert_eq!(format!("{}", time), "14:30:45.100000000");
365
366		let time = Time::new(14, 30, 45, 010000000).unwrap(); // 0.01 seconds
367		assert_eq!(format!("{}", time), "14:30:45.010000000");
368
369		let time = Time::new(14, 30, 45, 001000000).unwrap(); // 0.001 seconds
370		assert_eq!(format!("{}", time), "14:30:45.001000000");
371
372		let time = Time::new(14, 30, 45, 000100000).unwrap(); // 0.0001 seconds
373		assert_eq!(format!("{}", time), "14:30:45.000100000");
374
375		let time = Time::new(14, 30, 45, 000010000).unwrap(); // 0.00001 seconds
376		assert_eq!(format!("{}", time), "14:30:45.000010000");
377
378		let time = Time::new(14, 30, 45, 000001000).unwrap(); // 0.000001 seconds
379		assert_eq!(format!("{}", time), "14:30:45.000001000");
380
381		let time = Time::new(14, 30, 45, 000000100).unwrap(); // 0.0000001 seconds
382		assert_eq!(format!("{}", time), "14:30:45.000000100");
383
384		let time = Time::new(14, 30, 45, 000000010).unwrap(); // 0.00000001 seconds
385		assert_eq!(format!("{}", time), "14:30:45.000000010");
386
387		let time = Time::new(14, 30, 45, 000000001).unwrap(); // 0.000000001 seconds
388		assert_eq!(format!("{}", time), "14:30:45.000000001");
389	}
390
391	#[test]
392	fn test_invalid_times() {
393		assert!(Time::new(24, 0, 0, 0).is_none()); // Invalid hour
394		assert!(Time::new(0, 60, 0, 0).is_none()); // Invalid minute
395		assert!(Time::new(0, 0, 60, 0).is_none()); // Invalid second
396		assert!(Time::new(0, 0, 0, 1_000_000_000).is_none()); // Invalid nanosecond
397	}
398
399	#[test]
400	fn test_time_roundtrip() {
401		let test_times = [(0, 0, 0, 0), (12, 30, 45, 123456789), (23, 59, 59, 999999999)];
402
403		for (h, m, s, n) in test_times {
404			let time = Time::new(h, m, s, n).unwrap();
405			let nanos = time.to_nanos_since_midnight();
406			let recovered = Time::from_nanos_since_midnight(nanos).unwrap();
407
408			assert_eq!(time.hour(), recovered.hour());
409			assert_eq!(time.minute(), recovered.minute());
410			assert_eq!(time.second(), recovered.second());
411			assert_eq!(time.nanosecond(), recovered.nanosecond());
412		}
413	}
414
415	#[test]
416	fn test_serde_roundtrip() {
417		let time = Time::new(14, 30, 45, 123456789).unwrap();
418		let json = to_string(&time).unwrap();
419		// Wire format is the raw nanos-since-midnight integer, not an ISO-8601 string.
420		assert_eq!(json, time.to_nanos_since_midnight().to_string());
421
422		let recovered: Time = from_str(&json).unwrap();
423		assert_eq!(time, recovered);
424	}
425
426	#[test]
427	fn test_serde_postcard_roundtrip_preserves_all_fields() {
428		// Postcard is the CDC wire format; sub-second nanos must survive the integer encoding.
429		for (h, m, s, n) in [(0u32, 0u32, 0u32, 0u32), (14, 30, 45, 123456789), (23, 59, 59, 999999999)] {
430			let time = Time::new(h, m, s, n).unwrap();
431			let bytes = to_allocvec(&time).unwrap();
432			let recovered: Time = from_bytes(&bytes).unwrap();
433			assert_eq!(time, recovered);
434			assert_eq!(recovered.hour(), h);
435			assert_eq!(recovered.minute(), m);
436			assert_eq!(recovered.second(), s);
437			assert_eq!(recovered.nanosecond(), n);
438		}
439	}
440
441	#[test]
442	fn test_deserialize_rejects_out_of_range_nanos() {
443		// Nanos beyond the last instant of the day must not decode to a Time.
444		let json = (Time::MAX_NANOS_IN_DAY + 1).to_string();
445		assert!(from_str::<Time>(&json).is_err());
446	}
447
448	fn assert_time_overflow<T: Debug>(result: Result<T, Box<TypeError>>) {
449		let err = result.expect_err("expected TimeOverflow error");
450		match *err {
451			TypeError::Temporal {
452				kind: TemporalKind::TimeOverflow {
453					..
454				},
455				..
456			} => {}
457			other => panic!("expected TimeOverflow, got: {:?}", other),
458		}
459	}
460
461	#[test]
462	fn test_from_hms_invalid_hour() {
463		assert_time_overflow(Time::from_hms(24, 0, 0));
464	}
465
466	#[test]
467	fn test_from_hms_invalid_minute() {
468		assert_time_overflow(Time::from_hms(0, 60, 0));
469	}
470
471	#[test]
472	fn test_from_hms_invalid_second() {
473		assert_time_overflow(Time::from_hms(0, 0, 60));
474	}
475
476	#[test]
477	fn test_from_hms_nano_invalid_nano() {
478		assert_time_overflow(Time::from_hms_nano(0, 0, 0, 1_000_000_000));
479	}
480
481	#[test]
482	fn saturating_sub_before_midnight_clamps_to_midnight() {
483		// Subtracting past 00:00 clamps to midnight rather than wrapping or panicking.
484		let midnight = Time::midnight();
485		assert_eq!(midnight.saturating_sub(Duration::from_seconds(1).unwrap()), midnight);
486	}
487
488	#[test]
489	fn saturating_add_past_end_of_day_clamps_to_max() {
490		// Adding past 23:59:59.999999999 clamps to the last representable instant of the day.
491		let late = Time::from_hms(23, 59, 59).unwrap();
492		let max = Time::from_nanos_since_midnight(86_399_999_999_999).unwrap();
493		assert_eq!(late.saturating_add(Duration::from_seconds(3600).unwrap()), max);
494	}
495
496	#[test]
497	fn saturating_add_sub_within_day() {
498		let t = Time::from_hms(10, 30, 0).unwrap();
499		assert_eq!(t.saturating_add(Duration::from_seconds(60).unwrap()), Time::from_hms(10, 31, 0).unwrap());
500		assert_eq!(t.saturating_sub(Duration::from_seconds(60).unwrap()), Time::from_hms(10, 29, 0).unwrap());
501	}
502}