use core::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Dms {
pub sign: i8,
pub degrees: u32,
pub minutes: u32,
pub seconds: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hms {
pub hours: u32,
pub minutes: u32,
pub seconds: f64,
}
#[must_use]
pub fn normalize_degrees(angle: f64) -> f64 {
if !angle.is_finite() {
return 0.0;
}
let result = angle % 360.0;
let result = if result < 0.0 { result + 360.0 } else { result };
if result >= 360.0 { 0.0 } else { result }
}
#[must_use]
pub fn normalize_degrees_signed(angle: f64) -> f64 {
if !angle.is_finite() {
return 0.0;
}
let mut result = normalize_degrees(angle);
if result >= 180.0 {
result -= 360.0;
}
result
}
#[must_use]
pub fn normalize_radians(angle: f64) -> f64 {
if !angle.is_finite() {
return 0.0;
}
let two_pi = 2.0 * PI;
let result = angle % two_pi;
let result = if result < 0.0 {
result + two_pi
} else {
result
};
if result >= two_pi { 0.0 } else { result }
}
#[must_use]
pub fn deg_to_rad(deg: f64) -> f64 {
deg * (PI / 180.0)
}
#[must_use]
pub fn rad_to_deg(rad: f64) -> f64 {
rad * (180.0 / PI)
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn deg_to_dms(decimal_degrees: f64) -> Dms {
let sign = if decimal_degrees < 0.0 { -1i8 } else { 1i8 };
let abs_deg = decimal_degrees.abs();
let mut degrees = abs_deg as u32;
let remainder = (abs_deg - f64::from(degrees)) * 60.0;
let mut minutes = remainder as u32;
let mut seconds = (remainder - f64::from(minutes)) * 60.0;
if seconds >= 60.0 {
seconds -= 60.0;
minutes += 1;
}
if minutes >= 60 {
minutes -= 60;
degrees += 1;
}
Dms {
sign,
degrees,
minutes,
seconds,
}
}
#[must_use]
pub fn dms_to_deg(dms: &Dms) -> f64 {
let abs_val = f64::from(dms.degrees) + f64::from(dms.minutes) / 60.0 + dms.seconds / 3600.0;
f64::from(dms.sign) * abs_val
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn deg_to_hms(decimal_degrees: f64) -> Hms {
let normalized = normalize_degrees(decimal_degrees);
let total_hours = normalized / 15.0;
let mut hours = total_hours as u32;
let remainder = (total_hours - f64::from(hours)) * 60.0;
let mut minutes = remainder as u32;
let mut seconds = (remainder - f64::from(minutes)) * 60.0;
if seconds >= 60.0 {
seconds -= 60.0;
minutes += 1;
}
if minutes >= 60 {
minutes -= 60;
hours += 1;
}
Hms {
hours,
minutes,
seconds,
}
}
#[must_use]
pub fn hms_to_deg(hms: &Hms) -> f64 {
(f64::from(hms.hours) + f64::from(hms.minutes) / 60.0 + hms.seconds / 3600.0) * 15.0
}
#[must_use]
pub fn angular_separation(a: f64, b: f64) -> f64 {
let diff = normalize_degrees(a - b);
if diff > 180.0 { 360.0 - diff } else { diff }
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-12;
#[test]
fn normalize_degrees_zero() {
assert!((normalize_degrees(0.0) - 0.0).abs() < EPS);
}
#[test]
fn normalize_degrees_positive() {
assert!((normalize_degrees(45.0) - 45.0).abs() < EPS);
}
#[test]
fn normalize_degrees_360_becomes_0() {
assert!((normalize_degrees(360.0) - 0.0).abs() < EPS);
}
#[test]
fn normalize_degrees_negative() {
assert!((normalize_degrees(-90.0) - 270.0).abs() < EPS);
}
#[test]
fn normalize_degrees_large_positive() {
assert!((normalize_degrees(720.0) - 0.0).abs() < EPS);
}
#[test]
fn normalize_degrees_large_negative() {
assert!((normalize_degrees(-450.0) - 270.0).abs() < EPS);
}
#[test]
fn normalize_degrees_near_360() {
assert!((normalize_degrees(359.9999) - 359.9999).abs() < EPS);
}
#[test]
fn normalize_degrees_nan() {
assert!((normalize_degrees(f64::NAN) - 0.0).abs() < EPS);
}
#[test]
fn normalize_degrees_inf() {
assert!((normalize_degrees(f64::INFINITY) - 0.0).abs() < EPS);
}
#[test]
fn normalize_degrees_signed_positive() {
assert!((normalize_degrees_signed(90.0) - 90.0).abs() < EPS);
}
#[test]
fn normalize_degrees_signed_negative() {
assert!((normalize_degrees_signed(-45.0) - (-45.0)).abs() < EPS);
}
#[test]
fn normalize_degrees_signed_270_becomes_neg90() {
assert!((normalize_degrees_signed(270.0) - (-90.0)).abs() < EPS);
}
#[test]
fn normalize_degrees_signed_180() {
assert!((normalize_degrees_signed(180.0) - (-180.0)).abs() < EPS);
}
#[test]
fn normalize_radians_zero() {
assert!((normalize_radians(0.0) - 0.0).abs() < EPS);
}
#[test]
fn normalize_radians_pi() {
assert!((normalize_radians(PI) - PI).abs() < EPS);
}
#[test]
fn normalize_radians_two_pi() {
assert!((normalize_radians(2.0 * PI) - 0.0).abs() < EPS);
}
#[test]
fn normalize_radians_negative() {
assert!((normalize_radians(-PI) - PI).abs() < EPS);
}
#[test]
fn deg_rad_roundtrip() {
let angle = 123.456_f64;
assert!((rad_to_deg(deg_to_rad(angle)) - angle).abs() < EPS);
}
#[test]
fn deg_rad_90_is_half_pi() {
assert!((deg_to_rad(90.0) - PI / 2.0).abs() < EPS);
}
#[test]
fn dms_positive() {
let dms = deg_to_dms(45.5);
assert_eq!(dms.sign, 1);
assert_eq!(dms.degrees, 45);
assert_eq!(dms.minutes, 30);
assert!((dms.seconds - 0.0).abs() < 0.01);
}
#[test]
fn dms_negative() {
let dms = deg_to_dms(-10.25);
assert_eq!(dms.sign, -1);
assert_eq!(dms.degrees, 10);
assert_eq!(dms.minutes, 15);
assert!((dms.seconds - 0.0).abs() < 0.01);
}
#[test]
fn dms_roundtrip() {
let original = 37.123_456_f64;
let dms = deg_to_dms(original);
let back = dms_to_deg(&dms);
assert!((back - original).abs() < 1e-10);
}
#[test]
fn dms_zero() {
let dms = deg_to_dms(0.0);
assert_eq!(dms.sign, 1);
assert_eq!(dms.degrees, 0);
assert_eq!(dms.minutes, 0);
assert!((dms.seconds - 0.0).abs() < 0.01);
}
#[test]
fn hms_zero() {
let hms = deg_to_hms(0.0);
assert_eq!(hms.hours, 0);
assert_eq!(hms.minutes, 0);
assert!((hms.seconds - 0.0).abs() < 0.01);
}
#[test]
fn hms_90deg() {
let hms = deg_to_hms(90.0);
assert_eq!(hms.hours, 6);
assert_eq!(hms.minutes, 0);
assert!((hms.seconds - 0.0).abs() < 0.01);
}
#[test]
fn hms_roundtrip() {
let original = 135.0_f64;
let hms = deg_to_hms(original);
let back = hms_to_deg(&hms);
assert!((back - original).abs() < 1e-10);
}
#[test]
fn separation_same() {
assert!((angular_separation(45.0, 45.0) - 0.0).abs() < EPS);
}
#[test]
fn separation_opposite() {
assert!((angular_separation(0.0, 180.0) - 180.0).abs() < EPS);
}
#[test]
fn separation_wrap_around() {
assert!((angular_separation(10.0, 350.0) - 20.0).abs() < EPS);
}
#[test]
fn separation_negative_input() {
assert!((angular_separation(-10.0, 10.0) - 20.0).abs() < EPS);
}
}