Skip to main content

rfham_core/
non_si.rs

1//! Unit conversion helpers for ham-radio measurements.
2//!
3//! [`LengthInFeet`] stores a length as a (feet, inches) pair and displays it using prime /
4//! double-prime notation (`′` / `″`). The alternate formatter (`{:#}`) renders fractional
5//! inches as a rational number where possible (e.g. `8 1/4″` instead of `8.25″`).
6//!
7//! # Examples
8//!
9//! ```rust
10//! use rfham_core::conversions::LengthInFeet;
11//!
12//! let l = LengthInFeet::new(2.6875); // 2 feet 8.25 inches
13//! assert_eq!(l.to_string(),    "2′ 8.25″");
14//! assert_eq!(format!("{l:#}"), "2′ 8 1/4″");
15//! ```
16
17use num_rational::Rational32;
18use num_traits::{
19    ConstZero,
20    cast::{FromPrimitive, ToPrimitive},
21};
22use std::fmt::Display;
23
24#[derive(Clone, Copy, Debug, Default, PartialEq)]
25pub struct LengthInFeet {
26    feet: u32,
27    inches: f32,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub enum Units {
32    #[default]
33    Metric,
34    Imperial,
35}
36
37impl Display for LengthInFeet {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(
40            f,
41            "{}",
42            if f.alternate() {
43                if let Some((whole, fractional)) = self.inches_only_fractional() {
44                    format!(
45                        "{}′{}",
46                        self.feet,
47                        match (whole, fractional) {
48                            (0, z) if z == Rational32::ZERO => String::default(),
49                            (whole, z) if z == Rational32::ZERO => format!(" {whole}″"),
50                            (0, fractional) => format!(" {fractional}″"),
51                            (whole, fractional) => format!(" {whole} {fractional}″"),
52                        }
53                    )
54                } else {
55                    format!("{}′ {}″", self.feet, self.inches)
56                }
57            } else {
58                format!(
59                    "{}′{}",
60                    self.feet,
61                    if self.inches != f32::ZERO {
62                        format!(" {}″", self.inches)
63                    } else {
64                        String::default()
65                    }
66                )
67            }
68        )
69    }
70}
71
72impl PartialOrd for LengthInFeet {
73    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
74        match self.feet.partial_cmp(&other.feet) {
75            Some(core::cmp::Ordering::Equal) => {}
76            ord => return ord,
77        }
78        self.inches.partial_cmp(&other.inches)
79    }
80}
81
82impl LengthInFeet {
83    pub fn new(feet: f64) -> Self {
84        let uint_feet = feet.floor() as u32;
85        let float_inches = feet.fract() as f32;
86        Self::feet_and_inches(uint_feet, float_inches * 12.0)
87    }
88
89    pub fn inches(inches: f64) -> Self {
90        let feet = (inches / 12.0) as u32;
91        let float_inches = inches.rem_euclid(12.0) as f32;
92        Self::feet_and_inches(feet, float_inches)
93    }
94
95    pub fn feet_and_inches(feet: u32, inches: f32) -> Self {
96        Self { feet, inches }
97    }
98    pub fn feet_and_inches_fractional(
99        feet: u32,
100        inches: u32,
101        fractional: Rational32,
102    ) -> Option<Self> {
103        (fractional + Rational32::from_u32(inches).unwrap())
104            .to_f32()
105            .map(|inches| Self::feet_and_inches(feet, inches))
106    }
107
108    pub fn feet_only(&self) -> u32 {
109        self.feet
110    }
111
112    pub fn inches_only_decimal(&self) -> f32 {
113        self.inches
114    }
115
116    pub fn inches_only_fractional(&self) -> Option<(u32, Rational32)> {
117        if let Some(fractional) = Rational32::from_f32(self.inches.fract()) {
118            let uint_inches = self.inches.floor() as u32;
119            Some((uint_inches, fractional))
120        } else {
121            None
122        }
123    }
124}
125
126#[cfg(test)]
127mod test {
128    use crate::non_si::LengthInFeet;
129
130    #[test]
131    fn test_construct_feet() {
132        assert_eq!(
133            LengthInFeet::new(0.0),
134            LengthInFeet {
135                feet: 0,
136                inches: 0.0
137            }
138        );
139        assert_eq!(LengthInFeet::new(0.0).to_string(), "0′".to_string());
140        assert_eq!(format!("{:#}", LengthInFeet::new(0.0)), "0′".to_string());
141
142        assert_eq!(
143            LengthInFeet::new(1.5),
144            LengthInFeet {
145                feet: 1,
146                inches: 6.0
147            }
148        );
149        assert_eq!(LengthInFeet::new(1.5).to_string(), "1′ 6″".to_string());
150        assert_eq!(format!("{:#}", LengthInFeet::new(1.5)), "1′ 6″".to_string());
151
152        assert_eq!(
153            LengthInFeet::new(2.55),
154            LengthInFeet {
155                feet: 2,
156                inches: 6.6000004
157            }
158        );
159        assert_eq!(
160            LengthInFeet::new(2.6875).to_string(),
161            "2′ 8.25″".to_string()
162        );
163        assert_eq!(
164            format!("{:#}", LengthInFeet::new(2.6875)),
165            "2′ 8 1/4″".to_string()
166        );
167    }
168}