use core::{
f64::consts::TAU,
ffi::{CStr, c_char, c_int},
fmt,
ops::{Add, Neg, Sub},
str::FromStr,
};
use supernovas_ffi::{
novas_print_hms, novas_separator_type::NOVAS_SEP_UNITS_AND_SPACES, novas_str_hours,
};
use super::{Angle, Interval};
use crate::{
error::{Error, Result},
unit,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TimeAngle(f64);
impl TimeAngle {
pub fn from_radians(rad: f64) -> Result<Self> {
if !rad.is_finite() {
return Err(Error::NotFinite);
}
Ok(TimeAngle(normalize(rad)))
}
pub fn from_hours(hours: f64) -> Result<Self> {
Self::from_radians(hours * unit::HOUR_ANGLE)
}
pub fn from_minutes(minutes: f64) -> Result<Self> {
Self::from_hours(minutes / 60.0)
}
pub fn from_seconds(seconds: f64) -> Result<Self> {
Self::from_hours(seconds / 3600.0)
}
pub fn from_angle(angle: Angle) -> Self {
TimeAngle(normalize(angle.rad()))
}
pub fn rad(self) -> f64 {
self.0
}
pub fn deg(self) -> f64 {
self.0 / unit::DEG
}
pub fn hours(self) -> f64 {
self.0 / unit::HOUR_ANGLE
}
pub fn minutes(self) -> f64 {
self.hours() * 60.0
}
pub fn seconds(self) -> f64 {
self.hours() * 3600.0
}
}
impl From<Angle> for TimeAngle {
fn from(a: Angle) -> Self {
Self::from_angle(a)
}
}
impl From<TimeAngle> for Angle {
fn from(t: TimeAngle) -> Self {
Angle::from_radians(t.0).expect("TimeAngle inner value is finite by construction")
}
}
impl Add for TimeAngle {
type Output = TimeAngle;
fn add(self, rhs: TimeAngle) -> TimeAngle {
TimeAngle(normalize(self.0 + rhs.0))
}
}
impl Sub for TimeAngle {
type Output = TimeAngle;
fn sub(self, rhs: TimeAngle) -> TimeAngle {
TimeAngle(normalize(self.0 - rhs.0))
}
}
impl Add<Interval> for TimeAngle {
type Output = TimeAngle;
fn add(self, rhs: Interval) -> TimeAngle {
TimeAngle(normalize(
self.0 + rhs.seconds() * unit::HOUR_ANGLE / 3600.0,
))
}
}
impl Sub<Interval> for TimeAngle {
type Output = TimeAngle;
fn sub(self, rhs: Interval) -> TimeAngle {
TimeAngle(normalize(
self.0 - rhs.seconds() * unit::HOUR_ANGLE / 3600.0,
))
}
}
impl Neg for TimeAngle {
type Output = TimeAngle;
fn neg(self) -> TimeAngle {
TimeAngle(normalize(-self.0))
}
}
impl approx::AbsDiffEq for TimeAngle {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
unit::UAS
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
super::angle::wrapped_diff(self.0, other.0).abs() <= epsilon
}
}
impl FromStr for TimeAngle {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let bytes = s.as_bytes();
if bytes.contains(&0) || bytes.len() >= 64 {
return Err(Error::Parse);
}
let mut buf = [0u8; 64];
buf[..bytes.len()].copy_from_slice(bytes);
let cs = CStr::from_bytes_with_nul(&buf[..=bytes.len()]).map_err(|_| Error::Parse)?;
let hours = unsafe { novas_str_hours(cs.as_ptr()) };
if !hours.is_finite() {
return Err(Error::Parse);
}
Self::from_hours(hours)
}
}
impl fmt::Display for TimeAngle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let decimals: c_int = f.precision().map_or(3, |p| p.try_into().unwrap_or(3));
let mut buf = [0 as c_char; 100];
let n = unsafe {
novas_print_hms(
self.hours(),
NOVAS_SEP_UNITS_AND_SPACES,
decimals,
buf.as_mut_ptr(),
buf.len() as c_int,
)
};
if n < 0 {
return Err(fmt::Error);
}
let cs = unsafe { CStr::from_ptr(buf.as_ptr()) };
f.write_str(cs.to_str().map_err(|_| fmt::Error)?)
}
}
fn normalize(r: f64) -> f64 {
let r = r - TAU * (r / TAU).floor();
if r >= TAU { 0.0 } else { r }
}
#[cfg(test)]
mod tests {
#[cfg(not(feature = "std"))]
use std::format;
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn rejects_non_finite() {
assert!(matches!(
TimeAngle::from_hours(f64::NAN),
Err(Error::NotFinite)
));
}
#[test]
fn round_trip_units() {
let t = TimeAngle::from_hours(6.0).unwrap();
assert!((t.hours() - 6.0).abs() < 1e-12);
assert!((t.minutes() - 360.0).abs() < 1e-9);
assert!((t.seconds() - 21600.0).abs() < 1e-7);
assert!((t.rad() - core::f64::consts::FRAC_PI_2).abs() < 1e-12);
assert!((t.deg() - 90.0).abs() < 1e-12);
}
#[test]
fn normalizes_into_24h_range() {
let t = TimeAngle::from_hours(25.0).unwrap();
assert!((t.hours() - 1.0).abs() < 1e-12);
let t = TimeAngle::from_hours(-1.0).unwrap();
assert!((t.hours() - 23.0).abs() < 1e-12);
}
#[test]
fn approx_eq_across_midnight() {
let a = TimeAngle::from_hours(23.99999).unwrap();
let b = TimeAngle::from_hours(0.00001).unwrap();
let one_sec = unit::HOUR_ANGLE / 3600.0;
assert_abs_diff_eq!(a, b, epsilon = one_sec);
}
#[test]
fn arithmetic() {
let a = TimeAngle::from_hours(10.0).unwrap();
let b = TimeAngle::from_hours(16.0).unwrap();
assert!(((a + b).hours() - 2.0).abs() < 1e-12);
assert!(((a - b).hours() - 18.0).abs() < 1e-12);
}
#[test]
fn add_interval_advances_time_of_day() {
let t = TimeAngle::from_hours(12.0).unwrap();
let dt = Interval::from_minutes(30.0).unwrap();
let later = t + dt;
assert!((later.hours() - 12.5).abs() < 1e-12);
}
#[test]
fn from_angle_lifts_negatives() {
let a = Angle::from_degrees(-90.0).unwrap();
let t = TimeAngle::from_angle(a);
assert!((t.hours() - 18.0).abs() < 1e-12);
}
#[test]
fn parses_hms_string() {
let t: TimeAngle = "12:34:56.789".parse().unwrap();
let expected = 12.0 + 34.0 / 60.0 + 56.789 / 3600.0;
assert!((t.hours() - expected).abs() < 1e-9);
}
#[test]
fn displays_hms() {
let t = TimeAngle::from_hours(12.5).unwrap();
let s = format!("{t}");
assert!(s.contains("12"), "expected HMS string, got {s:?}");
}
}