Skip to main content

lox_core/
units.rs

1// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! Newtype wrappers for unitful [`f64`] double precision values
6
7use std::{
8    f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU},
9    fmt::{Display, Formatter, Result},
10    ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
11};
12
13use glam::DMat3;
14use lox_test_utils::ApproxEq;
15
16use crate::f64::consts::SECONDS_PER_DAY;
17
18/// Degrees in full circle
19pub const DEGREES_IN_CIRCLE: f64 = 360.0;
20
21/// Arcseconds in full circle
22pub const ARCSECONDS_IN_CIRCLE: f64 = DEGREES_IN_CIRCLE * 60.0 * 60.0;
23
24/// Radians per arcsecond
25pub const RADIANS_IN_ARCSECOND: f64 = TAU / ARCSECONDS_IN_CIRCLE;
26
27type Radians = f64;
28
29/// Angle in radians.
30#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[repr(transparent)]
33pub struct Angle(Radians);
34
35impl Angle {
36    /// An angle equal to zero.
37    pub const ZERO: Self = Self(0.0);
38    /// An angle equal to π.
39    pub const PI: Self = Self(PI);
40    /// An angle equal to τ = 2π.
41    pub const TAU: Self = Self(TAU);
42    /// An angle equal to π/2.
43    pub const FRAC_PI_2: Self = Self(FRAC_PI_2);
44    /// An angle equal to π/4.
45    pub const FRAC_PI_4: Self = Self(FRAC_PI_4);
46
47    /// Creates a new angle from an `f64` value in radians.
48    pub const fn new(rad: f64) -> Self {
49        Self(rad)
50    }
51
52    /// Creates a new angle from an `f64` value in radians.
53    pub const fn radians(rad: f64) -> Self {
54        Self(rad)
55    }
56
57    /// Creates a new angle from an `f64` value in radians and normalize the angle
58    /// to the interval [0.0, 2π).
59    pub const fn radians_normalized(rad: f64) -> Self {
60        Self(rad).mod_two_pi()
61    }
62
63    /// Creates a new angle from an `f64` value in radians and normalize the angle
64    /// to the interval (-2π, 2π).
65    pub const fn radians_normalized_signed(rad: f64) -> Self {
66        Self(rad).mod_two_pi_signed()
67    }
68
69    /// Creates a new angle from an `f64` value in degrees.
70    pub const fn degrees(deg: f64) -> Self {
71        Self(deg.to_radians())
72    }
73
74    /// Creates a new angle from hours, minutes, and seconds (HMS notation).
75    pub const fn from_hms(hours: i64, minutes: u8, seconds: f64) -> Self {
76        Self::degrees(15.0 * (hours as f64 + minutes as f64 / 60.0 + seconds / 3600.0))
77    }
78
79    /// Creates a new angle from an `f64` value in degrees and normalize the angle
80    /// to the interval [0.0, 2π).
81    pub const fn degrees_normalized(deg: f64) -> Self {
82        Self((deg % DEGREES_IN_CIRCLE).to_radians()).mod_two_pi()
83    }
84
85    /// Creates a new angle from an `f64` value in degrees and normalize the angle
86    /// to the interval (-2π, 2π).
87    pub const fn degrees_normalized_signed(deg: f64) -> Self {
88        Self((deg % DEGREES_IN_CIRCLE).to_radians())
89    }
90
91    /// Creates a new angle from an `f64` value in arcseconds.
92    pub const fn arcseconds(asec: f64) -> Self {
93        Self(asec * RADIANS_IN_ARCSECOND)
94    }
95
96    /// Creates a new angle from an `f64` value in arcseconds and normalize the angle
97    /// to the interval [0.0, 2π).
98    pub const fn arcseconds_normalized(asec: f64) -> Self {
99        Self((asec % ARCSECONDS_IN_CIRCLE) * RADIANS_IN_ARCSECOND).mod_two_pi()
100    }
101
102    /// Creates a new angle from an `f64` value in arcseconds and normalize the angle
103    /// to the interval (-2π, 2π).
104    pub const fn arcseconds_normalized_signed(asec: f64) -> Self {
105        Self((asec % ARCSECONDS_IN_CIRCLE) * RADIANS_IN_ARCSECOND)
106    }
107
108    /// Returns `true` if the angle is exactly zero.
109    pub fn is_zero(&self) -> bool {
110        self.0 == 0.0
111    }
112
113    /// Returns the absolute value of the angle.
114    pub const fn abs(&self) -> Self {
115        Self(self.0.abs())
116    }
117
118    /// Creates an angle from the arcsine of a value.
119    pub fn from_asin(value: f64) -> Self {
120        Self(value.asin())
121    }
122
123    /// Creates an angle from the inverse hyperbolic sine of a value.
124    pub fn from_asinh(value: f64) -> Self {
125        Self(value.asinh())
126    }
127
128    /// Creates an angle from the arccosine of a value.
129    pub fn from_acos(value: f64) -> Self {
130        Self(value.acos())
131    }
132
133    /// Creates an angle from the inverse hyperbolic cosine of a value.
134    pub fn from_acosh(value: f64) -> Self {
135        Self(value.acosh())
136    }
137
138    /// Creates an angle from the arctangent of a value.
139    pub fn from_atan(value: f64) -> Self {
140        Self(value.atan())
141    }
142
143    /// Creates an angle from the inverse hyperbolic tangent of a value.
144    pub fn from_atanh(value: f64) -> Self {
145        Self(value.atanh())
146    }
147
148    /// Creates an angle from the two-argument arctangent of `y` and `x`.
149    pub fn from_atan2(y: f64, x: f64) -> Self {
150        Self(y.atan2(x))
151    }
152
153    /// Returns the cosine of the angle.
154    pub fn cos(&self) -> f64 {
155        self.0.cos()
156    }
157
158    /// Returns the hyperbolic cosine of the angle.
159    pub fn cosh(&self) -> f64 {
160        self.0.cosh()
161    }
162
163    /// Returns the sine of the angle.
164    pub fn sin(&self) -> f64 {
165        self.0.sin()
166    }
167
168    /// Returns the hyperbolic sine of the angle.
169    pub fn sinh(&self) -> f64 {
170        self.0.sinh()
171    }
172
173    /// Returns sine and cosine of the angle.
174    pub fn sin_cos(&self) -> (f64, f64) {
175        self.0.sin_cos()
176    }
177
178    /// Returns the tangent of the angle.
179    pub fn tan(&self) -> f64 {
180        self.0.tan()
181    }
182
183    /// Returns the hyperbolic tangent of the angle.
184    pub fn tanh(&self) -> f64 {
185        self.0.tanh()
186    }
187
188    /// Returns a new angle that is normalized to the interval [0.0, 2π).
189    pub const fn mod_two_pi(&self) -> Self {
190        let mut a = self.0 % TAU;
191        if a < 0.0 {
192            a += TAU
193        }
194        Self(a)
195    }
196
197    /// Returns a new angle that is normalized to the interval (-2π, 2π).
198    pub const fn mod_two_pi_signed(&self) -> Self {
199        Self(self.0 % TAU)
200    }
201
202    /// Returns a new angle that is normalized to a (-π, π) interval
203    /// centered around `center`.
204    pub const fn normalize_two_pi(&self, center: Self) -> Self {
205        Self(self.0 - TAU * ((self.0 + PI - center.0) / TAU).floor())
206    }
207
208    /// Returns the value of the angle in radians.
209    pub const fn as_f64(&self) -> f64 {
210        self.0
211    }
212
213    /// Returns the value of the angle in radians.
214    pub const fn to_radians(&self) -> f64 {
215        self.0
216    }
217
218    /// Returns the value of the angle in degrees.
219    pub const fn to_degrees(&self) -> f64 {
220        self.0.to_degrees()
221    }
222
223    /// Returns the value of the angle in arcseconds.
224    pub const fn to_arcseconds(&self) -> f64 {
225        self.0 / RADIANS_IN_ARCSECOND
226    }
227
228    /// Returns the 3×3 rotation matrix for a rotation about the X axis.
229    pub fn rotation_x(&self) -> DMat3 {
230        DMat3::from_rotation_x(-self.to_radians())
231    }
232
233    /// Returns the 3×3 rotation matrix for a rotation about the Y axis.
234    pub fn rotation_y(&self) -> DMat3 {
235        DMat3::from_rotation_y(-self.to_radians())
236    }
237
238    /// Returns the 3×3 rotation matrix for a rotation about the Z axis.
239    pub fn rotation_z(&self) -> DMat3 {
240        DMat3::from_rotation_z(-self.to_radians())
241    }
242}
243
244impl Display for Angle {
245    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
246        self.0.to_degrees().fmt(f)?;
247        write!(f, " deg")
248    }
249}
250
251/// A trait for creating [`Angle`] instances from primitives.
252///
253/// By default it is implemented for [`f64`] and [`i64`].
254///
255/// # Examples
256///
257/// ```
258/// use lox_core::units::AngleUnits;
259///
260/// let angle = 360.deg();
261/// assert_eq!(angle.to_radians(), std::f64::consts::TAU);
262/// ```
263pub trait AngleUnits {
264    /// Creates an angle from a value in radians.
265    fn rad(&self) -> Angle;
266    /// Creates an angle from a value in degrees.
267    fn deg(&self) -> Angle;
268    /// Creates an angle from a value in arcseconds.
269    fn arcsec(&self) -> Angle;
270    /// Creates an angle from a value in milliarcseconds.
271    fn mas(&self) -> Angle;
272    /// Creates an angle from a value in microarcseconds.
273    fn uas(&self) -> Angle;
274}
275
276impl AngleUnits for f64 {
277    fn rad(&self) -> Angle {
278        Angle::radians(*self)
279    }
280
281    fn deg(&self) -> Angle {
282        Angle::degrees(*self)
283    }
284
285    fn arcsec(&self) -> Angle {
286        Angle::arcseconds(*self)
287    }
288
289    fn mas(&self) -> Angle {
290        Angle::arcseconds(self * 1e-3)
291    }
292
293    fn uas(&self) -> Angle {
294        Angle::arcseconds(self * 1e-6)
295    }
296}
297
298impl AngleUnits for i64 {
299    fn rad(&self) -> Angle {
300        Angle::radians(*self as f64)
301    }
302
303    fn deg(&self) -> Angle {
304        Angle::degrees(*self as f64)
305    }
306
307    fn arcsec(&self) -> Angle {
308        Angle::arcseconds(*self as f64)
309    }
310
311    fn mas(&self) -> Angle {
312        Angle::arcseconds(*self as f64 * 1e-3)
313    }
314
315    fn uas(&self) -> Angle {
316        Angle::arcseconds(*self as f64 * 1e-6)
317    }
318}
319
320/// The astronomical unit in meters.
321pub const ASTRONOMICAL_UNIT: f64 = 1.495978707e11;
322
323type Meters = f64;
324
325/// Distance in meters.
326#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
328#[repr(transparent)]
329pub struct Distance(Meters);
330
331impl Distance {
332    /// Create a new distance from an `f64` value in meters.
333    pub const fn new(m: f64) -> Self {
334        Self(m)
335    }
336
337    /// Create a new distance from an `f64` value in meters.
338    pub const fn meters(m: f64) -> Self {
339        Self(m)
340    }
341
342    /// Create a new distance from an `f64` value in kilometers.
343    pub const fn kilometers(m: f64) -> Self {
344        Self(m * 1e3)
345    }
346
347    /// Create a new distance from an `f64` value in astronomical units.
348    pub const fn astronomical_units(au: f64) -> Self {
349        Self(au * ASTRONOMICAL_UNIT)
350    }
351
352    /// Returns the value of the distance in meters as an `f64`.
353    pub const fn as_f64(&self) -> f64 {
354        self.0
355    }
356
357    /// Returns the value of the distance in meters.
358    pub const fn to_meters(&self) -> f64 {
359        self.0
360    }
361
362    /// Returns the value of the distance in kilometers.
363    pub const fn to_kilometers(&self) -> f64 {
364        self.0 * 1e-3
365    }
366
367    /// Returns the value of the distance in astronomical units.
368    pub const fn to_astronomical_units(&self) -> f64 {
369        self.0 / ASTRONOMICAL_UNIT
370    }
371}
372
373impl Display for Distance {
374    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
375        (1e-3 * self.0).fmt(f)?;
376        write!(f, " km")
377    }
378}
379
380/// A trait for creating [`Distance`] instances from primitives.
381///
382/// By default it is implemented for [`f64`] and [`i64`].
383///
384/// # Examples
385///
386/// ```
387/// use lox_core::units::DistanceUnits;
388///
389/// let d = 1.km();
390/// assert_eq!(d.to_meters(), 1e3);
391/// ```
392pub trait DistanceUnits {
393    /// Creates a distance from a value in meters.
394    fn m(&self) -> Distance;
395    /// Creates a distance from a value in kilometers.
396    fn km(&self) -> Distance;
397    /// Creates a distance from a value in astronomical units.
398    fn au(&self) -> Distance;
399}
400
401impl DistanceUnits for f64 {
402    fn m(&self) -> Distance {
403        Distance::meters(*self)
404    }
405
406    fn km(&self) -> Distance {
407        Distance::kilometers(*self)
408    }
409
410    fn au(&self) -> Distance {
411        Distance::astronomical_units(*self)
412    }
413}
414
415impl DistanceUnits for i64 {
416    fn m(&self) -> Distance {
417        Distance::meters(*self as f64)
418    }
419
420    fn km(&self) -> Distance {
421        Distance::kilometers(*self as f64)
422    }
423
424    fn au(&self) -> Distance {
425        Distance::astronomical_units(*self as f64)
426    }
427}
428
429type MetersPerSecond = f64;
430
431/// Velocity in meters per second.
432#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
433#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
434#[repr(transparent)]
435pub struct Velocity(MetersPerSecond);
436
437impl Velocity {
438    /// Creates a new velocity from an `f64` value in m/s.
439    pub const fn new(mps: f64) -> Self {
440        Self(mps)
441    }
442
443    /// Creates a new velocity from an `f64` value in m/s.
444    pub const fn meters_per_second(mps: f64) -> Self {
445        Self(mps)
446    }
447
448    /// Creates a new velocity from an `f64` value in km/s.
449    pub const fn kilometers_per_second(mps: f64) -> Self {
450        Self(mps * 1e3)
451    }
452
453    /// Creates a new velocity from an `f64` value in au/d.
454    pub const fn astronomical_units_per_day(aud: f64) -> Self {
455        Self(aud * ASTRONOMICAL_UNIT / SECONDS_PER_DAY)
456    }
457
458    /// Creates a new velocity from an `f64` value in 1/c.
459    pub const fn fraction_of_speed_of_light(c: f64) -> Self {
460        Self(c * SPEED_OF_LIGHT)
461    }
462
463    /// Returns the value of the velocity in m/s as an `f64`.
464    pub const fn as_f64(&self) -> f64 {
465        self.0
466    }
467
468    /// Returns the value of the velocity in m/s.
469    pub const fn to_meters_per_second(&self) -> f64 {
470        self.0
471    }
472
473    /// Returns the value of the velocity in km/s.
474    pub const fn to_kilometers_per_second(&self) -> f64 {
475        self.0 * 1e-3
476    }
477
478    /// Returns the value of the velocity in au/d.
479    pub const fn to_astronomical_units_per_day(&self) -> f64 {
480        self.0 * SECONDS_PER_DAY / ASTRONOMICAL_UNIT
481    }
482
483    /// Returns the value of the velocity in 1/c.
484    pub const fn to_fraction_of_speed_of_light(&self) -> f64 {
485        self.0 / SPEED_OF_LIGHT
486    }
487}
488
489impl Display for Velocity {
490    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
491        (1e-3 * self.0).fmt(f)?;
492        write!(f, " km/s")
493    }
494}
495
496/// A trait for creating [`Velocity`] instances from primitives.
497///
498/// By default it is implemented for [`f64`] and [`i64`].
499///
500/// # Examples
501///
502/// ```
503/// use lox_core::units::VelocityUnits;
504///
505/// let v = 1.kps();
506/// assert_eq!(v.to_meters_per_second(), 1e3);
507/// ```
508pub trait VelocityUnits {
509    /// Creates a velocity from a value in m/s.
510    fn mps(&self) -> Velocity;
511    /// Creates a velocity from a value in km/s.
512    fn kps(&self) -> Velocity;
513    /// Creates a velocity from a value in au/d.
514    fn aud(&self) -> Velocity;
515    /// Crates a velocity from a value in 1/c (fraction of the speed of light).
516    fn c(&self) -> Velocity;
517}
518
519impl VelocityUnits for f64 {
520    fn mps(&self) -> Velocity {
521        Velocity::meters_per_second(*self)
522    }
523
524    fn kps(&self) -> Velocity {
525        Velocity::kilometers_per_second(*self)
526    }
527
528    fn aud(&self) -> Velocity {
529        Velocity::astronomical_units_per_day(*self)
530    }
531
532    fn c(&self) -> Velocity {
533        Velocity::fraction_of_speed_of_light(*self)
534    }
535}
536
537impl VelocityUnits for i64 {
538    fn mps(&self) -> Velocity {
539        Velocity::meters_per_second(*self as f64)
540    }
541
542    fn kps(&self) -> Velocity {
543        Velocity::kilometers_per_second(*self as f64)
544    }
545
546    fn aud(&self) -> Velocity {
547        Velocity::astronomical_units_per_day(*self as f64)
548    }
549
550    fn c(&self) -> Velocity {
551        Velocity::fraction_of_speed_of_light(*self as f64)
552    }
553}
554
555/// The speed of light in vacuum in m/s.
556pub const SPEED_OF_LIGHT: f64 = 299792458.0;
557
558/// IEEE letter codes for frequency bands commonly used for satellite communications.
559#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
560#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
561pub enum FrequencyBand {
562    /// HF (High Frequency) – 3 to 30 MHz
563    HF,
564    /// VHF (Very High Frequency) – 30 to 300 MHz
565    VHF,
566    /// UHF (Ultra-High Frequency) – 0.3 to 1 GHz
567    UHF,
568    /// L – 1 to 2 GHz
569    L,
570    /// S – 2 to 4 GHz
571    S,
572    /// C – 4 to 8 GHz
573    C,
574    /// X – 8 to 12 GHz
575    X,
576    /// Kᵤ – 12 to 18 GHz
577    Ku,
578    /// K – 18 to 27 GHz
579    K,
580    /// Kₐ – 27 to 40 GHz
581    Ka,
582    /// V – 40 to 75 GHz
583    V,
584    /// W – 75 to 110 GHz
585    W,
586    /// G – 110 to 300 GHz
587    G,
588}
589
590type Hertz = f64;
591
592/// Frequency in Hertz
593#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
594#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
595#[repr(transparent)]
596pub struct Frequency(Hertz);
597
598impl Frequency {
599    /// Creates a new frequency from an `f64` value in Hz.
600    pub const fn new(hz: Hertz) -> Self {
601        Self(hz)
602    }
603
604    /// Creates a new frequency from an `f64` value in Hz.
605    pub const fn hertz(hz: Hertz) -> Self {
606        Self(hz)
607    }
608
609    /// Creates a new frequency from an `f64` value in KHz.
610    pub const fn kilohertz(hz: Hertz) -> Self {
611        Self(hz * 1e3)
612    }
613
614    /// Creates a new frequency from an `f64` value in MHz.
615    pub const fn megahertz(hz: Hertz) -> Self {
616        Self(hz * 1e6)
617    }
618
619    /// Creates a new frequency from an `f64` value in GHz.
620    pub const fn gigahertz(hz: Hertz) -> Self {
621        Self(hz * 1e9)
622    }
623
624    /// Creates a new frequency from an `f64` value in THz.
625    pub const fn terahertz(hz: Hertz) -> Self {
626        Self(hz * 1e12)
627    }
628
629    /// Returns the value of the frequency in Hz.
630    pub const fn to_hertz(&self) -> f64 {
631        self.0
632    }
633
634    /// Returns the value of the frequency in KHz.
635    pub const fn to_kilohertz(&self) -> f64 {
636        self.0 * 1e-3
637    }
638
639    /// Returns the value of the frequency in MHz.
640    pub const fn to_megahertz(&self) -> f64 {
641        self.0 * 1e-6
642    }
643
644    /// Returns the value of the frequency in GHz.
645    pub const fn to_gigahertz(&self) -> f64 {
646        self.0 * 1e-9
647    }
648
649    /// Returns the value of the frequency in THz.
650    pub const fn to_terahertz(&self) -> f64 {
651        self.0 * 1e-12
652    }
653
654    /// Returns the wavelength.
655    pub fn wavelength(&self) -> Distance {
656        Distance(SPEED_OF_LIGHT / self.0)
657    }
658
659    /// Returns the IEEE letter code if the frequency matches one of the bands.
660    pub fn band(&self) -> Option<FrequencyBand> {
661        match self.0 {
662            f if f < 3e6 => None,
663            f if f < 30e6 => Some(FrequencyBand::HF),
664            f if f < 300e6 => Some(FrequencyBand::VHF),
665            f if f < 1e9 => Some(FrequencyBand::UHF),
666            f if f < 2e9 => Some(FrequencyBand::L),
667            f if f < 4e9 => Some(FrequencyBand::S),
668            f if f < 8e9 => Some(FrequencyBand::C),
669            f if f < 12e9 => Some(FrequencyBand::X),
670            f if f < 18e9 => Some(FrequencyBand::Ku),
671            f if f < 27e9 => Some(FrequencyBand::K),
672            f if f < 40e9 => Some(FrequencyBand::Ka),
673            f if f < 75e9 => Some(FrequencyBand::V),
674            f if f < 110e9 => Some(FrequencyBand::W),
675            f if f < 300e9 => Some(FrequencyBand::G),
676            _ => None,
677        }
678    }
679}
680
681impl Display for Frequency {
682    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
683        (1e-9 * self.0).fmt(f)?;
684        write!(f, " GHz")
685    }
686}
687
688/// A trait for creating [`Frequency`] instances from primitives.
689///
690/// By default it is implemented for [`f64`] and [`i64`].
691///
692/// # Examples
693///
694/// ```
695/// use lox_core::units::FrequencyUnits;
696///
697/// let f = 1.ghz();
698/// assert_eq!(f.to_hertz(), 1e9);
699/// ```
700pub trait FrequencyUnits {
701    /// Creates a frequency from a value in Hz.
702    fn hz(&self) -> Frequency;
703    /// Creates a frequency from a value in KHz.
704    fn khz(&self) -> Frequency;
705    /// Creates a frequency from a value in MHz.
706    fn mhz(&self) -> Frequency;
707    /// Creates a frequency from a value in GHz.
708    fn ghz(&self) -> Frequency;
709    /// Creates a frequency from a value in THz.
710    fn thz(&self) -> Frequency;
711}
712
713impl FrequencyUnits for f64 {
714    fn hz(&self) -> Frequency {
715        Frequency::hertz(*self)
716    }
717
718    fn khz(&self) -> Frequency {
719        Frequency::kilohertz(*self)
720    }
721
722    fn mhz(&self) -> Frequency {
723        Frequency::megahertz(*self)
724    }
725
726    fn ghz(&self) -> Frequency {
727        Frequency::gigahertz(*self)
728    }
729
730    fn thz(&self) -> Frequency {
731        Frequency::terahertz(*self)
732    }
733}
734
735impl FrequencyUnits for i64 {
736    fn hz(&self) -> Frequency {
737        Frequency::hertz(*self as f64)
738    }
739
740    fn khz(&self) -> Frequency {
741        Frequency::kilohertz(*self as f64)
742    }
743
744    fn mhz(&self) -> Frequency {
745        Frequency::megahertz(*self as f64)
746    }
747
748    fn ghz(&self) -> Frequency {
749        Frequency::gigahertz(*self as f64)
750    }
751
752    fn thz(&self) -> Frequency {
753        Frequency::terahertz(*self as f64)
754    }
755}
756
757/// Temperature in Kelvin (deprecated type alias, use [`Temperature`] instead).
758pub type Kelvin = f64;
759
760type KelvinValue = f64;
761
762/// Temperature in Kelvin.
763#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
764#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
765#[repr(transparent)]
766pub struct Temperature(KelvinValue);
767
768impl Temperature {
769    /// Creates a new temperature from an `f64` value in Kelvin.
770    pub const fn new(k: f64) -> Self {
771        Self(k)
772    }
773
774    /// Creates a new temperature from an `f64` value in Kelvin.
775    pub const fn kelvin(k: f64) -> Self {
776        Self(k)
777    }
778
779    /// Returns the value in Kelvin as an `f64`.
780    pub const fn as_f64(&self) -> f64 {
781        self.0
782    }
783
784    /// Returns the value in Kelvin.
785    pub const fn to_kelvin(&self) -> f64 {
786        self.0
787    }
788}
789
790impl Display for Temperature {
791    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
792        self.0.fmt(f)?;
793        write!(f, " K")
794    }
795}
796
797type Pascals = f64;
798
799/// Pressure in pascals.
800#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
801#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
802#[repr(transparent)]
803pub struct Pressure(Pascals);
804
805impl Pressure {
806    /// Creates a new pressure from an `f64` value in pascals.
807    pub const fn new(pa: f64) -> Self {
808        Self(pa)
809    }
810
811    /// Creates a new pressure from an `f64` value in pascals.
812    pub const fn pa(pa: f64) -> Self {
813        Self(pa)
814    }
815
816    /// Creates a new pressure from an `f64` value in hectopascals.
817    pub const fn hpa(hpa: f64) -> Self {
818        Self(hpa * 100.0)
819    }
820
821    /// Returns the value in pascals as an `f64`.
822    pub const fn as_f64(&self) -> f64 {
823        self.0
824    }
825
826    /// Returns the value in pascals.
827    pub const fn to_pa(&self) -> f64 {
828        self.0
829    }
830
831    /// Returns the value in hectopascals.
832    pub const fn to_hpa(&self) -> f64 {
833        self.0 * 0.01
834    }
835}
836
837impl Display for Pressure {
838    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
839        self.0.fmt(f)?;
840        write!(f, " Pa")
841    }
842}
843
844type Watts = f64;
845
846/// Power in Watts.
847#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
848#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
849#[repr(transparent)]
850pub struct Power(Watts);
851
852impl Power {
853    /// Creates a new power from an `f64` value in Watts.
854    pub const fn new(w: f64) -> Self {
855        Self(w)
856    }
857
858    /// Creates a new power from an `f64` value in Watts.
859    pub const fn watts(w: f64) -> Self {
860        Self(w)
861    }
862
863    /// Creates a new power from an `f64` value in kilowatts.
864    pub const fn kilowatts(kw: f64) -> Self {
865        Self(kw * 1e3)
866    }
867
868    /// Returns the value in Watts as an `f64`.
869    pub const fn as_f64(&self) -> f64 {
870        self.0
871    }
872
873    /// Returns the value in Watts.
874    pub const fn to_watts(&self) -> f64 {
875        self.0
876    }
877
878    /// Returns the value in kilowatts.
879    pub const fn to_kilowatts(&self) -> f64 {
880        self.0 * 1e-3
881    }
882
883    /// Returns the value in dBW.
884    pub fn to_dbw(&self) -> f64 {
885        10.0 * self.0.log10()
886    }
887}
888
889impl Display for Power {
890    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
891        self.0.fmt(f)?;
892        write!(f, " W")
893    }
894}
895
896type RadiansPerSecond = f64;
897
898/// Angular rate in radians per second.
899#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
900#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
901#[repr(transparent)]
902pub struct AngularRate(RadiansPerSecond);
903
904impl AngularRate {
905    /// Creates a new angular rate from an `f64` value in rad/s.
906    pub const fn new(rps: f64) -> Self {
907        Self(rps)
908    }
909
910    /// Creates a new angular rate from an `f64` value in rad/s.
911    pub const fn radians_per_second(rps: f64) -> Self {
912        Self(rps)
913    }
914
915    /// Creates a new angular rate from an `f64` value in deg/s.
916    pub const fn degrees_per_second(dps: f64) -> Self {
917        Self(dps.to_radians())
918    }
919
920    /// Returns the value in rad/s as an `f64`.
921    pub const fn as_f64(&self) -> f64 {
922        self.0
923    }
924
925    /// Returns the value in rad/s.
926    pub const fn to_radians_per_second(&self) -> f64 {
927        self.0
928    }
929
930    /// Returns the value in deg/s.
931    pub const fn to_degrees_per_second(&self) -> f64 {
932        self.0.to_degrees()
933    }
934}
935
936impl Display for AngularRate {
937    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
938        self.0.to_degrees().fmt(f)?;
939        write!(f, " deg/s")
940    }
941}
942
943type DecibelValue = f64;
944
945/// A value in decibels.
946#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
947#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
948#[repr(transparent)]
949pub struct Decibel(DecibelValue);
950
951impl Decibel {
952    /// Creates a new `Decibel` from a value already in dB.
953    pub const fn new(db: f64) -> Self {
954        Self(db)
955    }
956
957    /// Converts a linear power-ratio value to decibels.
958    pub fn from_linear(val: f64) -> Self {
959        Self(10.0 * val.log10())
960    }
961
962    /// Converts this decibel value to a linear power-ratio.
963    pub fn to_linear(self) -> f64 {
964        10.0_f64.powf(self.0 / 10.0)
965    }
966
967    /// Returns the raw `f64` value in dB.
968    pub const fn as_f64(self) -> f64 {
969        self.0
970    }
971}
972
973impl Display for Decibel {
974    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
975        self.0.fmt(f)?;
976        write!(f, " dB")
977    }
978}
979
980/// A trait for creating [`Decibel`] instances from primitives.
981///
982/// By default it is implemented for [`f64`] and [`i64`].
983///
984/// # Examples
985///
986/// ```
987/// use lox_core::units::DecibelUnits;
988///
989/// let d = 3.0.db();
990/// assert_eq!(d.as_f64(), 3.0);
991/// ```
992pub trait DecibelUnits {
993    /// Creates a decibel value.
994    fn db(&self) -> Decibel;
995}
996
997impl DecibelUnits for f64 {
998    fn db(&self) -> Decibel {
999        Decibel::new(*self)
1000    }
1001}
1002
1003impl DecibelUnits for i64 {
1004    fn db(&self) -> Decibel {
1005        Decibel::new(*self as f64)
1006    }
1007}
1008
1009macro_rules! trait_impls {
1010    ($($unit:ident),*) => {
1011        $(
1012            impl Neg for $unit {
1013                type Output = Self;
1014
1015                fn neg(self) -> Self::Output {
1016                    Self(-self.0)
1017                }
1018            }
1019
1020            impl Add for $unit {
1021                type Output = Self;
1022
1023                fn add(self, rhs: Self) -> Self::Output {
1024                    Self(self.0 + rhs.0)
1025                }
1026            }
1027
1028            impl AddAssign for $unit {
1029                fn add_assign(&mut self, rhs: Self) {
1030                    self.0 = self.0 + rhs.0;
1031                }
1032            }
1033
1034            impl Sub for $unit {
1035                type Output = Self;
1036
1037                fn sub(self, rhs: Self) -> Self::Output {
1038                    Self(self.0 - rhs.0)
1039                }
1040            }
1041
1042            impl SubAssign for $unit {
1043                fn sub_assign(&mut self, rhs: Self) {
1044                    self.0 = self.0 - rhs.0
1045                }
1046            }
1047
1048            impl Mul<$unit> for f64 {
1049                type Output = $unit;
1050
1051                fn mul(self, rhs: $unit) -> Self::Output {
1052                    $unit(self * rhs.0)
1053                }
1054            }
1055
1056            impl From<$unit> for f64 {
1057                fn from(val: $unit) -> Self {
1058                    val.0
1059                }
1060            }
1061        )*
1062    };
1063}
1064
1065trait_impls!(
1066    Angle,
1067    AngularRate,
1068    Decibel,
1069    Distance,
1070    Frequency,
1071    Power,
1072    Pressure,
1073    Temperature,
1074    Velocity
1075);
1076
1077#[cfg(test)]
1078mod tests {
1079    use alloc::format;
1080    use std::f64::consts::{FRAC_PI_2, PI};
1081
1082    use lox_test_utils::assert_approx_eq;
1083    use rstest::rstest;
1084
1085    extern crate alloc;
1086
1087    use super::*;
1088
1089    #[test]
1090    fn test_angle_deg() {
1091        let angle = 90.0.deg();
1092        assert_approx_eq!(angle.0, FRAC_PI_2, rtol <= 1e-10);
1093    }
1094
1095    #[test]
1096    fn test_angle_rad() {
1097        let angle = PI.rad();
1098        assert_approx_eq!(angle.0, PI, rtol <= 1e-10);
1099    }
1100
1101    #[test]
1102    fn test_angle_conversions() {
1103        let angle_deg = 180.0.deg();
1104        let angle_rad = PI.rad();
1105        assert_approx_eq!(angle_deg.0, angle_rad.0, rtol <= 1e-10);
1106    }
1107
1108    #[test]
1109    fn test_angle_display() {
1110        let angle = 90.123456.deg();
1111        assert_eq!(format!("{:.2}", angle), "90.12 deg")
1112    }
1113
1114    #[test]
1115    fn test_angle_neg() {
1116        assert_eq!(Angle(-1.0), -1.0.rad())
1117    }
1118
1119    const TOLERANCE: f64 = f64::EPSILON;
1120
1121    #[rstest]
1122    // Center 0.0 – expected range [-π, π).
1123    #[case(Angle::ZERO, Angle::ZERO, 0.0)]
1124    #[case(Angle::PI, Angle::ZERO, -PI)]
1125    #[case(-Angle::PI, Angle::ZERO, -PI)]
1126    #[case(Angle::TAU, Angle::ZERO, 0.0)]
1127    #[case(Angle::FRAC_PI_2, Angle::ZERO, FRAC_PI_2)]
1128    #[case(-Angle::FRAC_PI_2, Angle::ZERO, -FRAC_PI_2)]
1129    // Center π – expected range [0, 2π).
1130    #[case(Angle::ZERO, Angle::PI, 0.0)]
1131    #[case(Angle::PI, Angle::PI, PI)]
1132    #[case(-Angle::PI, Angle::PI, PI)]
1133    #[case(Angle::TAU, Angle::PI, 0.0)]
1134    #[case(Angle::FRAC_PI_2, Angle::PI, FRAC_PI_2)]
1135    #[case(-Angle::FRAC_PI_2, Angle::PI, 3.0 * PI / 2.0)]
1136    // Center -π – expected range [-2π, 0).
1137    #[case(Angle::ZERO, -Angle::PI, -TAU)]
1138    #[case(Angle::PI, -Angle::PI, -PI)]
1139    #[case(-Angle::PI, -Angle::PI, -PI)]
1140    #[case(Angle::TAU, -Angle::PI, -TAU)]
1141    #[case(Angle::FRAC_PI_2, -Angle::PI, -3.0 * PI / 2.0)]
1142    #[case(-Angle::FRAC_PI_2, -Angle::PI, -FRAC_PI_2)]
1143    fn test_angle_normalize_two_pi(#[case] angle: Angle, #[case] center: Angle, #[case] exp: f64) {
1144        // atol is preferred to rtol for floating-point comparisons with 0.0. See
1145        // https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/#inferna
1146        if exp == 0.0 {
1147            assert_approx_eq!(angle.normalize_two_pi(center).0, exp, atol <= TOLERANCE);
1148        } else {
1149            assert_approx_eq!(angle.normalize_two_pi(center).0, exp, rtol <= TOLERANCE);
1150        }
1151    }
1152
1153    #[test]
1154    fn test_distance_m() {
1155        let distance = 1000.0.m();
1156        assert_eq!(distance.0, 1000.0);
1157    }
1158
1159    #[test]
1160    fn test_distance_km() {
1161        let distance = 1.0.km();
1162        assert_eq!(distance.0, 1000.0);
1163    }
1164
1165    #[test]
1166    fn test_distance_au() {
1167        let distance = 1.0.au();
1168        assert_eq!(distance.0, ASTRONOMICAL_UNIT);
1169    }
1170
1171    #[test]
1172    fn test_distance_conversions() {
1173        let d1 = 1.5e11.m();
1174        let d2 = (1.5e11 / ASTRONOMICAL_UNIT).au();
1175        assert_approx_eq!(d1.0, d2.0, rtol <= 1e-9);
1176    }
1177
1178    #[test]
1179    fn test_distance_display() {
1180        let distance = 9.123456.km();
1181        assert_eq!(format!("{:.2}", distance), "9.12 km")
1182    }
1183
1184    #[test]
1185    fn test_distance_neg() {
1186        assert_eq!(Distance(-1.0), -1.0.m())
1187    }
1188
1189    #[test]
1190    fn test_velocity_mps() {
1191        let velocity = 1000.0.mps();
1192        assert_eq!(velocity.0, 1000.0);
1193    }
1194
1195    #[test]
1196    fn test_velocity_kps() {
1197        let velocity = 1.0.kps();
1198        assert_eq!(velocity.0, 1000.0);
1199    }
1200
1201    #[test]
1202    fn test_velocity_conversions() {
1203        let v1 = 7500.0.mps();
1204        let v2 = 7.5.kps();
1205        assert_eq!(v1.0, v2.0);
1206    }
1207
1208    #[test]
1209    fn test_velocity_display() {
1210        let velocity = 9.123456.kps();
1211        assert_eq!(format!("{:.2}", velocity), "9.12 km/s")
1212    }
1213
1214    #[test]
1215    fn test_velocity_neg() {
1216        assert_eq!(Velocity(-1.0), -1.0.mps())
1217    }
1218
1219    #[test]
1220    fn test_frequency_hz() {
1221        let frequency = 1000.0.hz();
1222        assert_eq!(frequency.0, 1000.0);
1223    }
1224
1225    #[test]
1226    fn test_frequency_khz() {
1227        let frequency = 1.0.khz();
1228        assert_eq!(frequency.0, 1000.0);
1229    }
1230
1231    #[test]
1232    fn test_frequency_mhz() {
1233        let frequency = 1.0.mhz();
1234        assert_eq!(frequency.0, 1_000_000.0);
1235    }
1236
1237    #[test]
1238    fn test_frequency_ghz() {
1239        let frequency = 1.0.ghz();
1240        assert_eq!(frequency.0, 1_000_000_000.0);
1241    }
1242
1243    #[test]
1244    fn test_frequency_thz() {
1245        let frequency = 1.0.thz();
1246        assert_eq!(frequency.0, 1_000_000_000_000.0);
1247    }
1248
1249    #[test]
1250    fn test_frequency_conversions() {
1251        let f1 = 2.4.ghz();
1252        let f2 = 2400.0.mhz();
1253        assert_eq!(f1.0, f2.0);
1254    }
1255
1256    #[test]
1257    fn test_frequency_wavelength() {
1258        let f = 1.0.ghz();
1259        let wavelength = f.wavelength();
1260        assert_approx_eq!(wavelength.0, 0.299792458, rtol <= 1e-9);
1261    }
1262
1263    #[test]
1264    fn test_frequency_wavelength_speed_of_light() {
1265        let f = 299792458.0.hz(); // 1 Hz at speed of light
1266        let wavelength = f.wavelength();
1267        assert_approx_eq!(wavelength.0, 1.0, rtol <= 1e-10);
1268    }
1269
1270    #[test]
1271    fn test_frequency_display() {
1272        let frequency = 2.4123456.ghz();
1273        assert_eq!(format!("{:.2}", frequency), "2.41 GHz");
1274    }
1275
1276    #[rstest]
1277    #[case(0.0.hz(), None)]
1278    #[case(3.0.mhz(), Some(FrequencyBand::HF))]
1279    #[case(30.0.mhz(), Some(FrequencyBand::VHF))]
1280    #[case(300.0.mhz(), Some(FrequencyBand::UHF))]
1281    #[case(1.0.ghz(), Some(FrequencyBand::L))]
1282    #[case(2.0.ghz(), Some(FrequencyBand::S))]
1283    #[case(4.0.ghz(), Some(FrequencyBand::C))]
1284    #[case(8.0.ghz(), Some(FrequencyBand::X))]
1285    #[case(12.0.ghz(), Some(FrequencyBand::Ku))]
1286    #[case(18.0.ghz(), Some(FrequencyBand::K))]
1287    #[case(27.0.ghz(), Some(FrequencyBand::Ka))]
1288    #[case(40.0.ghz(), Some(FrequencyBand::V))]
1289    #[case(75.0.ghz(), Some(FrequencyBand::W))]
1290    #[case(110.0.ghz(), Some(FrequencyBand::G))]
1291    #[case(1.0.thz(), None)]
1292    fn test_frequency_band(#[case] f: Frequency, #[case] exp: Option<FrequencyBand>) {
1293        assert_eq!(f.band(), exp)
1294    }
1295
1296    #[test]
1297    fn test_decibel_db() {
1298        let d = 3.0.db();
1299        assert_eq!(d.as_f64(), 3.0);
1300    }
1301
1302    #[test]
1303    fn test_decibel_from_linear() {
1304        let d = Decibel::from_linear(100.0);
1305        assert_approx_eq!(d.0, 20.0, rtol <= 1e-10);
1306    }
1307
1308    #[test]
1309    fn test_decibel_to_linear() {
1310        let d = Decibel::new(20.0);
1311        assert_approx_eq!(d.to_linear(), 100.0, rtol <= 1e-10);
1312    }
1313
1314    #[test]
1315    fn test_decibel_roundtrip() {
1316        let val = 42.5;
1317        let d = Decibel::new(val);
1318        let roundtripped = Decibel::from_linear(d.to_linear());
1319        assert_approx_eq!(roundtripped.0, val, rtol <= 1e-10);
1320    }
1321
1322    #[test]
1323    fn test_decibel_add() {
1324        let sum = 3.0.db() + 3.0.db();
1325        assert_approx_eq!(sum.0, 6.0, rtol <= 1e-10);
1326    }
1327
1328    #[test]
1329    fn test_decibel_sub() {
1330        let diff = 6.0.db() - 3.0.db();
1331        assert_approx_eq!(diff.0, 3.0, rtol <= 1e-10);
1332    }
1333
1334    #[test]
1335    fn test_decibel_neg() {
1336        assert_eq!(-3.0.db(), Decibel::new(-3.0));
1337    }
1338
1339    #[test]
1340    fn test_decibel_display() {
1341        let d = 3.0.db();
1342        assert_eq!(format!("{:.1}", d), "3.0 dB");
1343    }
1344
1345    // --- Temperature ---
1346
1347    #[test]
1348    fn test_temperature_new() {
1349        let t = Temperature::new(290.0);
1350        assert_eq!(t.as_f64(), 290.0);
1351    }
1352
1353    #[test]
1354    fn test_temperature_kelvin() {
1355        let t = Temperature::kelvin(300.0);
1356        assert_eq!(t.to_kelvin(), 300.0);
1357    }
1358
1359    #[test]
1360    fn test_temperature_display() {
1361        let t = Temperature::new(290.0);
1362        assert_eq!(format!("{}", t), "290 K");
1363    }
1364
1365    #[test]
1366    fn test_temperature_arithmetic() {
1367        let a = Temperature::new(100.0);
1368        let b = Temperature::new(200.0);
1369        assert_eq!((a + b).as_f64(), 300.0);
1370        assert_eq!((b - a).as_f64(), 100.0);
1371        assert_eq!((-a).as_f64(), -100.0);
1372        assert_eq!((2.0 * a).as_f64(), 200.0);
1373    }
1374
1375    // --- Power ---
1376
1377    #[test]
1378    fn test_power_watts() {
1379        let p = Power::watts(100.0);
1380        assert_eq!(p.to_watts(), 100.0);
1381    }
1382
1383    #[test]
1384    fn test_power_kilowatts() {
1385        let p = Power::kilowatts(1.0);
1386        assert_eq!(p.to_watts(), 1000.0);
1387        assert_eq!(p.to_kilowatts(), 1.0);
1388    }
1389
1390    #[test]
1391    fn test_power_dbw() {
1392        let p = Power::watts(100.0);
1393        assert_approx_eq!(p.to_dbw(), 20.0, rtol <= 1e-10);
1394    }
1395
1396    #[test]
1397    fn test_power_display() {
1398        let p = Power::watts(100.0);
1399        assert_eq!(format!("{}", p), "100 W");
1400    }
1401
1402    #[test]
1403    fn test_power_arithmetic() {
1404        let a = Power::watts(50.0);
1405        let b = Power::watts(150.0);
1406        assert_eq!((a + b).as_f64(), 200.0);
1407        assert_eq!((b - a).as_f64(), 100.0);
1408        assert_eq!((-a).as_f64(), -50.0);
1409    }
1410
1411    // --- AngularRate ---
1412
1413    #[test]
1414    fn test_angular_rate_rps() {
1415        let ar = AngularRate::radians_per_second(1.0);
1416        assert_eq!(ar.to_radians_per_second(), 1.0);
1417        assert_approx_eq!(ar.to_degrees_per_second(), 57.29577951308232, rtol <= 1e-10);
1418    }
1419
1420    #[test]
1421    fn test_angular_rate_dps() {
1422        let ar = AngularRate::degrees_per_second(180.0);
1423        assert_approx_eq!(
1424            ar.to_radians_per_second(),
1425            core::f64::consts::PI,
1426            rtol <= 1e-10
1427        );
1428    }
1429
1430    #[test]
1431    fn test_angular_rate_display() {
1432        let ar = AngularRate::radians_per_second(1.0);
1433        let s = format!("{}", ar);
1434        assert!(s.contains("deg/s"));
1435    }
1436
1437    #[test]
1438    fn test_angular_rate_arithmetic() {
1439        let a = AngularRate::new(1.0);
1440        let b = AngularRate::new(2.0);
1441        assert_eq!((a + b).as_f64(), 3.0);
1442        assert_eq!((b - a).as_f64(), 1.0);
1443        assert_eq!((-a).as_f64(), -1.0);
1444        assert_eq!((3.0 * a).as_f64(), 3.0);
1445    }
1446
1447    // --- Pressure ---
1448
1449    #[test]
1450    fn test_pressure_hpa() {
1451        let p = Pressure::hpa(1013.25);
1452        assert_eq!(p.to_hpa(), 1013.25);
1453        assert_approx_eq!(p.to_pa(), 101325.0, rtol <= 1e-10);
1454    }
1455
1456    #[test]
1457    fn test_pressure_pa() {
1458        let p = Pressure::pa(101325.0);
1459        assert_approx_eq!(p.to_hpa(), 1013.25, rtol <= 1e-10);
1460    }
1461
1462    #[test]
1463    fn test_pressure_display() {
1464        let p = Pressure::pa(101325.0);
1465        let s = format!("{}", p);
1466        assert!(s.contains("Pa"));
1467    }
1468}