1use crate::scalar::Transcendental;
55use crate::{Quantity, Unit};
56use core::f64::consts::TAU;
57use qtty_derive::Unit;
58
59#[inline]
60fn rem_euclid(x: f64, modulus: f64) -> f64 {
61 #[cfg(feature = "std")]
62 {
63 x.rem_euclid(modulus)
64 }
65 #[cfg(not(feature = "std"))]
66 {
67 let r = crate::libm::fmod(x, modulus);
68 if r < 0.0 {
69 r + modulus
70 } else {
71 r
72 }
73 }
74}
75
76pub use crate::dimension::Angular;
78
79pub trait AngularUnit: Unit<Dim = Angular> {
86 const FULL_TURN: f64;
88 const HALF_TURN: f64;
90 const QUARTER_TURN: f64;
92}
93impl<T: Unit<Dim = Angular>> AngularUnit for T {
94 const FULL_TURN: f64 = Radians::new(TAU).to_const::<T>().value();
96 const HALF_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.5;
98 const QUARTER_TURN: f64 = Radians::new(TAU).to_const::<T>().value() * 0.25;
100}
101
102#[cfg(feature = "astro")]
103mod astro;
104#[cfg(feature = "astro")]
105pub use astro::*;
106#[cfg(feature = "navigation")]
107mod navigation;
108#[cfg(feature = "navigation")]
109pub use navigation::*;
110
111impl<U: AngularUnit + Copy> Quantity<U> {
112 pub const TAU: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
116 pub const FULL_TURN: Quantity<U> = Quantity::<U>::new(U::FULL_TURN);
118 pub const HALF_TURN: Quantity<U> = Quantity::<U>::new(U::HALF_TURN);
120 pub const QUARTER_TURN: Quantity<U> = Quantity::<U>::new(U::QUARTER_TURN);
122
123 #[inline]
125 pub const fn signum_const(self) -> f64 {
126 self.value().signum()
127 }
128
129 #[inline]
133 pub fn normalize(self) -> Self {
134 self.wrap_pos()
135 }
136
137 #[inline]
141 pub fn wrap_pos(self) -> Self {
142 Self::new(rem_euclid(self.value(), U::FULL_TURN))
143 }
144
145 #[inline]
151 pub fn wrap_signed(self) -> Self {
152 let full = U::FULL_TURN;
153 let half = 0.5 * full;
154 let x = self.value();
155 let y = rem_euclid(x + half, full) - half;
156 let norm = if y <= -half { y + full } else { y };
157 Self::new(norm)
158 }
159
160 #[inline]
166 pub fn wrap_signed_lo(self) -> Self {
167 let mut y = self.wrap_signed().value(); let half = 0.5 * U::FULL_TURN;
169 if y >= half {
170 y -= U::FULL_TURN;
172 }
173 Self::new(y)
174 }
175
176 #[inline]
182 pub fn wrap_quarter_fold(self) -> Self {
183 let full = U::FULL_TURN;
184 let half = 0.5 * full;
185 let quarter = 0.25 * full;
186 let y = rem_euclid(self.value() + quarter, full);
187 Self::new(quarter - (y - half).abs())
189 }
190
191 #[inline]
193 pub fn signed_separation(self, other: Self) -> Self {
194 (self - other).wrap_signed()
195 }
196
197 #[inline]
199 pub fn abs_separation(self, other: Self) -> Self {
200 let sep = self.signed_separation(other);
201 Self::new(sep.value().abs())
202 }
203
204 #[inline]
258 pub fn wrap_to_signed_pi(self) -> Self {
259 self.wrap_signed()
260 }
261
262 #[inline]
315 pub fn wrap_to_unsigned_pi(self) -> Self {
316 self.wrap_pos()
317 }
318
319 #[inline]
376 pub fn fold_to_pi(self) -> Self {
377 Self::new(self.wrap_signed().value().abs())
378 }
379}
380
381impl<U: AngularUnit + Copy, S: Transcendental> Quantity<U, S> {
386 #[inline]
391 pub fn sin(self) -> S {
392 let x_rad = self.to::<Radian>().value();
393 x_rad.sin()
394 }
395
396 #[inline]
401 pub fn cos(self) -> S {
402 let x_rad = self.to::<Radian>().value();
403 x_rad.cos()
404 }
405
406 #[inline]
411 pub fn tan(self) -> S {
412 let x_rad = self.to::<Radian>().value();
413 x_rad.tan()
414 }
415
416 #[inline]
421 pub fn sin_cos(self) -> (S, S) {
422 let x_rad = self.to::<Radian>().value();
423 x_rad.sin_cos()
424 }
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
429#[unit(symbol = "°", dimension = Angular, ratio = 1.0)]
430pub struct Degree;
431pub type Deg = Degree;
433pub type Degrees = Quantity<Deg>;
435pub const DEG: Degrees = Degrees::new(1.0);
437
438#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
440#[unit(symbol = "rad", dimension = Angular, ratio = 180.0 / core::f64::consts::PI)]
441pub struct Radian;
442pub type Rad = Radian;
444pub type Radians = Quantity<Rad>;
446pub const RAD: Radians = Radians::new(1.0);
448
449#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
451#[unit(symbol = "mrad", dimension = Angular, ratio = (180.0 / core::f64::consts::PI) / 1_000.0)]
452pub struct Milliradian;
453pub type Mrad = Milliradian;
455pub type Milliradians = Quantity<Mrad>;
457pub const MRAD: Milliradians = Milliradians::new(1.0);
459
460#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Unit)]
462#[unit(symbol = "tr", dimension = Angular, ratio = 360.0)]
463pub struct Turn;
464pub type Turns = Quantity<Turn>;
466pub const TURN: Turns = Turns::new(1.0);
468
469impl Degrees {
470 pub const fn from_dms(deg: i32, min: u32, sec: f64) -> Self {
481 let sign = if deg < 0 { -1.0 } else { 1.0 };
482 let d_abs = if deg < 0 { -(deg as f64) } else { deg as f64 };
483 let m = min as f64 / 60.0;
484 let s = sec / 3600.0;
485 let total = sign * (d_abs + m + s);
486 Self::new(total)
487 }
488
489 pub const fn from_dms_sign(sign: i8, deg: u32, min: u32, sec: f64) -> Self {
494 let s = if sign < 0 { -1.0 } else { 1.0 };
495 let total = (deg as f64) + (min as f64) / 60.0 + (sec / 3600.0);
496 Self::new(s * total)
497 }
498}
499
500#[macro_export]
506#[doc(hidden)]
507macro_rules! angular_units {
508 ($cb:path) => {
509 $cb!(Degree, Radian, Milliradian, Turn);
510 };
511}
512
513angular_units!(crate::impl_unit_from_conversions);
515
516#[cfg(feature = "cross-unit-ops")]
520angular_units!(crate::impl_unit_cross_unit_ops);
521
522#[cfg(all(feature = "astro", feature = "navigation"))]
524crate::__impl_from_each_extra_to_bases!(
525 {Arcminute, Arcsecond, MilliArcsecond, MicroArcsecond, HourAngle}
526 Gradian
527);
528#[cfg(all(feature = "astro", feature = "navigation", feature = "cross-unit-ops"))]
529crate::__impl_cross_ops_each_extra_to_bases!(
530 {Arcminute, Arcsecond, MilliArcsecond, MicroArcsecond, HourAngle}
531 Gradian
532);
533
534#[cfg(test)]
536angular_units!(crate::assert_units_are_builtin);
537
538#[cfg(all(test, feature = "std"))]
539mod tests {
540 use super::*;
541 use approx::{assert_abs_diff_eq, assert_relative_eq};
542 use core::f64::consts::{PI, TAU};
543 use proptest::prelude::*;
544
545 #[test]
550 fn test_full_turn() {
551 assert_abs_diff_eq!(Radian::FULL_TURN, TAU, epsilon = 1e-12);
552 assert_eq!(Degree::FULL_TURN, 360.0);
553 #[cfg(feature = "astro")]
554 assert_eq!(Arcsecond::FULL_TURN, 1_296_000.0);
555 }
556
557 #[test]
558 fn test_half_turn() {
559 assert_abs_diff_eq!(Radian::HALF_TURN, PI, epsilon = 1e-12);
560 assert_eq!(Degree::HALF_TURN, 180.0);
561 #[cfg(feature = "astro")]
562 assert_eq!(Arcsecond::HALF_TURN, 648_000.0);
563 }
564
565 #[test]
566 fn test_quarter_turn() {
567 assert_abs_diff_eq!(Radian::QUARTER_TURN, PI / 2.0, epsilon = 1e-12);
568 assert_eq!(Degree::QUARTER_TURN, 90.0);
569 #[cfg(feature = "astro")]
570 assert_eq!(Arcsecond::QUARTER_TURN, 324_000.0);
571 }
572
573 #[test]
574 fn test_quantity_constants() {
575 assert_eq!(Degrees::FULL_TURN.value(), 360.0);
576 assert_eq!(Degrees::HALF_TURN.value(), 180.0);
577 assert_eq!(Degrees::QUARTER_TURN.value(), 90.0);
578 assert_eq!(Degrees::TAU.value(), 360.0);
579 }
580
581 #[test]
586 fn conversion_degrees_to_radians() {
587 let deg = Degrees::new(180.0);
588 let rad = deg.to::<Radian>();
589 assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
590 }
591
592 #[test]
593 fn conversion_radians_to_degrees() {
594 let rad = Radians::new(PI);
595 let deg = rad.to::<Degree>();
596 assert_abs_diff_eq!(deg.value(), 180.0, epsilon = 1e-12);
597 }
598
599 #[test]
600 #[cfg(feature = "astro")]
601 fn conversion_degrees_to_arcseconds() {
602 let deg = Degrees::new(1.0);
603 let arcs = deg.to::<Arcsecond>();
604 assert_abs_diff_eq!(arcs.value(), 3600.0, epsilon = 1e-9);
605 }
606
607 #[test]
608 #[cfg(feature = "astro")]
609 fn conversion_arcseconds_to_degrees() {
610 let arcs = Arcseconds::new(3600.0);
611 let deg = arcs.to::<Degree>();
612 assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
613 }
614
615 #[test]
616 #[cfg(feature = "astro")]
617 fn conversion_degrees_to_milliarcseconds() {
618 let deg = Degrees::new(1.0);
619 let mas = deg.to::<MilliArcsecond>();
620 assert_abs_diff_eq!(mas.value(), 3_600_000.0, epsilon = 1e-6);
621 }
622
623 #[test]
624 #[cfg(feature = "astro")]
625 fn conversion_hour_angles_to_degrees() {
626 let ha = HourAngles::new(1.0);
627 let deg = ha.to::<Degree>();
628 assert_abs_diff_eq!(deg.value(), 15.0, epsilon = 1e-12);
629 }
630
631 #[test]
632 fn conversion_roundtrip() {
633 let original = Degrees::new(123.456);
634 let rad = original.to::<Radian>();
635 let back = rad.to::<Degree>();
636 assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
637 }
638
639 #[test]
640 fn from_impl_degrees_radians() {
641 let deg = Degrees::new(90.0);
642 let rad: Radians = deg.into();
643 assert_abs_diff_eq!(rad.value(), PI / 2.0, epsilon = 1e-12);
644
645 let rad2 = Radians::new(PI);
646 let deg2: Degrees = rad2.into();
647 assert_abs_diff_eq!(deg2.value(), 180.0, epsilon = 1e-12);
648 }
649
650 #[test]
655 fn test_trig() {
656 let a = Degrees::new(90.0);
657 assert!((a.sin() - 1.0).abs() < 1e-12);
658 assert!(a.cos().abs() < 1e-12);
659 }
660
661 #[test]
662 fn trig_sin_known_values() {
663 assert_abs_diff_eq!(Degrees::new(0.0).sin(), 0.0, epsilon = 1e-12);
664 assert_abs_diff_eq!(Degrees::new(30.0).sin(), 0.5, epsilon = 1e-12);
665 assert_abs_diff_eq!(Degrees::new(90.0).sin(), 1.0, epsilon = 1e-12);
666 assert_abs_diff_eq!(Degrees::new(180.0).sin(), 0.0, epsilon = 1e-12);
667 assert_abs_diff_eq!(Degrees::new(270.0).sin(), -1.0, epsilon = 1e-12);
668 }
669
670 #[test]
671 fn trig_cos_known_values() {
672 assert_abs_diff_eq!(Degrees::new(0.0).cos(), 1.0, epsilon = 1e-12);
673 assert_abs_diff_eq!(Degrees::new(60.0).cos(), 0.5, epsilon = 1e-12);
674 assert_abs_diff_eq!(Degrees::new(90.0).cos(), 0.0, epsilon = 1e-12);
675 assert_abs_diff_eq!(Degrees::new(180.0).cos(), -1.0, epsilon = 1e-12);
676 }
677
678 #[test]
679 fn trig_tan_known_values() {
680 assert_abs_diff_eq!(Degrees::new(0.0).tan(), 0.0, epsilon = 1e-12);
681 assert_abs_diff_eq!(Degrees::new(45.0).tan(), 1.0, epsilon = 1e-12);
682 assert_abs_diff_eq!(Degrees::new(180.0).tan(), 0.0, epsilon = 1e-12);
683 }
684
685 #[test]
686 fn trig_sin_cos_consistency() {
687 let angle = Degrees::new(37.5);
688 let (sin, cos) = angle.sin_cos();
689 assert_abs_diff_eq!(sin, angle.sin(), epsilon = 1e-15);
690 assert_abs_diff_eq!(cos, angle.cos(), epsilon = 1e-15);
691 }
692
693 #[test]
694 fn trig_pythagorean_identity() {
695 let angle = Degrees::new(123.456);
696 let sin = angle.sin();
697 let cos = angle.cos();
698 assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
699 }
700
701 #[test]
702 fn trig_radians() {
703 assert_abs_diff_eq!(Radians::new(0.0).sin(), 0.0, epsilon = 1e-12);
704 assert_abs_diff_eq!(Radians::new(PI / 2.0).sin(), 1.0, epsilon = 1e-12);
705 assert_abs_diff_eq!(Radians::new(PI).cos(), -1.0, epsilon = 1e-12);
706 }
707
708 #[test]
713 fn signum_positive() {
714 assert_eq!(Degrees::new(45.0).signum(), 1.0);
715 }
716
717 #[test]
718 fn signum_negative() {
719 assert_eq!(Degrees::new(-45.0).signum(), -1.0);
720 }
721
722 #[test]
723 fn signum_zero() {
724 assert_eq!(Degrees::new(0.0).signum(), 1.0);
725 }
726
727 #[test]
732 fn wrap_pos_basic() {
733 assert_abs_diff_eq!(
734 Degrees::new(370.0).wrap_pos().value(),
735 10.0,
736 epsilon = 1e-12
737 );
738 assert_abs_diff_eq!(Degrees::new(720.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
739 assert_abs_diff_eq!(Degrees::new(0.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
740 }
741
742 #[test]
743 fn wrap_pos_negative() {
744 assert_abs_diff_eq!(
745 Degrees::new(-10.0).wrap_pos().value(),
746 350.0,
747 epsilon = 1e-12
748 );
749 assert_abs_diff_eq!(
750 Degrees::new(-370.0).wrap_pos().value(),
751 350.0,
752 epsilon = 1e-12
753 );
754 assert_abs_diff_eq!(
755 Degrees::new(-720.0).wrap_pos().value(),
756 0.0,
757 epsilon = 1e-12
758 );
759 }
760
761 #[test]
762 fn wrap_pos_boundary() {
763 assert_abs_diff_eq!(Degrees::new(360.0).wrap_pos().value(), 0.0, epsilon = 1e-12);
764 assert_abs_diff_eq!(
765 Degrees::new(-360.0).wrap_pos().value(),
766 0.0,
767 epsilon = 1e-12
768 );
769 }
770
771 #[test]
772 fn normalize_is_wrap_pos() {
773 let angle = Degrees::new(450.0);
774 assert_eq!(angle.normalize().value(), angle.wrap_pos().value());
775 }
776
777 #[test]
782 fn test_wrap_signed() {
783 let a = Degrees::new(370.0).wrap_signed();
784 assert_eq!(a.value(), 10.0);
785 let b = Degrees::new(-190.0).wrap_signed();
786 assert_eq!(b.value(), 170.0);
787 }
788
789 #[test]
790 fn wrap_signed_basic() {
791 assert_abs_diff_eq!(
792 Degrees::new(10.0).wrap_signed().value(),
793 10.0,
794 epsilon = 1e-12
795 );
796 assert_abs_diff_eq!(
797 Degrees::new(-10.0).wrap_signed().value(),
798 -10.0,
799 epsilon = 1e-12
800 );
801 }
802
803 #[test]
804 fn wrap_signed_over_180() {
805 assert_abs_diff_eq!(
806 Degrees::new(190.0).wrap_signed().value(),
807 -170.0,
808 epsilon = 1e-12
809 );
810 assert_abs_diff_eq!(
811 Degrees::new(270.0).wrap_signed().value(),
812 -90.0,
813 epsilon = 1e-12
814 );
815 }
816
817 #[test]
818 fn wrap_signed_boundary_180() {
819 assert_abs_diff_eq!(
820 Degrees::new(180.0).wrap_signed().value(),
821 180.0,
822 epsilon = 1e-12
823 );
824 assert_abs_diff_eq!(
825 Degrees::new(-180.0).wrap_signed().value(),
826 180.0,
827 epsilon = 1e-12
828 );
829 }
830
831 #[test]
832 fn wrap_signed_large_values() {
833 assert_abs_diff_eq!(
834 Degrees::new(540.0).wrap_signed().value(),
835 180.0,
836 epsilon = 1e-12
837 );
838 assert_abs_diff_eq!(
839 Degrees::new(-540.0).wrap_signed().value(),
840 180.0,
841 epsilon = 1e-12
842 );
843 }
844
845 #[test]
850 fn wrap_quarter_fold_basic() {
851 assert_abs_diff_eq!(
852 Degrees::new(0.0).wrap_quarter_fold().value(),
853 0.0,
854 epsilon = 1e-12
855 );
856 assert_abs_diff_eq!(
857 Degrees::new(45.0).wrap_quarter_fold().value(),
858 45.0,
859 epsilon = 1e-12
860 );
861 assert_abs_diff_eq!(
862 Degrees::new(-45.0).wrap_quarter_fold().value(),
863 -45.0,
864 epsilon = 1e-12
865 );
866 }
867
868 #[test]
869 fn wrap_quarter_fold_boundary() {
870 assert_abs_diff_eq!(
871 Degrees::new(90.0).wrap_quarter_fold().value(),
872 90.0,
873 epsilon = 1e-12
874 );
875 assert_abs_diff_eq!(
876 Degrees::new(-90.0).wrap_quarter_fold().value(),
877 -90.0,
878 epsilon = 1e-12
879 );
880 }
881
882 #[test]
883 fn wrap_quarter_fold_over_90() {
884 assert_abs_diff_eq!(
885 Degrees::new(100.0).wrap_quarter_fold().value(),
886 80.0,
887 epsilon = 1e-12
888 );
889 assert_abs_diff_eq!(
890 Degrees::new(135.0).wrap_quarter_fold().value(),
891 45.0,
892 epsilon = 1e-12
893 );
894 assert_abs_diff_eq!(
895 Degrees::new(180.0).wrap_quarter_fold().value(),
896 0.0,
897 epsilon = 1e-12
898 );
899 }
900
901 #[test]
906 fn signed_separation_basic() {
907 let a = Degrees::new(30.0);
908 let b = Degrees::new(50.0);
909 assert_abs_diff_eq!(a.signed_separation(b).value(), -20.0, epsilon = 1e-12);
910 assert_abs_diff_eq!(b.signed_separation(a).value(), 20.0, epsilon = 1e-12);
911 }
912
913 #[test]
914 fn signed_separation_wrap() {
915 let a = Degrees::new(10.0);
916 let b = Degrees::new(350.0);
917 assert_abs_diff_eq!(a.signed_separation(b).value(), 20.0, epsilon = 1e-12);
918 assert_abs_diff_eq!(b.signed_separation(a).value(), -20.0, epsilon = 1e-12);
919 }
920
921 #[test]
922 fn abs_separation() {
923 let a = Degrees::new(30.0);
924 let b = Degrees::new(50.0);
925 assert_abs_diff_eq!(a.abs_separation(b).value(), 20.0, epsilon = 1e-12);
926 assert_abs_diff_eq!(b.abs_separation(a).value(), 20.0, epsilon = 1e-12);
927 }
928
929 #[test]
934 fn wrap_to_signed_pi_matches_wrap_signed_radians() {
935 for &x in &[
936 -7.0 * PI,
937 -PI - 0.1,
938 -PI,
939 -1.0,
940 0.0,
941 1.0,
942 PI,
943 PI + 0.1,
944 7.0 * PI,
945 ] {
946 let a = Radians::new(x);
947 assert_abs_diff_eq!(
948 a.wrap_to_signed_pi().value(),
949 a.wrap_signed().value(),
950 epsilon = 1e-12
951 );
952 }
953 }
954
955 #[test]
956 fn wrap_to_signed_pi_degree_analog() {
957 assert_abs_diff_eq!(
959 Degrees::new(190.0).wrap_to_signed_pi().value(),
960 -170.0,
961 epsilon = 1e-12
962 );
963 assert_abs_diff_eq!(
964 Degrees::new(180.0).wrap_to_signed_pi().value(),
965 180.0,
966 epsilon = 1e-12
967 );
968 assert_abs_diff_eq!(
969 Degrees::new(-180.0).wrap_to_signed_pi().value(),
970 180.0,
971 epsilon = 1e-12
972 );
973 assert_abs_diff_eq!(
974 Degrees::new(7.0 * 180.0).wrap_to_signed_pi().value(),
975 180.0,
976 epsilon = 1e-12
977 );
978 }
979
980 #[test]
981 fn wrap_to_unsigned_pi_matches_wrap_pos_radians() {
982 for &x in &[-7.0 * PI, -PI, -1.0, 0.0, 1.0, PI, TAU, 7.0 * PI] {
983 let a = Radians::new(x);
984 assert_abs_diff_eq!(
985 a.wrap_to_unsigned_pi().value(),
986 a.wrap_pos().value(),
987 epsilon = 1e-12
988 );
989 }
990 }
991
992 #[test]
993 fn wrap_to_unsigned_pi_degree_analog() {
994 assert_abs_diff_eq!(
995 Degrees::new(370.0).wrap_to_unsigned_pi().value(),
996 10.0,
997 epsilon = 1e-12
998 );
999 assert_abs_diff_eq!(
1000 Degrees::new(-10.0).wrap_to_unsigned_pi().value(),
1001 350.0,
1002 epsilon = 1e-12
1003 );
1004 assert_abs_diff_eq!(
1005 Degrees::new(360.0).wrap_to_unsigned_pi().value(),
1006 0.0,
1007 epsilon = 1e-12
1008 );
1009 }
1010
1011 #[test]
1012 fn fold_to_pi_radians() {
1013 assert_abs_diff_eq!(Radians::new(0.5).fold_to_pi().value(), 0.5, epsilon = 1e-12);
1014 assert_abs_diff_eq!(
1015 Radians::new(-0.5).fold_to_pi().value(),
1016 0.5,
1017 epsilon = 1e-12
1018 );
1019 assert_abs_diff_eq!(
1020 Radians::new(PI + 0.1).fold_to_pi().value(),
1021 PI - 0.1,
1022 epsilon = 1e-12
1023 );
1024 assert_abs_diff_eq!(
1025 Radians::new(7.0 * PI).fold_to_pi().value(),
1026 PI,
1027 epsilon = 1e-12
1028 );
1029 assert_abs_diff_eq!(Radians::new(PI).fold_to_pi().value(), PI, epsilon = 1e-12);
1030 assert_abs_diff_eq!(Radians::new(0.0).fold_to_pi().value(), 0.0, epsilon = 1e-12);
1031 }
1032
1033 #[test]
1034 fn fold_to_pi_degree_analog() {
1035 assert_abs_diff_eq!(
1036 Degrees::new(190.0).fold_to_pi().value(),
1037 170.0,
1038 epsilon = 1e-12
1039 );
1040 assert_abs_diff_eq!(
1041 Degrees::new(-30.0).fold_to_pi().value(),
1042 30.0,
1043 epsilon = 1e-12
1044 );
1045 assert_abs_diff_eq!(
1046 Degrees::new(360.0 * 7.0 + 50.0).fold_to_pi().value(),
1047 50.0,
1048 epsilon = 1e-12
1049 );
1050 }
1051
1052 #[test]
1053 fn fold_to_pi_nan_propagates() {
1054 assert!(Radians::new(f64::NAN).fold_to_pi().value().is_nan());
1055 assert!(Degrees::new(f64::NAN).fold_to_pi().value().is_nan());
1056 }
1057
1058 #[test]
1062 fn fold_to_pi_matches_nsb_legacy_loop() {
1063 fn nsb_legacy_fold(mut delta: f64) -> f64 {
1066 while delta > PI {
1067 delta -= TAU;
1068 }
1069 while delta < -PI {
1070 delta += TAU;
1071 }
1072 delta.abs()
1073 }
1074
1075 let samples = [
1076 -7.5 * PI,
1077 -PI - 1e-9,
1078 -PI,
1079 -PI / 2.0,
1080 -1e-12,
1081 0.0,
1082 1e-12,
1083 PI / 4.0,
1084 PI - 1e-9,
1085 PI,
1086 PI + 1e-9,
1087 3.0 * PI / 2.0,
1088 7.5 * PI,
1089 ];
1090 for &x in &samples {
1091 let helper = Radians::new(x).fold_to_pi().value();
1092 let legacy = nsb_legacy_fold(x);
1093 assert_abs_diff_eq!(helper, legacy, epsilon = 1e-9);
1094 }
1095 }
1096
1097 #[test]
1102 fn degrees_from_dms_positive() {
1103 let d = Degrees::from_dms(12, 30, 0.0);
1104 assert_abs_diff_eq!(d.value(), 12.5, epsilon = 1e-12);
1105 }
1106
1107 #[test]
1108 fn degrees_from_dms_negative() {
1109 let d = Degrees::from_dms(-33, 52, 0.0);
1110 assert!(d.value() < 0.0);
1111 assert_abs_diff_eq!(d.value(), -(33.0 + 52.0 / 60.0), epsilon = 1e-12);
1112 }
1113
1114 #[test]
1115 fn degrees_from_dms_with_seconds() {
1116 let d = Degrees::from_dms(10, 20, 30.0);
1117 assert_abs_diff_eq!(
1118 d.value(),
1119 10.0 + 20.0 / 60.0 + 30.0 / 3600.0,
1120 epsilon = 1e-12
1121 );
1122 }
1123
1124 #[test]
1125 fn degrees_from_dms_sign() {
1126 let pos = Degrees::from_dms_sign(1, 45, 30, 0.0);
1127 let neg = Degrees::from_dms_sign(-1, 45, 30, 0.0);
1128 assert_abs_diff_eq!(pos.value(), 45.5, epsilon = 1e-12);
1129 assert_abs_diff_eq!(neg.value(), -45.5, epsilon = 1e-12);
1130 }
1131
1132 #[test]
1133 fn degrees_from_dms_sign_zero_is_positive() {
1134 let zero_sign = Degrees::from_dms_sign(0, 45, 30, 0.0);
1135 assert_abs_diff_eq!(zero_sign.value(), 45.5, epsilon = 1e-12);
1136 }
1137
1138 #[test]
1139 #[cfg(feature = "astro")]
1140 fn hour_angles_from_hms() {
1141 let ha = HourAngles::from_hms(5, 30, 0.0);
1142 assert_abs_diff_eq!(ha.value(), 5.5, epsilon = 1e-12);
1143 }
1144
1145 #[test]
1146 #[cfg(feature = "astro")]
1147 fn hour_angles_from_hms_negative() {
1148 let ha = HourAngles::from_hms(-3, 15, 0.0);
1149 assert_abs_diff_eq!(ha.value(), -3.25, epsilon = 1e-12);
1150 }
1151
1152 #[test]
1153 #[cfg(feature = "astro")]
1154 fn hour_angles_to_degrees() {
1155 let ha = HourAngles::new(6.0);
1156 let deg = ha.to::<Degree>();
1157 assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
1158 }
1159
1160 #[test]
1165 fn display_degrees() {
1166 let d = Degrees::new(45.5);
1167 assert_eq!(format!("{}", d), "45.5 °");
1168 }
1169
1170 #[test]
1171 fn display_radians() {
1172 let r = Radians::new(1.0);
1173 assert_eq!(format!("{}", r), "1 rad");
1174 }
1175
1176 #[test]
1181 fn unit_constants() {
1182 assert_eq!(DEG.value(), 1.0);
1183 assert_eq!(RAD.value(), 1.0);
1184 assert_eq!(MRAD.value(), 1.0);
1185 #[cfg(feature = "astro")]
1186 {
1187 assert_eq!(ARCM.value(), 1.0);
1188 assert_eq!(ARCS.value(), 1.0);
1189 assert_eq!(MAS.value(), 1.0);
1190 assert_eq!(UAS.value(), 1.0);
1191 assert_eq!(HOUR_ANGLE.value(), 1.0);
1192 }
1193 #[cfg(feature = "navigation")]
1194 assert_eq!(GON.value(), 1.0);
1195 assert_eq!(TURN.value(), 1.0);
1196 }
1197
1198 #[test]
1203 fn wrap_signed_lo_boundary_half_turn() {
1204 assert_abs_diff_eq!(
1206 Degrees::new(180.0).wrap_signed_lo().value(),
1207 -180.0,
1208 epsilon = 1e-12
1209 );
1210 assert_abs_diff_eq!(
1211 Degrees::new(-180.0).wrap_signed_lo().value(),
1212 -180.0,
1213 epsilon = 1e-12
1214 );
1215 }
1216
1217 #[test]
1222 #[cfg(feature = "astro")]
1223 fn conversion_degrees_to_arcminutes() {
1224 let deg = Degrees::new(1.0);
1225 let arcm = deg.to::<Arcminute>();
1226 assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
1227 }
1228
1229 #[test]
1230 #[cfg(feature = "astro")]
1231 fn conversion_arcminutes_to_degrees() {
1232 let arcm = Arcminutes::new(60.0);
1233 let deg = arcm.to::<Degree>();
1234 assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-12);
1235 }
1236
1237 #[test]
1238 #[cfg(feature = "astro")]
1239 fn conversion_arcminutes_to_arcseconds() {
1240 let arcm = Arcminutes::new(1.0);
1241 let arcs = arcm.to::<Arcsecond>();
1242 assert_abs_diff_eq!(arcs.value(), 60.0, epsilon = 1e-12);
1243 }
1244
1245 #[test]
1246 #[cfg(feature = "astro")]
1247 fn conversion_arcseconds_to_microarcseconds() {
1248 let arcs = Arcseconds::new(1.0);
1249 let uas = arcs.to::<MicroArcsecond>();
1250 assert_abs_diff_eq!(uas.value(), 1_000_000.0, epsilon = 1e-6);
1251 }
1252
1253 #[test]
1254 #[cfg(feature = "astro")]
1255 fn conversion_microarcseconds_to_degrees() {
1256 let uas = MicroArcseconds::new(3_600_000_000.0);
1257 let deg = uas.to::<Degree>();
1258 assert_abs_diff_eq!(deg.value(), 1.0, epsilon = 1e-9);
1259 }
1260
1261 #[test]
1262 #[cfg(feature = "navigation")]
1263 fn conversion_degrees_to_gradians() {
1264 let deg = Degrees::new(90.0);
1265 let gon = deg.to::<Gradian>();
1266 assert_abs_diff_eq!(gon.value(), 100.0, epsilon = 1e-12);
1267 }
1268
1269 #[test]
1270 #[cfg(feature = "navigation")]
1271 fn conversion_gradians_to_degrees() {
1272 let gon = Gradians::new(400.0);
1273 let deg = gon.to::<Degree>();
1274 assert_abs_diff_eq!(deg.value(), 360.0, epsilon = 1e-12);
1275 }
1276
1277 #[test]
1278 #[cfg(feature = "navigation")]
1279 fn conversion_gradians_to_radians() {
1280 let gon = Gradians::new(200.0);
1281 let rad = gon.to::<Radian>();
1282 assert_abs_diff_eq!(rad.value(), PI, epsilon = 1e-12);
1283 }
1284
1285 #[test]
1286 fn conversion_degrees_to_turns() {
1287 let deg = Degrees::new(360.0);
1288 let turn = deg.to::<Turn>();
1289 assert_abs_diff_eq!(turn.value(), 1.0, epsilon = 1e-12);
1290 }
1291
1292 #[test]
1293 fn conversion_milliradians_to_radians() {
1294 let mrad = Milliradians::new(1_000.0);
1295 let rad = mrad.to::<Radian>();
1296 assert_abs_diff_eq!(rad.value(), 1.0, epsilon = 1e-12);
1297 }
1298
1299 #[test]
1300 fn conversion_turns_to_degrees() {
1301 let turn = Turns::new(2.5);
1302 let deg = turn.to::<Degree>();
1303 assert_abs_diff_eq!(deg.value(), 900.0, epsilon = 1e-12);
1304 }
1305
1306 #[test]
1307 fn conversion_turns_to_radians() {
1308 let turn = Turns::new(1.0);
1309 let rad = turn.to::<Radian>();
1310 assert_abs_diff_eq!(rad.value(), TAU, epsilon = 1e-12);
1311 }
1312
1313 #[test]
1314 #[cfg(feature = "astro")]
1315 fn from_impl_new_units() {
1316 let deg = Degrees::new(1.0);
1318 let arcm: Arcminutes = deg.into();
1319 assert_abs_diff_eq!(arcm.value(), 60.0, epsilon = 1e-12);
1320 }
1321
1322 #[test]
1323 #[cfg(feature = "navigation")]
1324 fn from_impl_gradian_to_deg() {
1325 let gon = Gradians::new(100.0);
1326 let deg: Degrees = gon.into();
1327 assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
1328 }
1329
1330 #[test]
1331 fn from_impl_turn_to_deg() {
1332 let turn = Turns::new(0.25);
1333 let deg: Degrees = turn.into();
1334 assert_abs_diff_eq!(deg.value(), 90.0, epsilon = 1e-12);
1335 }
1336
1337 #[test]
1338 #[cfg(feature = "astro")]
1339 fn roundtrip_arcminute_arcsecond() {
1340 let original = Arcminutes::new(5.0);
1341 let arcs = original.to::<Arcsecond>();
1342 let back = arcs.to::<Arcminute>();
1343 assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1344 }
1345
1346 #[test]
1347 #[cfg(feature = "navigation")]
1348 fn roundtrip_gradian_degree() {
1349 let original = Gradians::new(123.456);
1350 let deg = original.to::<Degree>();
1351 let back = deg.to::<Gradian>();
1352 assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1353 }
1354
1355 #[test]
1356 fn roundtrip_turn_radian() {
1357 let original = Turns::new(2.717);
1358 let rad = original.to::<Radian>();
1359 let back = rad.to::<Turn>();
1360 assert_abs_diff_eq!(back.value(), original.value(), epsilon = 1e-12);
1361 }
1362
1363 #[test]
1364 #[cfg(feature = "navigation")]
1365 fn gradian_full_turn() {
1366 assert_abs_diff_eq!(Gradian::FULL_TURN, 400.0, epsilon = 1e-12);
1367 }
1368
1369 #[test]
1370 fn turn_full_turn() {
1371 assert_abs_diff_eq!(Turn::FULL_TURN, 1.0, epsilon = 1e-12);
1372 }
1373
1374 #[test]
1375 #[cfg(feature = "astro")]
1376 fn arcminute_full_turn() {
1377 assert_abs_diff_eq!(Arcminute::FULL_TURN, 21_600.0, epsilon = 1e-9);
1378 }
1379
1380 #[test]
1381 #[cfg(feature = "astro")]
1382 fn microarcsecond_conversion_chain() {
1383 let uas = MicroArcseconds::new(1e9);
1385 let mas = uas.to::<MilliArcsecond>();
1386 let arcs = mas.to::<Arcsecond>();
1387 let arcm = arcs.to::<Arcminute>();
1388 let deg = arcm.to::<Degree>();
1389
1390 assert_abs_diff_eq!(mas.value(), 1_000_000.0, epsilon = 1e-6);
1391 assert_abs_diff_eq!(arcs.value(), 1_000.0, epsilon = 1e-9);
1392 assert_abs_diff_eq!(arcm.value(), 1_000.0 / 60.0, epsilon = 1e-9);
1393 assert_relative_eq!(deg.value(), 1_000.0 / 3600.0, max_relative = 1e-9);
1394 }
1395
1396 #[test]
1397 fn wrap_pos_with_turns() {
1398 let turn = Turns::new(2.7);
1399 let wrapped = turn.wrap_pos();
1400 assert_abs_diff_eq!(wrapped.value(), 0.7, epsilon = 1e-12);
1401 }
1402
1403 #[test]
1404 #[cfg(feature = "navigation")]
1405 fn wrap_signed_with_gradians() {
1406 let gon = Gradians::new(350.0);
1407 let wrapped = gon.wrap_signed();
1408 assert_abs_diff_eq!(wrapped.value(), -50.0, epsilon = 1e-12);
1409 }
1410
1411 #[test]
1412 #[cfg(feature = "navigation")]
1413 fn trig_with_gradians() {
1414 let gon = Gradians::new(100.0); assert_abs_diff_eq!(gon.sin(), 1.0, epsilon = 1e-12);
1416 assert_abs_diff_eq!(gon.cos(), 0.0, epsilon = 1e-12);
1417 }
1418
1419 #[test]
1420 fn trig_with_turns() {
1421 let turn = Turns::new(0.25); assert_abs_diff_eq!(turn.sin(), 1.0, epsilon = 1e-12);
1423 assert_abs_diff_eq!(turn.cos(), 0.0, epsilon = 1e-12);
1424 }
1425
1426 #[test]
1427 #[cfg(all(feature = "astro", feature = "navigation"))]
1428 fn all_units_to_degrees() {
1429 assert_abs_diff_eq!(
1431 Radians::new(PI).to::<Degree>().value(),
1432 180.0,
1433 epsilon = 1e-12
1434 );
1435 assert_abs_diff_eq!(
1436 Arcminutes::new(60.0).to::<Degree>().value(),
1437 1.0,
1438 epsilon = 1e-12
1439 );
1440 assert_abs_diff_eq!(
1441 Arcseconds::new(3600.0).to::<Degree>().value(),
1442 1.0,
1443 epsilon = 1e-12
1444 );
1445 assert_abs_diff_eq!(
1446 MilliArcseconds::new(3_600_000.0).to::<Degree>().value(),
1447 1.0,
1448 epsilon = 1e-9
1449 );
1450 assert_abs_diff_eq!(
1451 MicroArcseconds::new(3_600_000_000.0).to::<Degree>().value(),
1452 1.0,
1453 epsilon = 1e-6
1454 );
1455 assert_abs_diff_eq!(
1456 Gradians::new(100.0).to::<Degree>().value(),
1457 90.0,
1458 epsilon = 1e-12
1459 );
1460 assert_abs_diff_eq!(
1461 Turns::new(1.0).to::<Degree>().value(),
1462 360.0,
1463 epsilon = 1e-12
1464 );
1465 assert_abs_diff_eq!(
1466 HourAngles::new(1.0).to::<Degree>().value(),
1467 15.0,
1468 epsilon = 1e-12
1469 );
1470 }
1471
1472 proptest! {
1477 #[test]
1478 fn prop_wrap_pos_range(angle in -1e6..1e6f64) {
1479 let wrapped = Degrees::new(angle).wrap_pos();
1480 prop_assert!(wrapped.value() >= 0.0);
1481 prop_assert!(wrapped.value() < 360.0);
1482 }
1483
1484 #[test]
1485 fn prop_wrap_signed_range(angle in -1e6..1e6f64) {
1486 let wrapped = Degrees::new(angle).wrap_signed();
1487 prop_assert!(wrapped.value() > -180.0);
1488 prop_assert!(wrapped.value() <= 180.0);
1489 }
1490
1491 #[test]
1492 fn prop_wrap_quarter_fold_range(angle in -1e6..1e6f64) {
1493 let wrapped = Degrees::new(angle).wrap_quarter_fold();
1494 prop_assert!(wrapped.value() >= -90.0);
1495 prop_assert!(wrapped.value() <= 90.0);
1496 }
1497
1498 #[test]
1499 fn prop_pythagorean_identity(angle in -360.0..360.0f64) {
1500 let a = Degrees::new(angle);
1501 let sin = a.sin();
1502 let cos = a.cos();
1503 assert_abs_diff_eq!(sin * sin + cos * cos, 1.0, epsilon = 1e-12);
1504 }
1505
1506 #[test]
1507 fn prop_conversion_roundtrip(angle in -1e6..1e6f64) {
1508 let deg = Degrees::new(angle);
1509 let rad = deg.to::<Radian>();
1510 let back = rad.to::<Degree>();
1511 assert_relative_eq!(back.value(), deg.value(), max_relative = 1e-12);
1512 }
1513
1514 #[test]
1515 fn prop_abs_separation_symmetric(a in -360.0..360.0f64, b in -360.0..360.0f64) {
1516 let da = Degrees::new(a);
1517 let db = Degrees::new(b);
1518 assert_abs_diff_eq!(
1519 da.abs_separation(db).value(),
1520 db.abs_separation(da).value(),
1521 epsilon = 1e-12
1522 );
1523 }
1524 }
1525
1526 #[test]
1529 fn derive_coverage_unit_structs() {
1530 assert!(Degree == Degree);
1533 assert!(Radian == Radian);
1534 assert!(Milliradian == Milliradian);
1535 #[cfg(feature = "astro")]
1536 {
1537 assert!(Arcminute == Arcminute);
1538 assert!(Arcsecond == Arcsecond);
1539 assert!(MilliArcsecond == MilliArcsecond);
1540 assert!(MicroArcsecond == MicroArcsecond);
1541 assert!(HourAngle == HourAngle);
1542 }
1543 #[cfg(feature = "navigation")]
1544 assert!(Gradian == Gradian);
1545 assert!(Turn == Turn);
1546 let pos = Degrees::new(90.0);
1548 let neg = Degrees::new(-45.0);
1549 assert_eq!(pos.signum_const(), 1.0);
1550 assert_eq!(neg.signum_const(), -1.0);
1551 }
1552}