1use core::fmt;
28use core::ops::{Add, Div, Mul, Neg, Sub};
29use core::str::FromStr;
30use core::time::Duration;
31
32use crate::angle::wrap180;
33use crate::error::{ensure_finite, KernelError, Result};
34use crate::math;
35
36pub const METRES_PER_NAUTICAL_MILE: f64 = 1852.0;
38pub const METRES_PER_FOOT: f64 = 0.3048;
40pub const METRES_PER_FATHOM: f64 = 6.0 * METRES_PER_FOOT;
42pub const CABLES_PER_NAUTICAL_MILE: f64 = 10.0;
44const SECONDS_PER_HOUR: f64 = 3600.0;
46const SECONDS_PER_MINUTE: f64 = 60.0;
48const MINUTES_PER_HOUR: f64 = 60.0;
50
51#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
56#[cfg_attr(
57 feature = "serde",
58 derive(serde::Serialize, serde::Deserialize),
59 serde(try_from = "f64", into = "f64")
60)]
61pub struct Angle(f64);
62
63impl Angle {
64 pub const ZERO: Self = Self(0.0);
66
67 pub fn from_degrees(value: f64) -> Result<Self> {
73 ensure_finite("angle", value)?;
74 Ok(Self(value))
75 }
76
77 pub fn from_minutes(value: f64) -> Result<Self> {
83 ensure_finite("angle", value)?;
84 Ok(Self(value / 60.0))
85 }
86
87 pub fn from_degrees_minutes_seconds(degrees: f64, minutes: f64, seconds: f64) -> Result<Self> {
93 ensure_finite("angle", degrees)?;
94 ensure_finite("angle", minutes)?;
95 ensure_finite("angle", seconds)?;
96 Ok(Self(degrees + minutes / 60.0 + seconds / 3600.0))
97 }
98
99 pub fn from_radians(value: f64) -> Result<Self> {
105 ensure_finite("angle", value)?;
106 Ok(Self(math::to_degrees(value)))
107 }
108
109 #[doc(hidden)]
117 #[must_use]
118 pub const fn from_degrees_unchecked(value: f64) -> Self {
119 Self(value)
120 }
121
122 #[must_use]
124 pub const fn degrees(self) -> f64 {
125 self.0
126 }
127
128 #[must_use]
130 pub fn minutes(self) -> f64 {
131 self.0 * 60.0
132 }
133
134 #[must_use]
136 pub fn radians(self) -> f64 {
137 math::to_radians(self.0)
138 }
139
140 #[must_use]
142 pub fn abs(self) -> Self {
143 Self(math::abs(self.0))
144 }
145
146 #[must_use]
148 pub fn normalised(self) -> Self {
149 Self(wrap180(self.0))
150 }
151}
152
153impl fmt::Display for Angle {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 let precision = f.precision().unwrap_or(1);
156 write!(f, "{:.precision$}°", self.0)
157 }
158}
159
160impl Neg for Angle {
161 type Output = Self;
162 fn neg(self) -> Self {
163 Self(-self.0)
164 }
165}
166
167impl Add for Angle {
168 type Output = Self;
169 fn add(self, other: Self) -> Self {
170 Self(self.0 + other.0)
171 }
172}
173
174impl Sub for Angle {
175 type Output = Self;
176 fn sub(self, other: Self) -> Self {
177 Self(self.0 - other.0)
178 }
179}
180
181impl FromStr for Angle {
182 type Err = KernelError;
183
184 fn from_str(input: &str) -> Result<Self> {
193 let parsed = crate::parse::sexagesimal("angle", input)?;
194 if parsed.hemisphere.is_some() {
195 return Err(crate::parse::parse_error("angle", input));
196 }
197 Self::from_degrees(parsed.signed(""))
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
206#[cfg_attr(
207 feature = "serde",
208 derive(serde::Serialize, serde::Deserialize),
209 serde(try_from = "f64", into = "f64")
210)]
211pub struct Distance(f64);
212
213impl Distance {
214 pub const ZERO: Self = Self(0.0);
216
217 pub fn from_nautical_miles(value: f64) -> Result<Self> {
223 ensure_finite("distance", value)?;
224 Ok(Self(value))
225 }
226
227 pub fn from_cables(value: f64) -> Result<Self> {
233 ensure_finite("distance", value)?;
234 Ok(Self(value / CABLES_PER_NAUTICAL_MILE))
235 }
236
237 pub fn from_metres(value: f64) -> Result<Self> {
243 ensure_finite("distance", value)?;
244 Ok(Self(value / METRES_PER_NAUTICAL_MILE))
245 }
246
247 pub fn from_kilometres(value: f64) -> Result<Self> {
253 ensure_finite("distance", value)?;
254 Ok(Self(value * 1000.0 / METRES_PER_NAUTICAL_MILE))
255 }
256
257 pub fn from_feet(value: f64) -> Result<Self> {
263 ensure_finite("distance", value)?;
264 Ok(Self(value * METRES_PER_FOOT / METRES_PER_NAUTICAL_MILE))
265 }
266
267 pub fn from_fathoms(value: f64) -> Result<Self> {
273 ensure_finite("distance", value)?;
274 Ok(Self(value * METRES_PER_FATHOM / METRES_PER_NAUTICAL_MILE))
275 }
276
277 pub fn from_arc_minutes(value: f64) -> Result<Self> {
283 Self::from_nautical_miles(value)
284 }
285
286 #[doc(hidden)]
294 #[must_use]
295 pub const fn from_nautical_miles_unchecked(value: f64) -> Self {
296 Self(value)
297 }
298
299 #[must_use]
301 pub const fn nautical_miles(self) -> f64 {
302 self.0
303 }
304
305 #[must_use]
307 pub fn cables(self) -> f64 {
308 self.0 * CABLES_PER_NAUTICAL_MILE
309 }
310
311 #[must_use]
313 pub fn metres(self) -> f64 {
314 self.0 * METRES_PER_NAUTICAL_MILE
315 }
316
317 #[must_use]
319 pub fn kilometres(self) -> f64 {
320 self.0 * METRES_PER_NAUTICAL_MILE / 1000.0
321 }
322
323 #[must_use]
325 pub fn feet(self) -> f64 {
326 self.0 * METRES_PER_NAUTICAL_MILE / METRES_PER_FOOT
327 }
328
329 #[must_use]
331 pub fn abs(self) -> Self {
332 Self(math::abs(self.0))
333 }
334
335 #[must_use]
337 pub fn is_negative(self) -> bool {
338 self.0 < 0.0
339 }
340
341 pub fn time_at(self, speed: Speed) -> Result<Duration> {
347 speed.time_to_cover(self)
348 }
349}
350
351impl fmt::Display for Distance {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 let precision = f.precision().unwrap_or(1);
355 write!(f, "{:.precision$} M", self.0)
356 }
357}
358
359impl Neg for Distance {
360 type Output = Self;
361 fn neg(self) -> Self {
362 Self(-self.0)
363 }
364}
365
366impl Add for Distance {
367 type Output = Self;
368 fn add(self, other: Self) -> Self {
369 Self(self.0 + other.0)
370 }
371}
372
373impl Sub for Distance {
374 type Output = Self;
375 fn sub(self, other: Self) -> Self {
376 Self(self.0 - other.0)
377 }
378}
379
380impl Mul<f64> for Distance {
381 type Output = Self;
382 fn mul(self, factor: f64) -> Self {
383 Self(self.0 * factor)
384 }
385}
386
387impl Div<f64> for Distance {
388 type Output = Self;
389 fn div(self, divisor: f64) -> Self {
390 Self(self.0 / divisor)
391 }
392}
393
394#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
396#[cfg_attr(
397 feature = "serde",
398 derive(serde::Serialize, serde::Deserialize),
399 serde(try_from = "f64", into = "f64")
400)]
401pub struct Speed(f64);
402
403impl Speed {
404 pub const ZERO: Self = Self(0.0);
406
407 pub fn from_knots(value: f64) -> Result<Self> {
413 ensure_finite("speed", value)?;
414 Ok(Self(value))
415 }
416
417 pub fn from_metres_per_second(value: f64) -> Result<Self> {
423 ensure_finite("speed", value)?;
424 Ok(Self(value * SECONDS_PER_HOUR / METRES_PER_NAUTICAL_MILE))
425 }
426
427 pub fn from_kilometres_per_hour(value: f64) -> Result<Self> {
433 ensure_finite("speed", value)?;
434 Ok(Self(value * 1000.0 / METRES_PER_NAUTICAL_MILE))
435 }
436
437 #[doc(hidden)]
445 #[must_use]
446 pub const fn from_knots_unchecked(value: f64) -> Self {
447 Self(value)
448 }
449
450 #[must_use]
452 pub const fn knots(self) -> f64 {
453 self.0
454 }
455
456 #[must_use]
458 pub fn metres_per_second(self) -> f64 {
459 self.0 * METRES_PER_NAUTICAL_MILE / SECONDS_PER_HOUR
460 }
461
462 #[must_use]
464 pub fn kilometres_per_hour(self) -> f64 {
465 self.0 * METRES_PER_NAUTICAL_MILE / 1000.0
466 }
467
468 #[must_use]
470 pub fn abs(self) -> Self {
471 Self(math::abs(self.0))
472 }
473
474 #[must_use]
476 pub fn is_negative(self) -> bool {
477 self.0 < 0.0
478 }
479
480 #[must_use]
482 pub fn distance_covered(self, elapsed: Duration) -> Distance {
483 Distance(self.0 * elapsed.as_secs_f64() / SECONDS_PER_HOUR)
484 }
485
486 pub fn time_to_cover(self, distance: Distance) -> Result<Duration> {
493 let hours = distance.0 / self.0;
494 if !hours.is_finite() || hours < 0.0 {
495 return Err(KernelError::Indeterminate {
496 quantity: "time to cover the distance",
497 });
498 }
499 Duration::try_from_secs_f64(hours * SECONDS_PER_HOUR).map_err(|_| {
500 KernelError::Unrepresentable {
501 what: "the time to cover the distance",
502 }
503 })
504 }
505}
506
507impl fmt::Display for Speed {
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 let precision = f.precision().unwrap_or(1);
511 write!(f, "{:.precision$} kn", self.0)
512 }
513}
514
515impl Neg for Speed {
516 type Output = Self;
517 fn neg(self) -> Self {
518 Self(-self.0)
519 }
520}
521
522impl Add for Speed {
523 type Output = Self;
524 fn add(self, other: Self) -> Self {
525 Self(self.0 + other.0)
526 }
527}
528
529impl Sub for Speed {
530 type Output = Self;
531 fn sub(self, other: Self) -> Self {
532 Self(self.0 - other.0)
533 }
534}
535
536impl Mul<f64> for Speed {
537 type Output = Self;
538 fn mul(self, factor: f64) -> Self {
539 Self(self.0 * factor)
540 }
541}
542
543#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
547#[cfg_attr(
548 feature = "serde",
549 derive(serde::Serialize, serde::Deserialize),
550 serde(try_from = "f64", into = "f64")
551)]
552pub struct RateOfTurn(f64);
553
554impl RateOfTurn {
555 pub const ZERO: Self = Self(0.0);
557
558 pub fn from_degrees_per_minute(value: f64) -> Result<Self> {
564 ensure_finite("rate of turn", value)?;
565 Ok(Self(value))
566 }
567
568 pub fn from_radians_per_second(value: f64) -> Result<Self> {
574 ensure_finite("rate of turn", value)?;
575 Ok(Self(math::to_degrees(value) * SECONDS_PER_MINUTE))
576 }
577
578 #[must_use]
580 pub const fn degrees_per_minute(self) -> f64 {
581 self.0
582 }
583
584 #[must_use]
586 pub fn radians_per_second(self) -> f64 {
587 math::to_radians(self.0) / SECONDS_PER_MINUTE
588 }
589
590 #[must_use]
592 pub fn abs(self) -> Self {
593 Self(math::abs(self.0))
594 }
595
596 #[must_use]
598 pub fn is_to_port(self) -> bool {
599 self.0 < 0.0
600 }
601
602 pub fn radius_at(self, speed: Speed) -> Result<Distance> {
608 let radians_per_hour = math::to_radians(self.0) * MINUTES_PER_HOUR;
609 let radius = speed.0 / radians_per_hour;
610 if !radius.is_finite() {
611 return Err(KernelError::Indeterminate {
612 quantity: "the radius of a turn at no rate",
613 });
614 }
615 Ok(Distance(math::abs(radius)))
616 }
617
618 pub fn around(radius: Distance, speed: Speed) -> Result<Self> {
624 let radians_per_hour = speed.0 / radius.0;
625 if !radians_per_hour.is_finite() {
626 return Err(KernelError::Indeterminate {
627 quantity: "the rate of a turn of no radius",
628 });
629 }
630 Ok(Self(
631 math::to_degrees(math::abs(radians_per_hour)) / MINUTES_PER_HOUR,
632 ))
633 }
634}
635
636impl fmt::Display for RateOfTurn {
637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639 let precision = f.precision().unwrap_or(1);
640 write!(f, "{:.precision$}°/min", self.0)
641 }
642}
643
644impl Neg for RateOfTurn {
645 type Output = Self;
646 fn neg(self) -> Self {
647 Self(-self.0)
648 }
649}
650
651#[must_use]
653pub fn hours(elapsed: Duration) -> f64 {
654 elapsed.as_secs_f64() / SECONDS_PER_HOUR
655}
656
657pub fn duration_from_hours(value: f64) -> Result<Duration> {
664 if !value.is_finite() || value < 0.0 {
665 return Err(KernelError::Indeterminate {
666 quantity: "elapsed time",
667 });
668 }
669 Duration::try_from_secs_f64(value * SECONDS_PER_HOUR).map_err(|_| {
670 KernelError::Unrepresentable {
671 what: "the elapsed time",
672 }
673 })
674}
675
676#[cfg(feature = "serde")]
677impl TryFrom<f64> for Angle {
678 type Error = KernelError;
679
680 fn try_from(value: f64) -> Result<Self> {
682 Self::from_degrees(value)
683 }
684}
685
686#[cfg(feature = "serde")]
687impl From<Angle> for f64 {
688 fn from(value: Angle) -> Self {
689 value.0
690 }
691}
692
693#[cfg(feature = "serde")]
694impl TryFrom<f64> for Distance {
695 type Error = KernelError;
696
697 fn try_from(value: f64) -> Result<Self> {
699 Self::from_nautical_miles(value)
700 }
701}
702
703#[cfg(feature = "serde")]
704impl From<Distance> for f64 {
705 fn from(value: Distance) -> Self {
706 value.0
707 }
708}
709
710#[cfg(feature = "serde")]
711impl TryFrom<f64> for Speed {
712 type Error = KernelError;
713
714 fn try_from(value: f64) -> Result<Self> {
716 Self::from_knots(value)
717 }
718}
719
720#[cfg(feature = "serde")]
721impl From<Speed> for f64 {
722 fn from(value: Speed) -> Self {
723 value.0
724 }
725}
726
727#[cfg(feature = "serde")]
728impl TryFrom<f64> for RateOfTurn {
729 type Error = KernelError;
730
731 fn try_from(value: f64) -> Result<Self> {
733 Self::from_degrees_per_minute(value)
734 }
735}
736
737#[cfg(feature = "serde")]
738impl From<RateOfTurn> for f64 {
739 fn from(value: RateOfTurn) -> Self {
740 value.0
741 }
742}
743
744#[cfg(test)]
745#[allow(clippy::unwrap_used, clippy::float_cmp, clippy::indexing_slicing)]
746mod tests {
747 use super::*;
748 use alloc::format;
749
750 #[test]
751 fn a_rate_of_turn_and_a_radius_are_two_views_of_one_circle() {
752 let rate = RateOfTurn::from_degrees_per_minute(10.0).unwrap();
755 let speed = Speed::from_knots(10.0).unwrap();
756 let radius = rate.radius_at(speed).unwrap();
757 assert!((radius.nautical_miles() - 6.0 / core::f64::consts::TAU).abs() < 1e-12);
758 let back = RateOfTurn::around(radius, speed).unwrap();
759 assert!((back.degrees_per_minute() - 10.0).abs() < 1e-12);
760
761 assert_eq!((-rate).radius_at(speed).unwrap(), radius);
763 assert!((-rate).is_to_port());
764 assert!(!rate.is_to_port());
765 assert_eq!((-rate).abs(), rate);
766
767 assert!((rate.radians_per_second() - 10.0_f64.to_radians() / 60.0).abs() < 1e-15);
768 assert!(
769 (RateOfTurn::from_radians_per_second(rate.radians_per_second())
770 .unwrap()
771 .degrees_per_minute()
772 - 10.0)
773 .abs()
774 < 1e-12
775 );
776 assert_eq!(format!("{rate}"), "10.0°/min");
777 assert_eq!(format!("{}", RateOfTurn::ZERO), "0.0°/min");
778 }
779
780 #[test]
781 fn a_turn_at_no_rate_or_of_no_radius_has_no_circle() {
782 let speed = Speed::from_knots(10.0).unwrap();
783 assert!(RateOfTurn::ZERO.radius_at(speed).is_err());
784 assert!(RateOfTurn::around(Distance::ZERO, speed).is_err());
785 assert!(RateOfTurn::from_degrees_per_minute(f64::NAN).is_err());
786 assert!(RateOfTurn::from_radians_per_second(f64::INFINITY).is_err());
787 assert_eq!(
789 RateOfTurn::from_degrees_per_minute(5.0)
790 .unwrap()
791 .radius_at(Speed::ZERO)
792 .unwrap(),
793 Distance::ZERO
794 );
795 }
796
797 #[test]
798 fn distance_units_round_trip() {
799 let distance = Distance::from_nautical_miles(1.0).unwrap();
800 assert_eq!(distance.metres(), 1852.0);
801 assert_eq!(distance.cables(), 10.0);
802 assert!((distance.kilometres() - 1.852).abs() < 1e-12);
803 assert!((distance.feet() - 6076.115).abs() < 1e-3);
804
805 for constructor in [
806 Distance::from_metres(1852.0),
807 Distance::from_cables(10.0),
808 Distance::from_kilometres(1.852),
809 Distance::from_arc_minutes(1.0),
810 ] {
811 assert!((constructor.unwrap().nautical_miles() - 1.0).abs() < 1e-12);
812 }
813
814 assert!(
815 (Distance::from_feet(6.0).unwrap().nautical_miles()
816 - Distance::from_fathoms(1.0).unwrap().nautical_miles())
817 .abs()
818 < 1e-15
819 );
820 }
821
822 #[test]
823 fn speed_units_round_trip() {
824 let speed = Speed::from_knots(1.0).unwrap();
825 assert!((speed.metres_per_second() - 0.514_444_444).abs() < 1e-9);
826 assert!((speed.kilometres_per_hour() - 1.852).abs() < 1e-12);
827 assert!(
828 (Speed::from_metres_per_second(0.514_444_444_444_444_4)
829 .unwrap()
830 .knots()
831 - 1.0)
832 .abs()
833 < 1e-12
834 );
835 }
836
837 #[test]
838 fn distance_and_time_are_consistent() {
839 let speed = Speed::from_knots(8.0).unwrap();
840 let distance = Distance::from_nautical_miles(12.0).unwrap();
841 let elapsed = speed.time_to_cover(distance).unwrap();
842 assert_eq!(elapsed.as_secs(), 5400);
843 assert!((speed.distance_covered(elapsed).nautical_miles() - 12.0).abs() < 1e-12);
844 assert!((distance.time_at(speed).unwrap().as_secs_f64() - 5400.0).abs() < 1e-9);
845 }
846
847 #[test]
848 fn impossible_times_are_errors_not_panics() {
849 let distance = Distance::from_nautical_miles(10.0).unwrap();
850 assert!(Speed::ZERO.time_to_cover(distance).is_err());
851 assert!(Speed::from_knots(-5.0)
852 .unwrap()
853 .time_to_cover(distance)
854 .is_err());
855 assert!(Speed::from_knots(1e-300)
856 .unwrap()
857 .time_to_cover(Distance::from_nautical_miles(1e300).unwrap())
858 .is_err());
859 }
860
861 #[test]
862 fn non_finite_input_is_rejected() {
863 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
864 assert!(Distance::from_nautical_miles(value).is_err());
865 assert!(Distance::from_metres(value).is_err());
866 assert!(Speed::from_knots(value).is_err());
867 assert!(Angle::from_degrees(value).is_err());
868 assert!(Angle::from_minutes(value).is_err());
869 assert!(Angle::from_radians(value).is_err());
870 }
871 }
872
873 #[test]
874 fn angle_conversions() {
875 let angle = Angle::from_degrees_minutes_seconds(1.0, 30.0, 0.0).unwrap();
876 assert_eq!(angle.degrees(), 1.5);
877 assert_eq!(angle.minutes(), 90.0);
878 assert!((Angle::from_minutes(90.0).unwrap().degrees() - 1.5).abs() < 1e-12);
879 assert!(
880 (Angle::from_radians(core::f64::consts::PI)
881 .unwrap()
882 .degrees()
883 - 180.0)
884 .abs()
885 < 1e-12
886 );
887 assert_eq!((-angle).abs(), angle);
888 assert_eq!(
889 Angle::from_degrees(370.0).unwrap().normalised().degrees(),
890 10.0
891 );
892 }
893
894 #[test]
895 fn angles_parse_from_how_they_are_written() {
896 assert_eq!("1.5".parse::<Angle>().unwrap().degrees(), 1.5);
897 assert!(("1°30'".parse::<Angle>().unwrap().degrees() - 1.5).abs() < 1e-12);
898 assert!(("1 30 00".parse::<Angle>().unwrap().degrees() - 1.5).abs() < 1e-12);
899 assert!(("-2.7".parse::<Angle>().unwrap().degrees() + 2.7).abs() < 1e-12);
900 assert!("1°30'N".parse::<Angle>().is_err());
902 assert!("".parse::<Angle>().is_err());
903 assert!("1 60".parse::<Angle>().is_err());
904 }
905
906 #[test]
907 fn arithmetic_behaves() {
908 let a = Distance::from_nautical_miles(3.0).unwrap();
909 let b = Distance::from_nautical_miles(4.0).unwrap();
910 assert_eq!((a + b).nautical_miles(), 7.0);
911 assert_eq!((b - a).nautical_miles(), 1.0);
912 assert_eq!((a * 2.0).nautical_miles(), 6.0);
913 assert_eq!((b / 2.0).nautical_miles(), 2.0);
914 assert!((a - b).is_negative());
915 assert_eq!((a - b).abs().nautical_miles(), 1.0);
916 }
917
918 #[test]
919 fn display_is_readable() {
920 assert_eq!(
921 format!("{}", Distance::from_nautical_miles(12.0).unwrap()),
922 "12.0 M"
923 );
924 assert_eq!(format!("{:.2}", Speed::from_knots(8.5).unwrap()), "8.50 kn");
925 assert_eq!(format!("{}", Angle::from_degrees(-1.25).unwrap()), "-1.2°");
926 }
927}