use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
const NANOS_PER_DAY: u64 = 86_400_000_000_000;
const NANOS_PER_HOUR: u64 = 3_600_000_000_000;
const NANOS_PER_MINUTE: u64 = 60_000_000_000;
const NANOS_PER_SECOND: u64 = 1_000_000_000;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Time {
nanos: u64,
offset: Option<i32>,
}
impl Time {
#[must_use]
pub fn from_hms(hour: u32, min: u32, sec: u32) -> Option<Self> {
Self::from_hms_nano(hour, min, sec, 0)
}
#[must_use]
pub fn from_hms_nano(hour: u32, min: u32, sec: u32, nano: u32) -> Option<Self> {
if hour >= 24 || min >= 60 || sec >= 60 || nano >= 1_000_000_000 {
return None;
}
let nanos = hour as u64 * NANOS_PER_HOUR
+ min as u64 * NANOS_PER_MINUTE
+ sec as u64 * NANOS_PER_SECOND
+ nano as u64;
Some(Self {
nanos,
offset: None,
})
}
#[must_use]
pub fn from_nanos(nanos: u64) -> Option<Self> {
if nanos >= NANOS_PER_DAY {
return None;
}
Some(Self {
nanos,
offset: None,
})
}
#[must_use]
pub fn with_offset(self, offset_secs: i32) -> Self {
Self {
nanos: self.nanos,
offset: Some(offset_secs),
}
}
#[must_use]
pub fn hour(&self) -> u32 {
(self.nanos / NANOS_PER_HOUR) as u32
}
#[must_use]
pub fn minute(&self) -> u32 {
((self.nanos % NANOS_PER_HOUR) / NANOS_PER_MINUTE) as u32
}
#[must_use]
pub fn second(&self) -> u32 {
((self.nanos % NANOS_PER_MINUTE) / NANOS_PER_SECOND) as u32
}
#[must_use]
pub fn nanosecond(&self) -> u32 {
(self.nanos % NANOS_PER_SECOND) as u32
}
#[must_use]
pub fn as_nanos(&self) -> u64 {
self.nanos
}
#[must_use]
pub fn offset_seconds(&self) -> Option<i32> {
self.offset
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
let (time_part, offset) = parse_offset_suffix(s);
let parts: Vec<&str> = time_part.splitn(2, '.').collect();
let hms: Vec<&str> = parts[0].splitn(3, ':').collect();
if hms.len() < 2 {
return None;
}
let hour: u32 = hms[0].parse().ok()?;
let min: u32 = hms[1].parse().ok()?;
let sec: u32 = if hms.len() == 3 {
hms[2].parse().ok()?
} else {
0
};
let nano: u32 = if parts.len() == 2 {
let frac = parts[1];
let padded = if frac.len() >= 9 {
&frac[..9]
} else {
return {
let n: u32 = frac.parse().ok()?;
#[allow(clippy::cast_possible_truncation)]
let scale = 10u32.pow(9 - frac.len() as u32);
let mut t = Self::from_hms_nano(hour, min, sec, n * scale)?;
if let Some(off) = offset {
t = t.with_offset(off);
}
Some(t)
};
};
padded.parse().ok()?
} else {
0
};
let mut t = Self::from_hms_nano(hour, min, sec, nano)?;
if let Some(off) = offset {
t = t.with_offset(off);
}
Some(t)
}
#[must_use]
pub fn now() -> Self {
let ts = super::Timestamp::now();
ts.to_time()
}
#[must_use]
pub fn add_duration(self, dur: &super::Duration) -> Self {
#[allow(clippy::cast_possible_wrap)]
let total = self.nanos as i64 + dur.nanos();
#[allow(clippy::cast_possible_wrap)]
let wrapped = total.rem_euclid(NANOS_PER_DAY as i64) as u64;
Self {
nanos: wrapped,
offset: self.offset,
}
}
#[must_use]
pub fn truncate(&self, unit: &str) -> Option<Self> {
let truncated_nanos = match unit {
"hour" => (self.nanos / NANOS_PER_HOUR) * NANOS_PER_HOUR,
"minute" => (self.nanos / NANOS_PER_MINUTE) * NANOS_PER_MINUTE,
"second" => (self.nanos / NANOS_PER_SECOND) * NANOS_PER_SECOND,
_ => return None,
};
Some(Self {
nanos: truncated_nanos,
offset: self.offset,
})
}
fn utc_nanos(&self) -> u64 {
match self.offset {
Some(off) => {
#[allow(clippy::cast_possible_wrap)]
let adjusted = self.nanos as i64 - off as i64 * NANOS_PER_SECOND as i64;
#[allow(clippy::cast_possible_wrap)]
let result = adjusted.rem_euclid(NANOS_PER_DAY as i64) as u64;
result
}
None => self.nanos,
}
}
}
impl Default for Time {
fn default() -> Self {
Self {
nanos: 0,
offset: None,
}
}
}
impl Ord for Time {
fn cmp(&self, other: &Self) -> Ordering {
match (self.offset, other.offset) {
(Some(_), Some(_)) => self.utc_nanos().cmp(&other.utc_nanos()),
_ => self.nanos.cmp(&other.nanos),
}
}
}
impl PartialOrd for Time {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn parse_offset_suffix(s: &str) -> (&str, Option<i32>) {
if let Some(rest) = s.strip_suffix('Z') {
return (rest, Some(0));
}
if s.len() >= 6 {
let sign_pos = s.len() - 6;
let candidate = &s[sign_pos..];
if (candidate.starts_with('+') || candidate.starts_with('-'))
&& candidate.as_bytes()[3] == b':'
{
let sign: i32 = if candidate.starts_with('+') { 1 } else { -1 };
if let (Ok(h), Ok(m)) = (
candidate[1..3].parse::<i32>(),
candidate[4..6].parse::<i32>(),
) {
return (&s[..sign_pos], Some(sign * (h * 3600 + m * 60)));
}
}
}
(s, None)
}
impl fmt::Debug for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Time({})", self)
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let h = self.hour();
let m = self.minute();
let s = self.second();
let ns = self.nanosecond();
if ns > 0 {
let frac = format!("{:09}", ns);
let trimmed = frac.trim_end_matches('0');
write!(f, "{h:02}:{m:02}:{s:02}.{trimmed}")?;
} else {
write!(f, "{h:02}:{m:02}:{s:02}")?;
}
match self.offset {
Some(0) => write!(f, "Z"),
Some(off) => {
let sign = if off >= 0 { '+' } else { '-' };
let abs = off.unsigned_abs();
let oh = abs / 3600;
let om = (abs % 3600) / 60;
write!(f, "{sign}{oh:02}:{om:02}")
}
None => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
let t = Time::from_hms(14, 30, 45).unwrap();
assert_eq!(t.hour(), 14);
assert_eq!(t.minute(), 30);
assert_eq!(t.second(), 45);
assert_eq!(t.nanosecond(), 0);
}
#[test]
fn test_with_nanos() {
let t = Time::from_hms_nano(0, 0, 0, 123_456_789).unwrap();
assert_eq!(t.nanosecond(), 123_456_789);
assert_eq!(t.to_string(), "00:00:00.123456789");
}
#[test]
fn test_validation() {
assert!(Time::from_hms(24, 0, 0).is_none());
assert!(Time::from_hms(0, 60, 0).is_none());
assert!(Time::from_hms(0, 0, 60).is_none());
assert!(Time::from_hms_nano(0, 0, 0, 1_000_000_000).is_none());
}
#[test]
fn test_parse_basic() {
let t = Time::parse("14:30:00").unwrap();
assert_eq!(t.hour(), 14);
assert_eq!(t.minute(), 30);
assert_eq!(t.second(), 0);
assert!(t.offset_seconds().is_none());
}
#[test]
fn test_parse_with_offset() {
let t = Time::parse("14:30:00+02:00").unwrap();
assert_eq!(t.hour(), 14);
assert_eq!(t.offset_seconds(), Some(7200));
let t = Time::parse("14:30:00Z").unwrap();
assert_eq!(t.offset_seconds(), Some(0));
let t = Time::parse("14:30:00-05:30").unwrap();
assert_eq!(t.offset_seconds(), Some(-19800));
}
#[test]
fn test_parse_fractional() {
let t = Time::parse("14:30:00.5").unwrap();
assert_eq!(t.nanosecond(), 500_000_000);
let t = Time::parse("14:30:00.123").unwrap();
assert_eq!(t.nanosecond(), 123_000_000);
}
#[test]
fn test_display() {
assert_eq!(Time::from_hms(9, 5, 3).unwrap().to_string(), "09:05:03");
assert_eq!(
Time::from_hms(14, 30, 0)
.unwrap()
.with_offset(0)
.to_string(),
"14:30:00Z"
);
assert_eq!(
Time::from_hms(14, 30, 0)
.unwrap()
.with_offset(5 * 3600 + 30 * 60)
.to_string(),
"14:30:00+05:30"
);
}
#[test]
fn test_ordering() {
let t1 = Time::from_hms(10, 0, 0).unwrap();
let t2 = Time::from_hms(14, 0, 0).unwrap();
assert!(t1 < t2);
}
#[test]
fn test_ordering_with_offsets() {
let t1 = Time::from_hms(14, 0, 0).unwrap().with_offset(7200);
let t2 = Time::from_hms(13, 0, 0).unwrap().with_offset(0);
assert!(t1 < t2);
}
#[test]
fn test_truncate() {
let t = Time::from_hms_nano(14, 30, 45, 123_456_789).unwrap();
let hour = t.truncate("hour").unwrap();
assert_eq!(hour.hour(), 14);
assert_eq!(hour.minute(), 0);
assert_eq!(hour.second(), 0);
assert_eq!(hour.nanosecond(), 0);
let minute = t.truncate("minute").unwrap();
assert_eq!(minute.hour(), 14);
assert_eq!(minute.minute(), 30);
assert_eq!(minute.second(), 0);
let second = t.truncate("second").unwrap();
assert_eq!(second.hour(), 14);
assert_eq!(second.minute(), 30);
assert_eq!(second.second(), 45);
assert_eq!(second.nanosecond(), 0);
assert!(t.truncate("day").is_none());
}
#[test]
fn test_truncate_preserves_offset() {
let t = Time::from_hms_nano(14, 30, 45, 0)
.unwrap()
.with_offset(19800); let truncated = t.truncate("hour").unwrap();
assert_eq!(truncated.offset_seconds(), Some(19800));
assert_eq!(truncated.hour(), 14);
assert_eq!(truncated.minute(), 0);
}
#[test]
fn test_default() {
let t = Time::default();
assert_eq!(t.hour(), 0);
assert_eq!(t.minute(), 0);
assert_eq!(t.second(), 0);
}
#[test]
fn test_add_duration_wraps_at_midnight() {
use crate::types::Duration;
let t = Time::from_hms(23, 0, 0).unwrap();
let dur = Duration::from_nanos(2 * 3_600_000_000_000); let result = t.add_duration(&dur);
assert_eq!(result.hour(), 1);
assert_eq!(result.minute(), 0);
}
#[test]
fn test_add_duration_within_day() {
use crate::types::Duration;
let t = Time::from_hms(10, 30, 0).unwrap();
let dur = Duration::from_nanos(90 * 60 * 1_000_000_000); let result = t.add_duration(&dur);
assert_eq!(result.hour(), 12);
assert_eq!(result.minute(), 0);
}
}