use std::fmt::Display;
use std::str::FromStr;
use std::{cmp::Ordering, sync::OnceLock};
use crate::units::Angle;
use num::ToPrimitive;
use regex::Regex;
use thiserror::Error;
use super::subsecond::Subsecond;
use crate::i64::consts::{
SECONDS_PER_DAY, SECONDS_PER_HALF_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE,
};
fn iso_regex() -> &'static Regex {
static ISO: OnceLock<Regex> = OnceLock::new();
ISO.get_or_init(|| {
Regex::new(r"(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<subsecond>\.\d+)?").unwrap()
})
}
#[derive(Debug, Copy, Clone, Error)]
#[error("seconds must be in the range [0.0..86401.0) but was {0}")]
pub struct InvalidSeconds(f64);
impl PartialEq for InvalidSeconds {
fn eq(&self, other: &Self) -> bool {
self.0.total_cmp(&other.0) == Ordering::Equal
}
}
impl Eq for InvalidSeconds {}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum TimeOfDayError {
#[error("hour must be in the range [0..24) but was {0}")]
InvalidHour(u8),
#[error("minute must be in the range [0..60) but was {0}")]
InvalidMinute(u8),
#[error("second must be in the range [0..61) but was {0}")]
InvalidSecond(u8),
#[error("second must be in the range [0..86401) but was {0}")]
InvalidSecondOfDay(u64),
#[error(transparent)]
InvalidSeconds(#[from] InvalidSeconds),
#[error("leap seconds are only valid at the end of the day")]
InvalidLeapSecond,
#[error("invalid ISO string `{0}`")]
InvalidIsoString(String),
}
pub trait CivilTime {
fn time(&self) -> TimeOfDay;
fn hour(&self) -> u8 {
self.time().hour()
}
fn minute(&self) -> u8 {
self.time().minute()
}
fn second(&self) -> u8 {
self.time().second()
}
fn as_seconds_f64(&self) -> f64 {
self.time().subsecond().as_seconds_f64() + self.time().second() as f64
}
fn millisecond(&self) -> u32 {
self.time().subsecond().milliseconds()
}
fn microsecond(&self) -> u32 {
self.time().subsecond().microseconds()
}
fn nanosecond(&self) -> u32 {
self.time().subsecond().nanoseconds()
}
fn picosecond(&self) -> u32 {
self.time().subsecond().picoseconds()
}
fn femtosecond(&self) -> u32 {
self.time().subsecond().femtoseconds()
}
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimeOfDay {
hour: u8,
minute: u8,
second: u8,
subsecond: Subsecond,
}
impl TimeOfDay {
pub const MIDNIGHT: Self = TimeOfDay {
hour: 0,
minute: 0,
second: 0,
subsecond: Subsecond::ZERO,
};
pub const NOON: Self = TimeOfDay {
hour: 12,
minute: 0,
second: 0,
subsecond: Subsecond::ZERO,
};
pub fn new(hour: u8, minute: u8, second: u8) -> Result<Self, TimeOfDayError> {
if !(0..24).contains(&hour) {
return Err(TimeOfDayError::InvalidHour(hour));
}
if !(0..60).contains(&minute) {
return Err(TimeOfDayError::InvalidMinute(minute));
}
if !(0..61).contains(&second) {
return Err(TimeOfDayError::InvalidSecond(second));
}
Ok(Self {
hour,
minute,
second,
subsecond: Subsecond::default(),
})
}
pub fn from_iso(iso: &str) -> Result<Self, TimeOfDayError> {
let caps = iso_regex()
.captures(iso)
.ok_or(TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
let hour: u8 = caps["hour"]
.parse()
.map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
let minute: u8 = caps["minute"]
.parse()
.map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
let second: u8 = caps["second"]
.parse()
.map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
let mut time = TimeOfDay::new(hour, minute, second)?;
if let Some(subsecond) = caps.name("subsecond") {
let subsecond_str = subsecond.as_str().trim_start_matches('.');
let subsecond: Subsecond = subsecond_str
.parse()
.map_err(|_| TimeOfDayError::InvalidIsoString(iso.to_owned()))?;
time.with_subsecond(subsecond);
}
Ok(time)
}
pub fn from_hour(hour: u8) -> Result<Self, TimeOfDayError> {
Self::new(hour, 0, 0)
}
pub fn from_hour_and_minute(hour: u8, minute: u8) -> Result<Self, TimeOfDayError> {
Self::new(hour, minute, 0)
}
pub fn from_hms(hour: u8, minute: u8, seconds: f64) -> Result<Self, TimeOfDayError> {
if !(0.0..86401.0).contains(&seconds) {
return Err(TimeOfDayError::InvalidSeconds(InvalidSeconds(seconds)));
}
let second = seconds.trunc() as u8;
let fraction = seconds.fract();
let subsecond = Subsecond::from_f64(fraction)
.ok_or(TimeOfDayError::InvalidSeconds(InvalidSeconds(seconds)))?;
Ok(Self::new(hour, minute, second)?.with_subsecond(subsecond))
}
pub fn from_second_of_day(second_of_day: u64) -> Result<Self, TimeOfDayError> {
if !(0..86401).contains(&second_of_day) {
return Err(TimeOfDayError::InvalidSecondOfDay(second_of_day));
}
if second_of_day == SECONDS_PER_DAY as u64 {
return Self::new(23, 59, 60);
}
let hour = (second_of_day / 3600) as u8;
let minute = ((second_of_day % 3600) / 60) as u8;
let second = (second_of_day % 60) as u8;
Self::new(hour, minute, second)
}
pub fn from_seconds_since_j2000(seconds: i64) -> Self {
let mut second_of_day = (seconds + SECONDS_PER_HALF_DAY) % SECONDS_PER_DAY;
if second_of_day.is_negative() {
second_of_day += SECONDS_PER_DAY;
}
Self::from_second_of_day(
second_of_day
.to_u64()
.unwrap_or_else(|| unreachable!("second of day should be positive")),
)
.unwrap_or_else(|_| unreachable!("second of day should be in range"))
}
pub fn with_subsecond(&mut self, subsecond: Subsecond) -> Self {
self.subsecond = subsecond;
*self
}
pub fn hour(&self) -> u8 {
self.hour
}
pub fn minute(&self) -> u8 {
self.minute
}
pub fn second(&self) -> u8 {
self.second
}
pub fn subsecond(&self) -> Subsecond {
self.subsecond
}
pub fn seconds_f64(&self) -> f64 {
self.subsecond.as_seconds_f64() + self.second as f64
}
pub fn second_of_day(&self) -> i64 {
self.hour as i64 * SECONDS_PER_HOUR
+ self.minute as i64 * SECONDS_PER_MINUTE
+ self.second as i64
}
pub fn to_angle(&self) -> Angle {
Angle::from_hms(self.hour as i64, self.minute, self.seconds_f64())
}
}
impl Display for TimeOfDay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let precision = f.precision().unwrap_or(3);
write!(
f,
"{:02}:{:02}:{:02}{}",
self.hour,
self.minute,
self.second,
format!("{:.*}", precision, self.subsecond).trim_start_matches('0')
)
}
}
impl FromStr for TimeOfDay {
type Err = TimeOfDayError;
fn from_str(iso: &str) -> Result<Self, Self::Err> {
Self::from_iso(iso)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(43201, TimeOfDay::new(12, 0, 1))]
#[case(86399, TimeOfDay::new(23, 59, 59))]
#[case(86400, TimeOfDay::new(23, 59, 60))]
fn test_time_of_day_from_second_of_day(
#[case] second_of_day: u64,
#[case] expected: Result<TimeOfDay, TimeOfDayError>,
) {
let actual = TimeOfDay::from_second_of_day(second_of_day);
assert_eq!(actual, expected);
}
#[test]
fn test_time_of_day_display() {
let subsecond: Subsecond = "123456789123456".parse().unwrap();
let time = TimeOfDay::new(12, 0, 0).unwrap().with_subsecond(subsecond);
assert_eq!(format!("{time}"), "12:00:00.123");
assert_eq!(format!("{time:.15}"), "12:00:00.123456789123456");
}
#[rstest]
#[case(TimeOfDay::new(24, 0, 0), Err(TimeOfDayError::InvalidHour(24)))]
#[case(TimeOfDay::new(0, 60, 0), Err(TimeOfDayError::InvalidMinute(60)))]
#[case(TimeOfDay::new(0, 0, 61), Err(TimeOfDayError::InvalidSecond(61)))]
#[case(
TimeOfDay::from_second_of_day(86401),
Err(TimeOfDayError::InvalidSecondOfDay(86401))
)]
#[case(TimeOfDay::from_hms(12, 0, -0.123), Err(TimeOfDayError::InvalidSeconds(InvalidSeconds(-0.123))))]
fn test_time_of_day_error(
#[case] actual: Result<TimeOfDay, TimeOfDayError>,
#[case] expected: Result<TimeOfDay, TimeOfDayError>,
) {
assert_eq!(actual, expected);
}
#[rstest]
#[case("12:13:14", Ok(TimeOfDay::new(12, 13, 14).unwrap()))]
#[case("12:13:14.123", Ok(TimeOfDay::new(12, 13, 14).unwrap().with_subsecond("123".parse().unwrap())))]
#[case("2:13:14.123", Err(TimeOfDayError::InvalidIsoString("2:13:14.123".to_string())))]
#[case("12:3:14.123", Err(TimeOfDayError::InvalidIsoString("12:3:14.123".to_string())))]
#[case("12:13:4.123", Err(TimeOfDayError::InvalidIsoString("12:13:4.123".to_string())))]
fn test_time_of_day_from_string(
#[case] iso: &str,
#[case] expected: Result<TimeOfDay, TimeOfDayError>,
) {
let actual: Result<TimeOfDay, TimeOfDayError> = iso.parse();
assert_eq!(actual, expected)
}
#[test]
fn test_invalid_seconds_eq() {
let a = InvalidSeconds(-f64::NAN);
let b = InvalidSeconds(f64::NAN);
assert_ne!(a, b);
let c = InvalidSeconds(f64::NAN);
let d = InvalidSeconds(f64::NAN);
assert_eq!(c, d);
}
}