1#![allow(clippy::float_cmp, clippy::suboptimal_flops)]
62
63use crate::vector2d;
64use crate::{Degrees, Radians, Validate, two_sum};
65use core::{cmp::Ordering, ops::Neg};
66use num_traits::{Float, float::FloatConst};
67
68pub const SQ_EPSILON: f64 = f64::EPSILON * f64::EPSILON;
70
71#[allow(clippy::excessive_precision, clippy::unreadable_literal)]
74pub const SQRT_3: f64 = 1.732050807568877293527446341505872367_f64;
75
76pub const COS_30_DEGREES: f64 = SQRT_3 / 2.0;
78pub const MAX_LINEAR_SIN_ANGLE: f64 = 9.67e7 * f64::EPSILON;
80pub const MAX_COS_ANGLE_IS_ONE: f64 = 3.35e7 * f64::EPSILON;
82
83pub const THIRTY: f64 = 30.0;
84pub const FORTY_FIVE: f64 = 45.0;
85
86#[must_use]
90fn to_radians<T: Float + FloatConst>(angle: Degrees<T>) -> Radians<T> {
91 let thirty = T::from(THIRTY).expect("Could not convert constant to Float");
92 if angle.0.abs() == thirty {
93 Radians(T::FRAC_PI_6().copysign(angle.0))
94 } else {
95 Radians(angle.0.to_radians())
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)]
102#[repr(transparent)]
103pub struct UnitNegRange<T: Float>(pub T);
104
105impl<T: Float> Default for UnitNegRange<T> {
106 fn default() -> Self {
107 Self(T::zero())
108 }
109}
110
111impl<T: Float> UnitNegRange<T> {
112 #[must_use]
125 pub fn clamp(value: T) -> Self {
126 Self(value.clamp(-T::one(), T::one()))
127 }
128
129 #[must_use]
131 pub fn abs(self) -> Self {
132 Self(self.0.abs())
133 }
134}
135
136impl<T: Float> Validate for UnitNegRange<T> {
137 fn is_valid(&self) -> bool {
151 (-T::one()..=T::one()).contains(&self.0)
152 }
153}
154
155impl<T: Float> Neg for UnitNegRange<T> {
156 type Output = Self;
157
158 fn neg(self) -> Self {
162 Self(T::zero() - self.0)
163 }
164}
165
166#[must_use]
173pub fn sq_a_minus_sq_b<T: Float>(a: UnitNegRange<T>, b: UnitNegRange<T>) -> UnitNegRange<T> {
174 UnitNegRange::<T>((a.0 - b.0) * (a.0 + b.0))
175}
176
177#[must_use]
184pub fn one_minus_sq_value<T: Float>(a: UnitNegRange<T>) -> UnitNegRange<T> {
185 sq_a_minus_sq_b(UnitNegRange(T::one()), a)
186}
187
188#[must_use]
203pub fn swap_sin_cos<T: Float>(a: UnitNegRange<T>) -> UnitNegRange<T> {
204 UnitNegRange(one_minus_sq_value(a).0.sqrt())
205}
206
207#[allow(clippy::missing_panics_doc)]
221#[must_use]
222pub fn cosine_from_sine<T: Float>(a: UnitNegRange<T>, sign: T) -> UnitNegRange<T> {
223 let max_cos_angle_is_one =
224 T::from(MAX_COS_ANGLE_IS_ONE).expect("Could not convert constant to Float");
225 if a.0.abs() > max_cos_angle_is_one {
226 let b = swap_sin_cos(a);
227 if b.0 > T::zero() {
228 UnitNegRange(b.0.copysign(sign))
229 } else {
230 b
231 }
232 } else {
233 UnitNegRange(T::one().copysign(sign))
234 }
235}
236
237#[allow(clippy::missing_panics_doc)]
241#[must_use]
242pub fn sine<T: Float + FloatConst>(angle: Radians<T>) -> UnitNegRange<T> {
243 let max_linear_sin_angle =
244 T::from(MAX_LINEAR_SIN_ANGLE).expect("Could not convert constant to Float");
245 let angle_abs = angle.0.abs();
246 if angle_abs == T::FRAC_PI_4() {
247 UnitNegRange(T::FRAC_1_SQRT_2().copysign(angle.0))
248 } else if angle_abs > max_linear_sin_angle {
249 UnitNegRange(angle.0.sin())
250 } else {
251 UnitNegRange(angle.0)
252 }
253}
254
255#[must_use]
259pub fn cosine<T: Float + FloatConst>(angle: Radians<T>, sin: UnitNegRange<T>) -> UnitNegRange<T> {
260 let angle_abs = angle.0.abs();
261 if angle_abs == T::FRAC_PI_4() {
262 UnitNegRange(T::FRAC_1_SQRT_2().copysign(T::FRAC_PI_2() - angle_abs))
263 } else {
264 cosine_from_sine(sin, T::FRAC_PI_2() - angle_abs)
265 }
266}
267
268#[must_use]
275fn assign_sin_cos_to_quadrant<T: Float>(
276 sin: UnitNegRange<T>,
277 cos: UnitNegRange<T>,
278 q: i32,
279) -> (UnitNegRange<T>, UnitNegRange<T>) {
280 match q & 3 {
281 1 => (cos, -sin), 2 => (-sin, -cos), 3 => (-cos, sin), _ => (sin, cos),
285 }
286}
287
288#[must_use]
301pub fn sincos<T>(radians: Radians<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
302where
303 T: Float + FloatConst,
304 f64: From<T>,
305{
306 let radians = f64::from(radians.0);
307 let rq = libm::remquo(radians, core::f64::consts::FRAC_PI_2);
308
309 let radians_q = T::from(rq.0).expect("Could not convert value to Float");
311 let radians_q = Radians(radians_q);
312 let sin = sine(radians_q);
313 assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
314}
315
316#[must_use]
330pub fn sincos_diff<T>(a: Radians<T>, b: Radians<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
331where
332 T: Float + FloatConst,
333 f64: From<T>,
334{
335 let delta = two_sum(a.0, -b.0);
336 let radians = f64::from(delta.0);
337 let rq = libm::remquo(radians, core::f64::consts::FRAC_PI_2);
338
339 let radians_q = T::from(rq.0).expect("Could not convert value to Float");
341 let radians_q = Radians(radians_q + delta.1);
342 let sin = sine(radians_q);
343 assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
344}
345
346#[must_use]
360pub fn arctan2<T: Float + FloatConst>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Radians<T> {
361 let sin_abs = sin.0.abs();
362 let cos_abs = cos.0.abs();
363
364 let radians_pi_2 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
366 Ordering::Equal => T::FRAC_PI_4(),
367 Ordering::Less => sin_abs.atan2(cos_abs),
368 Ordering::Greater => T::FRAC_PI_2() - cos_abs.atan2(sin_abs),
369 };
370
371 let radians_pi = if cos.0 < T::zero() {
373 T::PI() - radians_pi_2
374 } else {
375 radians_pi_2
376 };
377
378 Radians(radians_pi.copysign(sin.0))
380}
381
382#[must_use]
395pub fn sincosd<T>(degrees: Degrees<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
396where
397 T: Float + FloatConst,
398 f64: From<T>,
399{
400 let rq: (f64, i32) = libm::remquo(f64::from(degrees.0), 90.0);
401
402 let radians_q = T::from(rq.0).expect("Could not convert value to Float");
404 let radians_q = to_radians(Degrees(radians_q));
405 let sin = sine(radians_q);
406 assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
407}
408
409#[must_use]
423pub fn sincosd_diff<T>(a: Degrees<T>, b: Degrees<T>) -> (UnitNegRange<T>, UnitNegRange<T>)
424where
425 T: Float + FloatConst,
426 f64: From<T>,
427{
428 let delta = two_sum(a.0, -b.0);
429 let rq: (f64, i32) = libm::remquo(f64::from(delta.0), 90.0);
430
431 let radians_q = T::from(rq.0).expect("Could not convert value to Float");
433 let radians_q = to_radians(Degrees(radians_q + delta.1));
434 let sin = sine(radians_q);
435 assign_sin_cos_to_quadrant(sin, cosine(radians_q, sin), rq.1)
436}
437
438#[must_use]
442fn arctan2_degrees<T: Float + FloatConst>(sin_abs: T, cos_abs: T) -> T {
443 let half = T::one() / (T::one() + T::one());
444 let thirty = T::from(THIRTY).expect("Could not convert constant to Float");
445 if sin_abs == half {
446 thirty
447 } else {
448 sin_abs.atan2(cos_abs).to_degrees()
449 }
450}
451
452#[must_use]
461pub fn arctan2d<T: Float + FloatConst>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Degrees<T> {
462 let forty_five = T::from(FORTY_FIVE).expect("Could not convert constant to Float");
463 let ninety = forty_five + forty_five;
464 let one_eighty = ninety + ninety;
465
466 let sin_abs = sin.0.abs();
467 let cos_abs = cos.0.abs();
468
469 let degrees_90 = match sin_abs.partial_cmp(&cos_abs).expect("sin or cos is NaN") {
471 Ordering::Equal => forty_five,
472 Ordering::Less => arctan2_degrees(sin_abs, cos_abs),
473 Ordering::Greater => ninety - arctan2_degrees(cos_abs, sin_abs),
474 };
475
476 let degrees_180 = if cos.0 < T::zero() {
478 one_eighty - degrees_90
479 } else {
480 degrees_90
481 };
482
483 Degrees(degrees_180.copysign(sin.0))
485}
486
487#[must_use]
493pub fn csc<T: Float>(sin: UnitNegRange<T>) -> Option<T> {
494 let sq_epsilon = T::epsilon() * T::epsilon();
495 if sin.0.abs() >= sq_epsilon {
496 Some(T::one() / sin.0)
497 } else {
498 None
499 }
500}
501
502#[must_use]
508pub fn sec<T: Float>(cos: UnitNegRange<T>) -> Option<T> {
509 let sq_epsilon = T::epsilon() * T::epsilon();
510 if cos.0.abs() >= sq_epsilon {
511 Some(T::one() / cos.0)
512 } else {
513 None
514 }
515}
516
517#[must_use]
523pub fn tan<T: Float>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Option<T> {
524 sec(cos).map(|secant| sin.0 * secant)
525}
526
527#[must_use]
533pub fn cot<T: Float>(sin: UnitNegRange<T>, cos: UnitNegRange<T>) -> Option<T> {
534 csc(sin).map(|cosecant| cos.0 * cosecant)
535}
536
537#[must_use]
546pub fn sine_diff<T: Float>(
547 sin_a: UnitNegRange<T>,
548 cos_a: UnitNegRange<T>,
549 sin_b: UnitNegRange<T>,
550 cos_b: UnitNegRange<T>,
551) -> UnitNegRange<T> {
552 UnitNegRange::clamp(vector2d::perp_product(sin_a.0, cos_a.0, sin_b.0, cos_b.0))
553}
554
555#[must_use]
564pub fn sine_sum<T: Float>(
565 sin_a: UnitNegRange<T>,
566 cos_a: UnitNegRange<T>,
567 sin_b: UnitNegRange<T>,
568 cos_b: UnitNegRange<T>,
569) -> UnitNegRange<T> {
570 sine_diff(sin_a, cos_a, -sin_b, cos_b)
571}
572
573#[must_use]
582pub fn cosine_diff<T: Float>(
583 sin_a: UnitNegRange<T>,
584 cos_a: UnitNegRange<T>,
585 sin_b: UnitNegRange<T>,
586 cos_b: UnitNegRange<T>,
587) -> UnitNegRange<T> {
588 UnitNegRange::clamp(vector2d::dot_product(sin_a.0, cos_a.0, sin_b.0, cos_b.0))
589}
590
591#[must_use]
600pub fn cosine_sum<T: Float>(
601 sin_a: UnitNegRange<T>,
602 cos_a: UnitNegRange<T>,
603 sin_b: UnitNegRange<T>,
604 cos_b: UnitNegRange<T>,
605) -> UnitNegRange<T> {
606 cosine_diff(sin_a, cos_a, -sin_b, cos_b)
607}
608
609#[must_use]
613pub fn sq_sine_half<T: Float>(cos: UnitNegRange<T>) -> T {
614 let half = T::one() / (T::one() + T::one());
615 (T::one() - cos.0) * half
616}
617
618#[must_use]
622pub fn sq_cosine_half<T: Float>(cos: UnitNegRange<T>) -> T {
623 let half = T::one() / (T::one() + T::one());
624 (T::one() + cos.0) * half
625}
626
627#[must_use]
637pub fn calculate_adjacent_length<T: Float>(length: T, hypotenuse: T) -> T {
638 if length <= T::zero() {
639 hypotenuse
640 } else if length >= hypotenuse {
641 T::zero()
642 } else {
643 ((hypotenuse - length) * (hypotenuse + length)).sqrt()
644 }
645}
646
647#[must_use]
657pub fn spherical_adjacent_length<T: Float + FloatConst>(
658 a: Radians<T>,
659 c: Radians<T>,
660) -> Radians<T> {
661 if a <= Radians(T::zero()) {
662 c
663 } else if a >= c {
664 Radians(T::zero())
665 } else {
666 Radians((c.0.cos() / a.0.cos()).acos())
667 }
668}
669
670#[must_use]
678pub fn spherical_hypotenuse_length<T: Float + FloatConst>(
679 a: Radians<T>,
680 b: Radians<T>,
681) -> Radians<T> {
682 if a <= Radians(T::zero()) {
683 b
684 } else if b <= Radians(T::zero()) {
685 a
686 } else {
687 Radians((a.0.cos() * b.0.cos()).acos())
688 }
689}
690
691#[must_use]
700pub fn spherical_cosine_rule<T: Float + FloatConst>(
701 cos_angle: UnitNegRange<T>,
702 length: Radians<T>,
703) -> Radians<T> {
704 Radians((cos_angle.0 * length.0.tan()).atan())
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use crate::is_within_tolerance;
711
712 #[test]
713 fn unit_neg_range_traits() {
714 let zero = UnitNegRange::default();
715 assert_eq!(UnitNegRange(0.0), zero);
716 let one = UnitNegRange(1.0);
717
718 let one_clone = one.clone();
719 assert_eq!(one_clone, one);
720
721 let minus_one = -one;
722 assert_eq!(minus_one, UnitNegRange(-1.0));
723 assert!(minus_one < one);
724 assert_eq!(one, minus_one.abs());
725
726 print!("UnitNegRange: {:?}", one);
727 }
728
729 #[test]
730 fn unit_neg_range_clamp() {
731 assert_eq!(-1.0, UnitNegRange::clamp(-1.0 - f64::EPSILON).0);
733 assert_eq!(-1.0, UnitNegRange::clamp(-1.0).0);
735 assert_eq!(1.0, UnitNegRange::clamp(1.0).0);
737 assert_eq!(1.0, UnitNegRange::clamp(1.0 + f64::EPSILON).0);
739 }
740
741 #[test]
742 fn unit_neg_range_is_valid() {
743 assert!(!UnitNegRange(-1.0 - f64::EPSILON).is_valid());
744 assert!(UnitNegRange(-1.0).is_valid());
745 assert!(UnitNegRange(1.0).is_valid());
746 assert!(!UnitNegRange(1.0 + f64::EPSILON).is_valid());
747 }
748
749 #[test]
750 fn test_trig_functions() {
751 let cos_60 = UnitNegRange(0.5);
752 let sin_60 = swap_sin_cos(cos_60);
753 assert_eq!(COS_30_DEGREES, sin_60.0);
754
755 let sin_120 = sin_60;
756 let cos_120 = cosine_from_sine(sin_120, -1.0);
757
758 let zero = cosine_from_sine(UnitNegRange(1.0), -1.0);
759 assert_eq!(0.0, zero.0);
760 assert!(zero.0.is_sign_positive());
761
762 let recip_sq_epsilon = 1.0 / SQ_EPSILON;
763
764 let sin_msq_epsilon = UnitNegRange(-SQ_EPSILON);
765 assert_eq!(-recip_sq_epsilon, csc(sin_msq_epsilon).unwrap());
766 assert_eq!(-recip_sq_epsilon, sec(sin_msq_epsilon).unwrap());
767
768 let cos_msq_epsilon = swap_sin_cos(sin_msq_epsilon);
769 assert_eq!(1.0, sec(cos_msq_epsilon).unwrap());
770 assert_eq!(1.0, csc(cos_msq_epsilon).unwrap());
771
772 assert_eq!(-SQ_EPSILON, tan(sin_msq_epsilon, cos_msq_epsilon).unwrap());
773 assert_eq!(
774 -recip_sq_epsilon,
775 cot(sin_msq_epsilon, cos_msq_epsilon).unwrap()
776 );
777
778 assert!(is_within_tolerance(
779 sin_120.0,
780 sine_sum(sin_60, cos_60, sin_60, cos_60).0,
781 f64::EPSILON
782 ));
783 assert!(is_within_tolerance(
784 cos_120.0,
785 cosine_sum(sin_60, cos_60, sin_60, cos_60).0,
786 f64::EPSILON
787 ));
788
789 let result = sq_sine_half(cos_120);
790 assert_eq!(sin_60.0, result.sqrt());
791
792 let result = sq_cosine_half(cos_120);
793 assert!(is_within_tolerance(cos_60.0, result.sqrt(), f64::EPSILON));
794 }
795
796 #[test]
797 fn test_small_angle_conversion() {
798 assert_eq!(MAX_LINEAR_SIN_ANGLE, sine(Radians(MAX_LINEAR_SIN_ANGLE)).0);
800
801 let s = sine(Radians(MAX_COS_ANGLE_IS_ONE));
803 assert_eq!(
804 MAX_COS_ANGLE_IS_ONE.cos(),
805 cosine(Radians(MAX_COS_ANGLE_IS_ONE), s).0
806 );
807 assert_eq!(1.0, MAX_COS_ANGLE_IS_ONE.cos());
808
809 let angle = Radians(4.74e7 * f64::EPSILON);
811 assert_eq!(1.0, angle.0.cos());
812
813 let s = sine(angle);
816 let result = cosine(angle, s);
817 assert_eq!(1.0 - f64::EPSILON / 2.0, result.0);
818 assert!(result.0 < angle.0.cos());
819 }
820
821 #[test]
822 fn test_radians_conversion() {
823 assert!(
827 core::f64::consts::FRAC_PI_2
828 != core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
829 );
830
831 assert_eq!(
833 core::f64::consts::FRAC_PI_2 + f64::EPSILON,
834 core::f64::consts::FRAC_PI_3 + core::f64::consts::FRAC_PI_6
835 );
836
837 assert_eq!(
839 core::f64::consts::FRAC_PI_2,
840 2.0 * core::f64::consts::FRAC_PI_4
841 );
842 assert_eq!(core::f64::consts::PI, 2.0 * core::f64::consts::FRAC_PI_2);
844
845 assert_eq!(core::f64::consts::FRAC_PI_4, 45.0_f64.to_radians());
847
848 assert_eq!(
850 core::f64::consts::FRAC_1_SQRT_2 - 0.5 * f64::EPSILON,
851 core::f64::consts::FRAC_PI_4.sin()
852 );
853
854 let result = sincos(Radians(-core::f64::consts::FRAC_PI_6));
856 assert_eq!(-0.5, result.0.0);
857 assert_eq!(COS_30_DEGREES, result.1.0);
858 assert_eq!(-core::f64::consts::FRAC_PI_6, arctan2(result.0, result.1).0);
859
860 let result = sincos(Radians(core::f64::consts::FRAC_PI_3));
862 assert!(is_within_tolerance(
865 COS_30_DEGREES,
866 result.0.0,
867 f64::EPSILON
868 ));
869 assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
871 assert_eq!(core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
872
873 let result = sincos(Radians(-core::f64::consts::PI));
875 assert_eq!(0.0, result.0.0);
876 assert_eq!(-1.0, result.1.0);
877 assert_eq!(core::f64::consts::PI, arctan2(result.0, result.1).0);
878
879 let result = sincos_diff(
881 Radians(core::f64::consts::PI),
882 Radians(core::f64::consts::FRAC_PI_4),
883 );
884 assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
885 assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
886 assert_eq!(
887 core::f64::consts::PI - core::f64::consts::FRAC_PI_4,
888 arctan2(result.0, result.1).0
889 );
890
891 let result = sincos_diff(
893 Radians(3.0 * core::f64::consts::TAU),
894 Radians(core::f64::consts::FRAC_PI_3),
895 );
896 assert!(is_within_tolerance(
899 -COS_30_DEGREES,
900 result.0.0,
901 f64::EPSILON
902 ));
903 assert!(is_within_tolerance(0.5, result.1.0, f64::EPSILON));
905 assert_eq!(-core::f64::consts::FRAC_PI_3, arctan2(result.0, result.1).0);
906 }
907
908 #[test]
909 fn test_degrees_conversion() {
910 assert_eq!(90.0, 60.0 + 30.0);
912 assert_eq!(90.0, 2.0 * 45.0);
913 assert_eq!(180.0, 2.0 * 90.0);
914
915 let result = sincosd(Degrees(-30.0));
917 assert_eq!(-0.5, result.0.0);
918 assert_eq!(COS_30_DEGREES, result.1.0);
919 assert_eq!(-30.0, arctan2d(result.0, result.1).0);
920
921 let result = sincosd(Degrees(60.0));
923 assert_eq!(COS_30_DEGREES, result.0.0);
924 assert_eq!(0.5, result.1.0);
925 assert_eq!(60.0, arctan2d(result.0, result.1).0);
926
927 let result = sincosd(Degrees(-180.0));
929 assert_eq!(0.0, result.0.0);
930 assert_eq!(-1.0, result.1.0);
931 assert_eq!(180.0, arctan2d(result.0, result.1).0);
932
933 let result = sincosd_diff(Degrees(180.0), Degrees(45.0));
935 assert_eq!(core::f64::consts::FRAC_1_SQRT_2, result.0.0);
936 assert_eq!(-core::f64::consts::FRAC_1_SQRT_2, result.1.0);
937 assert_eq!(180.0 - 45.0, arctan2d(result.0, result.1).0);
938
939 let result = sincosd_diff(Degrees(1080.0), Degrees(60.0));
941 assert_eq!(-COS_30_DEGREES, result.0.0);
942 assert_eq!(0.5, result.1.0);
943 assert_eq!(-60.0, arctan2d(result.0, result.1).0);
944 }
945
946 #[test]
947 fn test_calculate_adjacent_length() {
948 assert_eq!(0.0, calculate_adjacent_length(5.0, 5.0));
950
951 assert_eq!(5.0, calculate_adjacent_length(0.0, 5.0));
953
954 assert_eq!(0.0, calculate_adjacent_length(6.0, 5.0));
956
957 assert_eq!(3.0, calculate_adjacent_length(4.0, 5.0));
959 }
960
961 #[test]
962 fn test_spherical_adjacent_length() {
963 assert_eq!(
965 Radians(0.0),
966 spherical_adjacent_length(Radians(5.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
967 );
968
969 assert_eq!(
971 Radians(5.0_f64.to_radians()),
972 spherical_adjacent_length(Radians(0.0), Radians(5.0_f64.to_radians()))
973 );
974
975 assert_eq!(
977 Radians(0.0),
978 spherical_adjacent_length(Radians(6.0_f64.to_radians()), Radians(5.0_f64.to_radians()))
979 );
980
981 let result =
983 spherical_adjacent_length(Radians(4.0_f64.to_radians()), Radians(5.0_f64.to_radians()));
984 assert!(is_within_tolerance(3.0_f64.to_radians(), result.0, 1.0e-4));
985 }
986
987 #[test]
988 fn test_spherical_hypotenuse_length() {
989 let zero = Radians(0.0);
990 let three = Radians(3.0_f64.to_radians());
991 let four = Radians(4.0_f64.to_radians());
992
993 assert_eq!(three, spherical_hypotenuse_length(-four, three));
995 assert_eq!(four, spherical_hypotenuse_length(four, -three));
997
998 assert_eq!(three, spherical_hypotenuse_length(zero, three));
1000 assert_eq!(four, spherical_hypotenuse_length(four, zero));
1002 assert_eq!(zero, spherical_hypotenuse_length(zero, zero));
1004
1005 let result = Radians(0.087240926337265545);
1007 assert_eq!(result, spherical_hypotenuse_length(four, three));
1008 assert_eq!(result, spherical_hypotenuse_length(three, four));
1009 }
1010
1011 #[test]
1012 fn test_spherical_cosine_rule() {
1013 let result = spherical_cosine_rule(UnitNegRange(0.0), Radians(1.0));
1014 assert_eq!(0.0, result.0);
1015
1016 let result = spherical_cosine_rule(UnitNegRange(0.8660254037844386), Radians(0.5));
1017 assert_eq!(0.44190663576327144, result.0);
1018
1019 let result = spherical_cosine_rule(UnitNegRange(0.5), Radians(1.0));
1020 assert_eq!(0.66161993185017653, result.0);
1021
1022 let result = spherical_cosine_rule(UnitNegRange(1.0), Radians(1.0));
1023 assert_eq!(1.0, result.0);
1024 }
1025}