rfham-core 0.1.1

Core data types for RF-Ham libraries.
Documentation
//! Unit conversion helpers for ham-radio measurements.
//!
//! [`LengthInFeet`] stores a length as a (feet, inches) pair and displays it using prime /
//! double-prime notation (`′` / `″`). The alternate formatter (`{:#}`) renders fractional
//! inches as a rational number where possible (e.g. `8 1/4″` instead of `8.25″`).
//!
//! # Examples
//!
//! ```rust
//! use rfham_core::conversions::LengthInFeet;
//!
//! let l = LengthInFeet::new(2.6875); // 2 feet 8.25 inches
//! assert_eq!(l.to_string(),    "2′ 8.25″");
//! assert_eq!(format!("{l:#}"), "2′ 8 1/4″");
//! ```

use num_rational::Rational32;
use num_traits::{
    ConstZero,
    cast::{FromPrimitive, ToPrimitive},
};
use std::fmt::Display;

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LengthInFeet {
    feet: u32,
    inches: f32,
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Units {
    #[default]
    Metric,
    Imperial,
}

impl Display for LengthInFeet {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            if f.alternate() {
                if let Some((whole, fractional)) = self.inches_only_fractional() {
                    format!(
                        "{}{}",
                        self.feet,
                        match (whole, fractional) {
                            (0, z) if z == Rational32::ZERO => String::default(),
                            (whole, z) if z == Rational32::ZERO => format!(" {whole}"),
                            (0, fractional) => format!(" {fractional}"),
                            (whole, fractional) => format!(" {whole} {fractional}"),
                        }
                    )
                } else {
                    format!("{}{}", self.feet, self.inches)
                }
            } else {
                format!(
                    "{}{}",
                    self.feet,
                    if self.inches != f32::ZERO {
                        format!(" {}", self.inches)
                    } else {
                        String::default()
                    }
                )
            }
        )
    }
}

impl PartialOrd for LengthInFeet {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        match self.feet.partial_cmp(&other.feet) {
            Some(core::cmp::Ordering::Equal) => {}
            ord => return ord,
        }
        self.inches.partial_cmp(&other.inches)
    }
}

impl LengthInFeet {
    pub fn new(feet: f64) -> Self {
        let uint_feet = feet.floor() as u32;
        let float_inches = feet.fract() as f32;
        Self::feet_and_inches(uint_feet, float_inches * 12.0)
    }

    pub fn inches(inches: f64) -> Self {
        let feet = (inches / 12.0) as u32;
        let float_inches = inches.rem_euclid(12.0) as f32;
        Self::feet_and_inches(feet, float_inches)
    }

    pub fn feet_and_inches(feet: u32, inches: f32) -> Self {
        Self { feet, inches }
    }
    pub fn feet_and_inches_fractional(
        feet: u32,
        inches: u32,
        fractional: Rational32,
    ) -> Option<Self> {
        (fractional + Rational32::from_u32(inches).unwrap())
            .to_f32()
            .map(|inches| Self::feet_and_inches(feet, inches))
    }

    pub fn feet_only(&self) -> u32 {
        self.feet
    }

    pub fn inches_only_decimal(&self) -> f32 {
        self.inches
    }

    pub fn inches_only_fractional(&self) -> Option<(u32, Rational32)> {
        if let Some(fractional) = Rational32::from_f32(self.inches.fract()) {
            let uint_inches = self.inches.floor() as u32;
            Some((uint_inches, fractional))
        } else {
            None
        }
    }
}

#[cfg(test)]
mod test {
    use crate::non_si::LengthInFeet;

    #[test]
    fn test_construct_feet() {
        assert_eq!(
            LengthInFeet::new(0.0),
            LengthInFeet {
                feet: 0,
                inches: 0.0
            }
        );
        assert_eq!(LengthInFeet::new(0.0).to_string(), "0′".to_string());
        assert_eq!(format!("{:#}", LengthInFeet::new(0.0)), "0′".to_string());

        assert_eq!(
            LengthInFeet::new(1.5),
            LengthInFeet {
                feet: 1,
                inches: 6.0
            }
        );
        assert_eq!(LengthInFeet::new(1.5).to_string(), "1′ 6″".to_string());
        assert_eq!(format!("{:#}", LengthInFeet::new(1.5)), "1′ 6″".to_string());

        assert_eq!(
            LengthInFeet::new(2.55),
            LengthInFeet {
                feet: 2,
                inches: 6.6000004
            }
        );
        assert_eq!(
            LengthInFeet::new(2.6875).to_string(),
            "2′ 8.25″".to_string()
        );
        assert_eq!(
            format!("{:#}", LengthInFeet::new(2.6875)),
            "2′ 8 1/4″".to_string()
        );
    }
}