use crate::scalar::Transcendental;
use crate::{Quantity, Unit};
use core::f64::consts::TAU;
use qtty_derive::Unit;
#[inline]
fn rem_euclid(x: f64, modulus: f64) -> f64 {
#[cfg(feature = "std")]
{
x.rem_euclid(modulus)
}
#[cfg(not(feature = "std"))]
{
let r = crate::libm::fmod(x, modulus);
if r < 0.0 {
r + modulus
} else {
r
}
}
}
pub use crate::dimension::Angular;
pub trait AngularUnit: Unit<Dim = Angular> {
const FULL_TURN: f64;
const HALF_TURN: f64;
const QUARTER_TURN: f64;
}
impl<T: Unit<Dim = Angular>> AngularUnit for T {
const FULL_TURN: f64 = Radians::new(TAU).to_const::<T>().value();
const HALF_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.5;
const QUARTER_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.25;
}
#[cfg(feature = "astro")]
mod astro;
#[cfg(feature = "astro")]
pub use astro::*;
#[cfg(feature = "navigation")]
mod navigation;
#[cfg(feature = "navigation")]
pub use navigation::*;
impl<U: AngularUnit + Copy> Quantity<U> {
pub const TAU: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
pub const FULL_TURN: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
pub const HALF_TURN: Quantity<U> = Quantity::<U>::new(U::HALF_TURN);
pub const QUARTER_TURN: Quantity<U> = Quantity::<U>::new(U::QUARTER_TURN);
#[inline]
pub const fn signum_const(self) -> f64 {
self.value().signum()
}
#[inline]
pub fn normalize(self) -> Self {
self.wrap_pos()
}
#[inline]
pub fn wrap_pos(self) -> Self {
Self::new(rem_euclid(self.value(), U::FULL_TURN))
}
#[inline]
pub fn wrap_signed(self) -> Self {
let full = U::FULL_TURN;
let half = 0.5 * full;
let x = self.value();
let y = rem_euclid(x + half, full) - half;
let norm = if y <= -half { y + full } else { y };
Self::new(norm)
}
#[inline]
pub fn wrap_signed_lo(self) -> Self {
let mut y = self.wrap_signed().value(); let half = 0.5 * U::FULL_TURN;
if y >= half {
y -= U::FULL_TURN;
}
Self::new(y)
}
#[inline]
pub fn wrap_quarter_fold(self) -> Self {
let full = U::FULL_TURN;
let half = 0.5 * full;
let quarter = 0.25 * full;
let y = rem_euclid(self.value() + quarter, full);
Self::new(quarter - (y - half).abs())
}
#[inline]
pub fn signed_separation(self, other: Self) -> Self {
(self - other).wrap_signed()
}
#[inline]
pub fn abs_separation(self, other: Self) -> Self {
let sep = self.signed_separation(other);
Self::new(sep.value().abs())
}
#[inline]
pub fn wrap_to_signed_pi(self) -> Self {
self.wrap_signed()
}
#[inline]
pub fn wrap_to_unsigned_pi(self) -> Self {
self.wrap_pos()
}
#[inline]
pub fn fold_to_pi(self) -> Self {
Self::new(self.wrap_signed().value().abs())
}
}
impl<U: AngularUnit + Copy, S: Transcendental> Quantity<U, S> {
#[inline]
pub fn sin(self) -> S {
let x_rad = self.to::<Radian>().value();
x_rad.sin()
}
#[inline]
pub fn cos(self) -> S {
let x_rad = self.to::<Radian>().value();
x_rad.cos()
}
#[inline]
pub fn tan(self) -> S {
let x_rad = self.to::<Radian>().value();
x_rad.tan()
}
#[inline]
pub fn sin_cos(self) -> (S, S) {
let x_rad = self.to::<Radian>().value();
x_rad.sin_cos()
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
#[unit(symbol = "°", dimension = Angular, ratio = 1.0)]
pub struct Degree;
pub type Deg = Degree;
pub type Degrees = Quantity<Deg>;
pub const DEG: Degrees = Degrees::new(1.0);
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
#[unit(symbol = "rad", dimension = Angular, ratio = 180.0 / core::f64::consts::PI)]
pub struct Radian;
pub type Rad = Radian;
pub type Radians = Quantity<Rad>;
pub const RAD: Radians = Radians::new(1.0);
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
#[unit(symbol = "mrad", dimension = Angular, ratio = (180.0 / core::f64::consts::PI) / 1_000.0)]
pub struct Milliradian;
pub type Mrad = Milliradian;
pub type Milliradians = Quantity<Mrad>;
pub const MRAD: Milliradians = Milliradians::new(1.0);
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
#[unit(symbol = "tr", dimension = Angular, ratio = 360.0)]
pub struct Turn;
pub type Turns = Quantity<Turn>;
pub const TURN: Turns = Turns::new(1.0);
impl Degrees {
pub const fn from_dms(deg: i32, min: u32, sec: f64) -> Self {
let sign = if deg < 0 { -1.0 } else { 1.0 };
let d_abs = if deg < 0 { -(deg as f64) } else { deg as f64 };
let m = min as f64 / 60.0;
let s = sec / 3600.0;
let total = sign * (d_abs + m + s);
Self::new(total)
}
pub const fn from_dms_sign(sign: i8, deg: u32, min: u32, sec: f64) -> Self {
let s = if sign < 0 { -1.0 } else { 1.0 };
let total = (deg as f64) + (min as f64) / 60.0 + (sec / 3600.0);
Self::new(s * total)
}
}
#[macro_export]
#[doc(hidden)]
macro_rules! angular_units {
($cb:path) => {
$cb!(Degree, Radian, Milliradian, Turn);
};
}
angular_units!(crate::impl_unit_from_conversions);
#[cfg(feature = "cross-unit-ops")]
angular_units!(crate::impl_unit_cross_unit_ops);
#[cfg(all(feature = "astro", feature = "navigation"))]
crate::__impl_from_each_extra_to_bases!(
{Arcminute, Arcsecond, MilliArcsecond, MicroArcsecond, HourAngle}
Gradian
);
#[cfg(all(feature = "astro", feature = "navigation", feature = "cross-unit-ops"))]
crate::__impl_cross_ops_each_extra_to_bases!(
{Arcminute, Arcsecond, MilliArcsecond, MicroArcsecond, HourAngle}
Gradian
);
#[cfg(test)]
angular_units!(crate::assert_units_are_builtin);
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use approx::{assert_abs_diff_eq, assert_relative_eq};
use core::f64::consts::{PI, TAU};
use proptest::prelude::*;
#[test]
fn test_full_turn() {
assert_abs_diff_eq!(Radian::FULL_TURN, TAU, epsilon = 1e-12);
assert_eq!(Degree::FULL_TURN, 360.0);
#[cfg(feature = "astro")]
assert_eq!(Arcsecond::FULL_TURN, 1_296_000.0);
}
#[test]
fn test_half_turn() {
assert_abs_diff_eq!(Radian::HALF_TURN, PI, epsilon = 1e-12);
assert_eq!(Degree::HALF_TURN, 180.0);
#[cfg(feature = "astro")]
assert_eq!(Arcsecond::HALF_TURN, 648_000.0);
}
#[test]
fn test_quarter_turn() {
assert_abs_diff_eq!(Radian::QUARTER_TURN, PI / 2.0, epsilon = 1e-12);
assert_eq!(Degree::QUARTER_TURN, 90.0);
#[cfg(feature = "astro")]
assert_eq!(Arcsecond::QUARTER_TURN, 324_000.0);
}
#[test]
fn test_quantity_constants() {
assert_eq!(Degrees::FULL_TURN.value(), 360.0);
assert_eq!(Degrees::HALF_TURN.value(), 180.0);
assert_eq!(Degrees::QUARTER_TURN.value(), 90.0);
assert_eq!(Degrees::TAU.value(), 360.0);
}
#[test]
fn conversion_degrees_to_radians() {
let deg = Degrees::new(180.0);
let rad = deg.to::<Radian>();
assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
}
#[test]
fn conversion_radians_to_degrees() {
let rad = Radians::new(PI);
let deg = rad.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 180.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_degrees_to_arcseconds() {
let deg = Degrees::new(1.0);
let arcs = deg.to::<Arcsecond>();
assert_abs_diff_eq!(arcs.value(), 3600.0, epsilon = 1e-9);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_arcseconds_to_degrees() {
let arcs = Arcseconds::new(3600.0);
let deg = arcs.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_degrees_to_milliarcseconds() {
let deg = Degrees::new(1.0);
let mas = deg.to::<MilliArcsecond>();
assert_abs_diff_eq!(mas.value(), 3_600_000.0, epsilon = 1e-6);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_hour_angles_to_degrees() {
let ha = HourAngles::new(1.0);
let deg = ha.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 15.0, epsilon = 1e-12);
}
#[test]
fn conversion_roundtrip() {
let original = Degrees::new(123.456);
let rad = original.to::<Radian>();
let back = rad.to::<Degree>();
assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
}
#[test]
fn from_impl_degrees_radians() {
let deg = Degrees::new(90.0);
let rad: Radians = deg.into();
assert_abs_diff_eq!(rad.value(), PI / 2.0, epsilon = 1e-12);
let rad2 = Radians::new(PI);
let deg2: Degrees = rad2.into();
assert_abs_diff_eq!(deg2.value(), 180.0, epsilon = 1e-12);
}
#[test]
fn test_trig() {
let a = Degrees::new(90.0);
assert!((a.sin() - 1.0).abs() < 1e-12);
assert!(a.cos().abs() < 1e-12);
}
#[test]
fn trig_sin_known_values() {
assert_abs_diff_eq!(Degrees::new(0.0).sin(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(30.0).sin(), 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(90.0).sin(), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(180.0).sin(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(270.0).sin(), -1.0, epsilon = 1e-12);
}
#[test]
fn trig_cos_known_values() {
assert_abs_diff_eq!(Degrees::new(0.0).cos(), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(60.0).cos(), 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(90.0).cos(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(180.0).cos(), -1.0, epsilon = 1e-12);
}
#[test]
fn trig_tan_known_values() {
assert_abs_diff_eq!(Degrees::new(0.0).tan(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(45.0).tan(), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(180.0).tan(), 0.0, epsilon = 1e-12);
}
#[test]
fn trig_sin_cos_consistency() {
let angle = Degrees::new(37.5);
let (sin, cos) = angle.sin_cos();
assert_abs_diff_eq!(sin, angle.sin(), epsilon = 1e-15);
assert_abs_diff_eq!(cos, angle.cos(), epsilon = 1e-15);
}
#[test]
fn trig_pythagorean_identity() {
let angle = Degrees::new(123.456);
let sin = angle.sin();
let cos = angle.cos();
assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
}
#[test]
fn trig_radians() {
assert_abs_diff_eq!(Radians::new(0.0).sin(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(Radians::new(PI / 2.0).sin(), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(Radians::new(PI).cos(), -1.0, epsilon = 1e-12);
}
#[test]
fn signum_positive() {
assert_eq!(Degrees::new(45.0).signum(), 1.0);
}
#[test]
fn signum_negative() {
assert_eq!(Degrees::new(-45.0).signum(), -1.0);
}
#[test]
fn signum_zero() {
assert_eq!(Degrees::new(0.0).signum(), 1.0);
}
#[test]
fn wrap_pos_basic() {
assert_abs_diff_eq!(
Degrees::new(370.0).wrap_pos().value(),
10.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(Degrees::new(720.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(Degrees::new(0.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
}
#[test]
fn wrap_pos_negative() {
assert_abs_diff_eq!(
Degrees::new(-10.0).wrap_pos().value(),
350.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-370.0).wrap_pos().value(),
350.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-720.0).wrap_pos().value(),
0.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_pos_boundary() {
assert_abs_diff_eq!(Degrees::new(360.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(
Degrees::new(-360.0).wrap_pos().value(),
0.0,
epsilon = 1e-12
);
}
#[test]
fn normalize_is_wrap_pos() {
let angle = Degrees::new(450.0);
assert_eq!(angle.normalize().value(), angle.wrap_pos().value());
}
#[test]
fn test_wrap_signed() {
let a = Degrees::new(370.0).wrap_signed();
assert_eq!(a.value(), 10.0);
let b = Degrees::new(-190.0).wrap_signed();
assert_eq!(b.value(), 170.0);
}
#[test]
fn wrap_signed_basic() {
assert_abs_diff_eq!(
Degrees::new(10.0).wrap_signed().value(),
10.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-10.0).wrap_signed().value(),
-10.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_signed_over_180() {
assert_abs_diff_eq!(
Degrees::new(190.0).wrap_signed().value(),
-170.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(270.0).wrap_signed().value(),
-90.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_signed_boundary_180() {
assert_abs_diff_eq!(
Degrees::new(180.0).wrap_signed().value(),
180.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-180.0).wrap_signed().value(),
180.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_signed_large_values() {
assert_abs_diff_eq!(
Degrees::new(540.0).wrap_signed().value(),
180.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-540.0).wrap_signed().value(),
180.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_quarter_fold_basic() {
assert_abs_diff_eq!(
Degrees::new(0.0).wrap_quarter_fold().value(),
0.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(45.0).wrap_quarter_fold().value(),
45.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-45.0).wrap_quarter_fold().value(),
-45.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_quarter_fold_boundary() {
assert_abs_diff_eq!(
Degrees::new(90.0).wrap_quarter_fold().value(),
90.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-90.0).wrap_quarter_fold().value(),
-90.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_quarter_fold_over_90() {
assert_abs_diff_eq!(
Degrees::new(100.0).wrap_quarter_fold().value(),
80.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(135.0).wrap_quarter_fold().value(),
45.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(180.0).wrap_quarter_fold().value(),
0.0,
epsilon = 1e-12
);
}
#[test]
fn signed_separation_basic() {
let a = Degrees::new(30.0);
let b = Degrees::new(50.0);
assert_abs_diff_eq!(a.signed_separation(b).value(), -20.0, epsilon = 1e-12);
assert_abs_diff_eq!(b.signed_separation(a).value(), 20.0, epsilon = 1e-12);
}
#[test]
fn signed_separation_wrap() {
let a = Degrees::new(10.0);
let b = Degrees::new(350.0);
assert_abs_diff_eq!(a.signed_separation(b).value(), 20.0, epsilon = 1e-12);
assert_abs_diff_eq!(b.signed_separation(a).value(), -20.0, epsilon = 1e-12);
}
#[test]
fn abs_separation() {
let a = Degrees::new(30.0);
let b = Degrees::new(50.0);
assert_abs_diff_eq!(a.abs_separation(b).value(), 20.0, epsilon = 1e-12);
assert_abs_diff_eq!(b.abs_separation(a).value(), 20.0, epsilon = 1e-12);
}
#[test]
fn wrap_to_signed_pi_matches_wrap_signed_radians() {
for &x in &[
-7.0 * PI,
-PI - 0.1,
-PI,
-1.0,
0.0,
1.0,
PI,
PI + 0.1,
7.0 * PI,
] {
let a = Radians::new(x);
assert_abs_diff_eq!(
a.wrap_to_signed_pi().value(),
a.wrap_signed().value(),
epsilon = 1e-12
);
}
}
#[test]
fn wrap_to_signed_pi_degree_analog() {
assert_abs_diff_eq!(
Degrees::new(190.0).wrap_to_signed_pi().value(),
-170.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(180.0).wrap_to_signed_pi().value(),
180.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-180.0).wrap_to_signed_pi().value(),
180.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(7.0 * 180.0).wrap_to_signed_pi().value(),
180.0,
epsilon = 1e-12
);
}
#[test]
fn wrap_to_unsigned_pi_matches_wrap_pos_radians() {
for &x in &[-7.0 * PI, -PI, -1.0, 0.0, 1.0, PI, TAU, 7.0 * PI] {
let a = Radians::new(x);
assert_abs_diff_eq!(
a.wrap_to_unsigned_pi().value(),
a.wrap_pos().value(),
epsilon = 1e-12
);
}
}
#[test]
fn wrap_to_unsigned_pi_degree_analog() {
assert_abs_diff_eq!(
Degrees::new(370.0).wrap_to_unsigned_pi().value(),
10.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-10.0).wrap_to_unsigned_pi().value(),
350.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(360.0).wrap_to_unsigned_pi().value(),
0.0,
epsilon = 1e-12
);
}
#[test]
fn fold_to_pi_radians() {
assert_abs_diff_eq!(Radians::new(0.5).fold_to_pi().value(), 0.5, epsilon = 1e-12);
assert_abs_diff_eq!(
Radians::new(-0.5).fold_to_pi().value(),
0.5,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Radians::new(PI + 0.1).fold_to_pi().value(),
PI - 0.1,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Radians::new(7.0 * PI).fold_to_pi().value(),
PI,
epsilon = 1e-12
);
assert_abs_diff_eq!(Radians::new(PI).fold_to_pi().value(), PI, epsilon = 1e-12);
assert_abs_diff_eq!(Radians::new(0.0).fold_to_pi().value(), 0.0, epsilon = 1e-12);
}
#[test]
fn fold_to_pi_degree_analog() {
assert_abs_diff_eq!(
Degrees::new(190.0).fold_to_pi().value(),
170.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-30.0).fold_to_pi().value(),
30.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(360.0 * 7.0 + 50.0).fold_to_pi().value(),
50.0,
epsilon = 1e-12
);
}
#[test]
fn fold_to_pi_nan_propagates() {
assert!(Radians::new(f64::NAN).fold_to_pi().value().is_nan());
assert!(Degrees::new(f64::NAN).fold_to_pi().value().is_nan());
}
#[test]
fn fold_to_pi_matches_nsb_legacy_loop() {
fn nsb_legacy_fold(mut delta: f64) -> f64 {
while delta > PI {
delta -= TAU;
}
while delta < -PI {
delta += TAU;
}
delta.abs()
}
let samples = [
-7.5 * PI,
-PI - 1e-9,
-PI,
-PI / 2.0,
-1e-12,
0.0,
1e-12,
PI / 4.0,
PI - 1e-9,
PI,
PI + 1e-9,
3.0 * PI / 2.0,
7.5 * PI,
];
for &x in &samples {
let helper = Radians::new(x).fold_to_pi().value();
let legacy = nsb_legacy_fold(x);
assert_abs_diff_eq!(helper, legacy, epsilon = 1e-9);
}
}
#[test]
fn degrees_from_dms_positive() {
let d = Degrees::from_dms(12, 30, 0.0);
assert_abs_diff_eq!(d.value(), 12.5, epsilon = 1e-12);
}
#[test]
fn degrees_from_dms_negative() {
let d = Degrees::from_dms(-33, 52, 0.0);
assert!(d.value() < 0.0);
assert_abs_diff_eq!(d.value(), -(33.0 + 52.0 / 60.0), epsilon = 1e-12);
}
#[test]
fn degrees_from_dms_with_seconds() {
let d = Degrees::from_dms(10, 20, 30.0);
assert_abs_diff_eq!(
d.value(),
10.0 + 20.0 / 60.0 + 30.0 / 3600.0,
epsilon = 1e-12
);
}
#[test]
fn degrees_from_dms_sign() {
let pos = Degrees::from_dms_sign(1, 45, 30, 0.0);
let neg = Degrees::from_dms_sign(-1, 45, 30, 0.0);
assert_abs_diff_eq!(pos.value(), 45.5, epsilon = 1e-12);
assert_abs_diff_eq!(neg.value(), -45.5, epsilon = 1e-12);
}
#[test]
fn degrees_from_dms_sign_zero_is_positive() {
let zero_sign = Degrees::from_dms_sign(0, 45, 30, 0.0);
assert_abs_diff_eq!(zero_sign.value(), 45.5, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn hour_angles_from_hms() {
let ha = HourAngles::from_hms(5, 30, 0.0);
assert_abs_diff_eq!(ha.value(), 5.5, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn hour_angles_from_hms_negative() {
let ha = HourAngles::from_hms(-3, 15, 0.0);
assert_abs_diff_eq!(ha.value(), -3.25, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn hour_angles_to_degrees() {
let ha = HourAngles::new(6.0);
let deg = ha.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
}
#[test]
fn display_degrees() {
let d = Degrees::new(45.5);
assert_eq!(format!("{}", d), "45.5 °");
}
#[test]
fn display_radians() {
let r = Radians::new(1.0);
assert_eq!(format!("{}", r), "1 rad");
}
#[test]
fn unit_constants() {
assert_eq!(DEG.value(), 1.0);
assert_eq!(RAD.value(), 1.0);
assert_eq!(MRAD.value(), 1.0);
#[cfg(feature = "astro")]
{
assert_eq!(ARCM.value(), 1.0);
assert_eq!(ARCS.value(), 1.0);
assert_eq!(MAS.value(), 1.0);
assert_eq!(UAS.value(), 1.0);
assert_eq!(HOUR_ANGLE.value(), 1.0);
}
#[cfg(feature = "navigation")]
assert_eq!(GON.value(), 1.0);
assert_eq!(TURN.value(), 1.0);
}
#[test]
fn wrap_signed_lo_boundary_half_turn() {
assert_abs_diff_eq!(
Degrees::new(180.0).wrap_signed_lo().value(),
-180.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Degrees::new(-180.0).wrap_signed_lo().value(),
-180.0,
epsilon = 1e-12
);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_degrees_to_arcminutes() {
let deg = Degrees::new(1.0);
let arcm = deg.to::<Arcminute>();
assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_arcminutes_to_degrees() {
let arcm = Arcminutes::new(60.0);
let deg = arcm.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_arcminutes_to_arcseconds() {
let arcm = Arcminutes::new(1.0);
let arcs = arcm.to::<Arcsecond>();
assert_abs_diff_eq!(arcs.value(), 60.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_arcseconds_to_microarcseconds() {
let arcs = Arcseconds::new(1.0);
let uas = arcs.to::<MicroArcsecond>();
assert_abs_diff_eq!(uas.value(), 1_000_000.0, epsilon = 1e-6);
}
#[test]
#[cfg(feature = "astro")]
fn conversion_microarcseconds_to_degrees() {
let uas = MicroArcseconds::new(3_600_000_000.0);
let deg = uas.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-9);
}
#[test]
#[cfg(feature = "navigation")]
fn conversion_degrees_to_gradians() {
let deg = Degrees::new(90.0);
let gon = deg.to::<Gradian>();
assert_abs_diff_eq!(gon.value(), 100.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn conversion_gradians_to_degrees() {
let gon = Gradians::new(400.0);
let deg = gon.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 360.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn conversion_gradians_to_radians() {
let gon = Gradians::new(200.0);
let rad = gon.to::<Radian>();
assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
}
#[test]
fn conversion_degrees_to_turns() {
let deg = Degrees::new(360.0);
let turn = deg.to::<Turn>();
assert_abs_diff_eq!(turn.value(), 1.0, epsilon = 1e-12);
}
#[test]
fn conversion_milliradians_to_radians() {
let mrad = Milliradians::new(1_000.0);
let rad = mrad.to::<Radian>();
assert_abs_diff_eq!(rad.value(), 1.0, epsilon = 1e-12);
}
#[test]
fn conversion_turns_to_degrees() {
let turn = Turns::new(2.5);
let deg = turn.to::<Degree>();
assert_abs_diff_eq!(deg.value(), 900.0, epsilon = 1e-12);
}
#[test]
fn conversion_turns_to_radians() {
let turn = Turns::new(1.0);
let rad = turn.to::<Radian>();
assert_abs_diff_eq!(rad.value(), TAU, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn from_impl_new_units() {
let deg = Degrees::new(1.0);
let arcm: Arcminutes = deg.into();
assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn from_impl_gradian_to_deg() {
let gon = Gradians::new(100.0);
let deg: Degrees = gon.into();
assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
}
#[test]
fn from_impl_turn_to_deg() {
let turn = Turns::new(0.25);
let deg: Degrees = turn.into();
assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn roundtrip_arcminute_arcsecond() {
let original = Arcminutes::new(5.0);
let arcs = original.to::<Arcsecond>();
let back = arcs.to::<Arcminute>();
assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn roundtrip_gradian_degree() {
let original = Gradians::new(123.456);
let deg = original.to::<Degree>();
let back = deg.to::<Gradian>();
assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
}
#[test]
fn roundtrip_turn_radian() {
let original = Turns::new(2.717);
let rad = original.to::<Radian>();
let back = rad.to::<Turn>();
assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn gradian_full_turn() {
assert_abs_diff_eq!(Gradian::FULL_TURN, 400.0, epsilon = 1e-12);
}
#[test]
fn turn_full_turn() {
assert_abs_diff_eq!(Turn::FULL_TURN, 1.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "astro")]
fn arcminute_full_turn() {
assert_abs_diff_eq!(Arcminute::FULL_TURN, 21_600.0, epsilon = 1e-9);
}
#[test]
#[cfg(feature = "astro")]
fn microarcsecond_conversion_chain() {
let uas = MicroArcseconds::new(1e9);
let mas = uas.to::<MilliArcsecond>();
let arcs = mas.to::<Arcsecond>();
let arcm = arcs.to::<Arcminute>();
let deg = arcm.to::<Degree>();
assert_abs_diff_eq!(mas.value(), 1_000_000.0, epsilon = 1e-6);
assert_abs_diff_eq!(arcs.value(), 1_000.0, epsilon = 1e-9);
assert_abs_diff_eq!(arcm.value(), 1_000.0 / 60.0, epsilon = 1e-9);
assert_relative_eq!(deg.value(), 1_000.0 / 3600.0, max_relative = 1e-9);
}
#[test]
fn wrap_pos_with_turns() {
let turn = Turns::new(2.7);
let wrapped = turn.wrap_pos();
assert_abs_diff_eq!(wrapped.value(), 0.7, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn wrap_signed_with_gradians() {
let gon = Gradians::new(350.0);
let wrapped = gon.wrap_signed();
assert_abs_diff_eq!(wrapped.value(), -50.0, epsilon = 1e-12);
}
#[test]
#[cfg(feature = "navigation")]
fn trig_with_gradians() {
let gon = Gradians::new(100.0); assert_abs_diff_eq!(gon.sin(), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(gon.cos(), 0.0, epsilon = 1e-12);
}
#[test]
fn trig_with_turns() {
let turn = Turns::new(0.25); assert_abs_diff_eq!(turn.sin(), 1.0, epsilon = 1e-12);
assert_abs_diff_eq!(turn.cos(), 0.0, epsilon = 1e-12);
}
#[test]
#[cfg(all(feature = "astro", feature = "navigation"))]
fn all_units_to_degrees() {
assert_abs_diff_eq!(
Radians::new(PI).to::<Degree>().value(),
180.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Arcminutes::new(60.0).to::<Degree>().value(),
1.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Arcseconds::new(3600.0).to::<Degree>().value(),
1.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
MilliArcseconds::new(3_600_000.0).to::<Degree>().value(),
1.0,
epsilon = 1e-9
);
assert_abs_diff_eq!(
MicroArcseconds::new(3_600_000_000.0).to::<Degree>().value(),
1.0,
epsilon = 1e-6
);
assert_abs_diff_eq!(
Gradians::new(100.0).to::<Degree>().value(),
90.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
Turns::new(1.0).to::<Degree>().value(),
360.0,
epsilon = 1e-12
);
assert_abs_diff_eq!(
HourAngles::new(1.0).to::<Degree>().value(),
15.0,
epsilon = 1e-12
);
}
proptest! {
#[test]
fn prop_wrap_pos_range(angle in -1e6..1e6f64) {
let wrapped = Degrees::new(angle).wrap_pos();
prop_assert!(wrapped.value() >= 0.0);
prop_assert!(wrapped.value() < 360.0);
}
#[test]
fn prop_wrap_signed_range(angle in -1e6..1e6f64) {
let wrapped = Degrees::new(angle).wrap_signed();
prop_assert!(wrapped.value() > -180.0);
prop_assert!(wrapped.value() <= 180.0);
}
#[test]
fn prop_wrap_quarter_fold_range(angle in -1e6..1e6f64) {
let wrapped = Degrees::new(angle).wrap_quarter_fold();
prop_assert!(wrapped.value() >= -90.0);
prop_assert!(wrapped.value() <= 90.0);
}
#[test]
fn prop_pythagorean_identity(angle in -360.0..360.0f64) {
let a = Degrees::new(angle);
let sin = a.sin();
let cos = a.cos();
assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
}
#[test]
fn prop_conversion_roundtrip(angle in -1e6..1e6f64) {
let deg = Degrees::new(angle);
let rad = deg.to::<Radian>();
let back = rad.to::<Degree>();
assert_relative_eq!(back.value(), deg.value(), max_relative = 1e-12);
}
#[test]
fn prop_abs_separation_symmetric(a in -360.0..360.0f64, b in -360.0..360.0f64) {
let da = Degrees::new(a);
let db = Degrees::new(b);
assert_abs_diff_eq!(
da.abs_separation(db).value(),
db.abs_separation(da).value(),
epsilon = 1e-12
);
}
}
#[test]
fn derive_coverage_unit_structs() {
assert!(Degree == Degree);
assert!(Radian == Radian);
assert!(Milliradian == Milliradian);
#[cfg(feature = "astro")]
{
assert!(Arcminute == Arcminute);
assert!(Arcsecond == Arcsecond);
assert!(MilliArcsecond == MilliArcsecond);
assert!(MicroArcsecond == MicroArcsecond);
assert!(HourAngle == HourAngle);
}
#[cfg(feature = "navigation")]
assert!(Gradian == Gradian);
assert!(Turn == Turn);
let pos = Degrees::new(90.0);
let neg = Degrees::new(-45.0);
assert_eq!(pos.signum_const(), 1.0);
assert_eq!(neg.signum_const(), -1.0);
}
}