const UM_PER_FOOT: i64 = 304_800;
const UM_PER_METER: i64 = 1_000_000;
const UM_PER_KILOMETER: i64 = 1_000_000_000;
const UM_PER_NAUTICAL_MILE: i64 = 1_852_000_000;
const UM_PER_STATUTE_MILE: i64 = 1_609_344_000;
const UM_PER_INCH: i64 = 25_400;
const UM_PER_HUNDREDTH_INCH: i64 = 254;
const UM_PER_MILLIMETER: i64 = 1_000;
const MMH_PER_KNOT: i64 = 1_852_000;
const MMH_PER_MPH: i64 = 1_609_344;
const MMH_PER_KMH: i64 = 1_000_000;
const MMH_PER_MS: i64 = 3_600_000;
const MMH_PER_MMS: i64 = 3_600;
const UNITS_PER_CELSIUS: i64 = 45_000;
const UNITS_PER_FAHRENHEIT: i64 = 25_000;
const UNITS_PER_MILLICELSIUS: i64 = 45;
const FAHRENHEIT_OFFSET: i64 = 32;
const MPA_PER_PASCAL: i64 = 1_000;
const MPA_PER_HPA: i64 = 100_000;
const MPA_PER_TENTH_HPA: i64 = 10_000;
const MPA_PER_INHG: i64 = 3_386_389;
const UW_PER_WATT: i64 = 1_000_000;
const UW_PER_MILLIWATT: i64 = 1_000;
const DBM_MANTISSA: [i64; 10] = [
1_000_000, 1_258_925, 1_584_893, 1_995_262, 2_511_886, 3_162_278, 3_981_072, 5_011_872,
6_309_573, 7_943_282,
];
const POW10: [i64; 19] = [
1,
10,
100,
1_000,
10_000,
100_000,
1_000_000,
10_000_000,
100_000_000,
1_000_000_000,
10_000_000_000,
100_000_000_000,
1_000_000_000_000,
10_000_000_000_000,
100_000_000_000_000,
1_000_000_000_000_000,
10_000_000_000_000_000,
100_000_000_000_000_000,
1_000_000_000_000_000_000,
];
const DBM_MIN: i32 = -30;
const DBM_MAX: i32 = 160;
const fn div_round(value: i64, divisor: i64) -> i64 {
let half = divisor / 2;
if value >= 0 {
value.saturating_add(half) / divisor
} else {
value.saturating_sub(half) / divisor
}
}
const fn narrow(value: i64) -> i32 {
if value > i32::MAX as i64 {
i32::MAX
} else if value < i32::MIN as i64 {
i32::MIN
} else {
value as i32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnitError {
BadBearing {
got: u16,
},
BadHumidity {
got: u8,
},
}
impl core::fmt::Display for UnitError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::BadBearing { got } => {
write!(f, "bearing {got} is outside 0..=360 degrees")
}
Self::BadHumidity { got } => {
write!(f, "relative humidity {got} is outside 1..=100 percent")
}
}
}
}
impl core::error::Error for UnitError {}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Distance {
micrometers: i64,
}
impl Distance {
pub const ZERO: Self = Self { micrometers: 0 };
#[must_use]
pub const fn from_feet(feet: i32) -> Self {
Self {
micrometers: (feet as i64).saturating_mul(UM_PER_FOOT),
}
}
#[must_use]
pub const fn from_meters(meters: i32) -> Self {
Self {
micrometers: (meters as i64).saturating_mul(UM_PER_METER),
}
}
#[must_use]
pub const fn from_millimeters(millimeters: i32) -> Self {
Self {
micrometers: (millimeters as i64).saturating_mul(UM_PER_MILLIMETER),
}
}
#[must_use]
pub const fn from_kilometers(kilometers: i32) -> Self {
Self {
micrometers: (kilometers as i64).saturating_mul(UM_PER_KILOMETER),
}
}
#[must_use]
pub const fn from_nautical_miles(nautical_miles: i32) -> Self {
Self {
micrometers: (nautical_miles as i64).saturating_mul(UM_PER_NAUTICAL_MILE),
}
}
#[must_use]
pub const fn from_statute_miles(statute_miles: i32) -> Self {
Self {
micrometers: (statute_miles as i64).saturating_mul(UM_PER_STATUTE_MILE),
}
}
#[must_use]
pub const fn from_inches(inches: i32) -> Self {
Self {
micrometers: (inches as i64).saturating_mul(UM_PER_INCH),
}
}
#[must_use]
pub const fn from_micrometers(micrometers: i64) -> Self {
Self { micrometers }
}
#[must_use]
pub const fn feet(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_FOOT))
}
#[must_use]
pub const fn meters(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_METER))
}
#[must_use]
pub const fn millimeters(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_MILLIMETER))
}
#[must_use]
pub const fn kilometers(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_KILOMETER))
}
#[must_use]
pub const fn nautical_miles(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_NAUTICAL_MILE))
}
#[must_use]
pub const fn statute_miles(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_STATUTE_MILE))
}
#[must_use]
pub const fn inches(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_INCH))
}
#[must_use]
pub const fn micrometers(self) -> i64 {
self.micrometers
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Speed {
millimeters_per_hour: i64,
}
impl Speed {
pub const ZERO: Self = Self {
millimeters_per_hour: 0,
};
#[must_use]
pub const fn from_knots(knots: i32) -> Self {
Self {
millimeters_per_hour: (knots as i64).saturating_mul(MMH_PER_KNOT),
}
}
#[must_use]
pub const fn from_mph(mph: i32) -> Self {
Self {
millimeters_per_hour: (mph as i64).saturating_mul(MMH_PER_MPH),
}
}
#[must_use]
pub const fn from_kmh(kmh: i32) -> Self {
Self {
millimeters_per_hour: (kmh as i64).saturating_mul(MMH_PER_KMH),
}
}
#[must_use]
pub const fn from_meters_per_second(meters_per_second: i32) -> Self {
Self {
millimeters_per_hour: (meters_per_second as i64).saturating_mul(MMH_PER_MS),
}
}
#[must_use]
pub const fn from_millimeters_per_second(millimeters_per_second: i64) -> Self {
Self {
millimeters_per_hour: millimeters_per_second.saturating_mul(MMH_PER_MMS),
}
}
#[must_use]
pub const fn from_millimeters_per_hour(millimeters_per_hour: i64) -> Self {
Self {
millimeters_per_hour,
}
}
#[must_use]
pub const fn knots(self) -> i32 {
narrow(div_round(self.millimeters_per_hour, MMH_PER_KNOT))
}
#[must_use]
pub const fn mph(self) -> i32 {
narrow(div_round(self.millimeters_per_hour, MMH_PER_MPH))
}
#[must_use]
pub const fn kmh(self) -> i32 {
narrow(div_round(self.millimeters_per_hour, MMH_PER_KMH))
}
#[must_use]
pub const fn meters_per_second(self) -> i32 {
narrow(div_round(self.millimeters_per_hour, MMH_PER_MS))
}
#[must_use]
pub const fn millimeters_per_second(self) -> i64 {
div_round(self.millimeters_per_hour, MMH_PER_MMS)
}
#[must_use]
pub const fn millimeters_per_hour(self) -> i64 {
self.millimeters_per_hour
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Rainfall {
micrometers: i64,
}
impl Rainfall {
pub const ZERO: Self = Self { micrometers: 0 };
#[must_use]
pub const fn from_hundredths_inch(hundredths: i32) -> Self {
Self {
micrometers: (hundredths as i64).saturating_mul(UM_PER_HUNDREDTH_INCH),
}
}
#[must_use]
pub const fn from_millimeters(millimeters: i32) -> Self {
Self {
micrometers: (millimeters as i64).saturating_mul(UM_PER_MILLIMETER),
}
}
#[must_use]
pub const fn from_micrometers(micrometers: i64) -> Self {
Self { micrometers }
}
#[must_use]
pub const fn hundredths_inch(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_HUNDREDTH_INCH))
}
#[must_use]
pub const fn millimeters(self) -> i32 {
narrow(div_round(self.micrometers, UM_PER_MILLIMETER))
}
#[must_use]
pub const fn micrometers(self) -> i64 {
self.micrometers
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Temperature {
units: i64,
}
impl Temperature {
pub const FREEZING: Self = Self { units: 0 };
#[must_use]
pub const fn from_celsius(celsius: i32) -> Self {
Self {
units: (celsius as i64).saturating_mul(UNITS_PER_CELSIUS),
}
}
#[must_use]
pub const fn from_fahrenheit(fahrenheit: i32) -> Self {
Self {
units: (fahrenheit as i64)
.saturating_sub(FAHRENHEIT_OFFSET)
.saturating_mul(UNITS_PER_FAHRENHEIT),
}
}
#[must_use]
pub const fn from_millidegrees_celsius(millidegrees: i32) -> Self {
Self {
units: (millidegrees as i64).saturating_mul(UNITS_PER_MILLICELSIUS),
}
}
#[must_use]
pub const fn from_tenths_fahrenheit(tenths: i32) -> Self {
Self {
units: (tenths as i64)
.saturating_sub(FAHRENHEIT_OFFSET * 10)
.saturating_mul(UNITS_PER_FAHRENHEIT / 10),
}
}
#[must_use]
pub const fn celsius(self) -> i32 {
narrow(div_round(self.units, UNITS_PER_CELSIUS))
}
#[must_use]
pub const fn fahrenheit(self) -> i32 {
narrow(div_round(
self.units
.saturating_add(FAHRENHEIT_OFFSET * UNITS_PER_FAHRENHEIT),
UNITS_PER_FAHRENHEIT,
))
}
#[must_use]
pub const fn millidegrees_celsius(self) -> i32 {
narrow(div_round(self.units, UNITS_PER_MILLICELSIUS))
}
#[must_use]
pub const fn tenths_fahrenheit(self) -> i32 {
narrow(div_round(
self.units
.saturating_add(FAHRENHEIT_OFFSET * UNITS_PER_FAHRENHEIT),
UNITS_PER_FAHRENHEIT / 10,
))
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Pressure {
millipascals: i64,
}
impl Pressure {
pub const ZERO: Self = Self { millipascals: 0 };
#[must_use]
pub const fn from_pascals(pascals: i32) -> Self {
Self {
millipascals: (pascals as i64).saturating_mul(MPA_PER_PASCAL),
}
}
#[must_use]
pub const fn from_hpa(hpa: i32) -> Self {
Self {
millipascals: (hpa as i64).saturating_mul(MPA_PER_HPA),
}
}
#[must_use]
pub const fn from_tenths_hpa(tenths: i32) -> Self {
Self {
millipascals: (tenths as i64).saturating_mul(MPA_PER_TENTH_HPA),
}
}
#[must_use]
pub const fn from_hundredths_inhg(hundredths: i32) -> Self {
Self {
millipascals: div_round((hundredths as i64).saturating_mul(MPA_PER_INHG), 100),
}
}
#[must_use]
pub const fn from_millipascals(millipascals: i64) -> Self {
Self { millipascals }
}
#[must_use]
pub const fn pascals(self) -> i32 {
narrow(div_round(self.millipascals, MPA_PER_PASCAL))
}
#[must_use]
pub const fn hpa(self) -> i32 {
narrow(div_round(self.millipascals, MPA_PER_HPA))
}
#[must_use]
pub const fn tenths_hpa(self) -> i32 {
narrow(div_round(self.millipascals, MPA_PER_TENTH_HPA))
}
#[must_use]
pub const fn hundredths_inhg(self) -> i32 {
narrow(div_round(
self.millipascals.saturating_mul(100),
MPA_PER_INHG,
))
}
#[must_use]
pub const fn millipascals(self) -> i64 {
self.millipascals
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Power {
microwatts: i64,
}
impl Power {
pub const ZERO: Self = Self { microwatts: 0 };
#[must_use]
pub const fn from_watts(watts: i32) -> Self {
Self {
microwatts: (watts as i64).saturating_mul(UW_PER_WATT),
}
}
#[must_use]
pub const fn from_milliwatts(milliwatts: i32) -> Self {
Self {
microwatts: (milliwatts as i64).saturating_mul(UW_PER_MILLIWATT),
}
}
#[must_use]
pub const fn from_microwatts(microwatts: i64) -> Self {
Self { microwatts }
}
#[must_use]
pub const fn from_dbm(dbm: i32) -> Self {
let shifted = (dbm as i64).saturating_add(30);
let decade = shifted.div_euclid(10);
let mantissa = DBM_MANTISSA[shifted.rem_euclid(10) as usize];
let microwatts = if decade >= 6 {
let shift = decade - 6;
if shift as usize >= POW10.len() {
i64::MAX
} else {
mantissa.saturating_mul(POW10[shift as usize])
}
} else {
let shift = 6 - decade;
if shift as usize >= POW10.len() {
0
} else {
div_round(mantissa, POW10[shift as usize])
}
};
Self { microwatts }
}
#[must_use]
pub const fn dbm(self) -> Option<i32> {
if self.microwatts <= 0 {
return None;
}
let squared = (self.microwatts as i128) * (self.microwatts as i128);
let mut candidate = DBM_MIN;
let mut power = Self::from_dbm(candidate).microwatts as i128;
while candidate < DBM_MAX {
let next = Self::from_dbm(candidate + 1).microwatts as i128;
if squared <= power * next {
return Some(candidate);
}
candidate += 1;
power = next;
}
Some(DBM_MAX)
}
#[must_use]
pub const fn watts(self) -> i32 {
narrow(div_round(self.microwatts, UW_PER_WATT))
}
#[must_use]
pub const fn milliwatts(self) -> i32 {
narrow(div_round(self.microwatts, UW_PER_MILLIWATT))
}
#[must_use]
pub const fn microwatts(self) -> i64 {
self.microwatts
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Bearing {
degrees: u16,
}
impl Bearing {
pub const NORTH: Self = Self { degrees: 0 };
pub const fn new(degrees: u16) -> Result<Self, UnitError> {
if degrees > 360 {
Err(UnitError::BadBearing { got: degrees })
} else if degrees == 360 {
Ok(Self { degrees: 0 })
} else {
Ok(Self { degrees })
}
}
#[must_use]
pub const fn degrees(self) -> u16 {
self.degrees
}
#[must_use]
pub const fn compass_point(self) -> CompassPoint {
let index = ((self.degrees as u32 * 16 + 180) / 360) % 16;
CompassPoint::ALL[index as usize]
}
#[must_use]
pub const fn reciprocal(self) -> Self {
Self {
degrees: (self.degrees + 180) % 360,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub enum CompassPoint {
#[default]
North,
NorthNortheast,
Northeast,
EastNortheast,
East,
EastSoutheast,
Southeast,
SouthSoutheast,
South,
SouthSouthwest,
Southwest,
WestSouthwest,
West,
WestNorthwest,
Northwest,
NorthNorthwest,
}
impl CompassPoint {
pub const ALL: [Self; 16] = [
Self::North,
Self::NorthNortheast,
Self::Northeast,
Self::EastNortheast,
Self::East,
Self::EastSoutheast,
Self::Southeast,
Self::SouthSoutheast,
Self::South,
Self::SouthSouthwest,
Self::Southwest,
Self::WestSouthwest,
Self::West,
Self::WestNorthwest,
Self::Northwest,
Self::NorthNorthwest,
];
#[must_use]
pub const fn abbreviation(self) -> &'static str {
match self {
Self::North => "N",
Self::NorthNortheast => "NNE",
Self::Northeast => "NE",
Self::EastNortheast => "ENE",
Self::East => "E",
Self::EastSoutheast => "ESE",
Self::Southeast => "SE",
Self::SouthSoutheast => "SSE",
Self::South => "S",
Self::SouthSouthwest => "SSW",
Self::Southwest => "SW",
Self::WestSouthwest => "WSW",
Self::West => "W",
Self::WestNorthwest => "WNW",
Self::Northwest => "NW",
Self::NorthNorthwest => "NNW",
}
}
#[must_use]
pub const fn bearing(self) -> Bearing {
let index = self as u16;
Bearing {
degrees: (index * 45).div_ceil(2),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Humidity {
percent: u8,
}
impl Humidity {
pub const fn new(percent: u8) -> Result<Self, UnitError> {
if percent == 0 || percent > 100 {
Err(UnitError::BadHumidity { got: percent })
} else {
Ok(Self { percent })
}
}
pub const fn from_wire_percent(wire: u8) -> Result<Self, UnitError> {
if wire == 0 {
Ok(Self { percent: 100 })
} else {
Self::new(wire)
}
}
#[must_use]
pub const fn percent(self) -> u8 {
self.percent
}
#[must_use]
pub const fn wire_percent(self) -> u8 {
if self.percent == 100 { 0 } else { self.percent }
}
}
macro_rules! quantity_debug {
($type:ident, $fmt:literal, $($accessor:ident),+) => {
impl core::fmt::Debug for $type {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, concat!(stringify!($type), "(", $fmt, ")"), $(self.$accessor()),+)
}
}
};
}
macro_rules! quantity_arithmetic {
($type:ident, $field:ident) => {
impl core::ops::Add for $type {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self {
$field: self.$field.saturating_add(rhs.$field),
}
}
}
impl core::ops::Sub for $type {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self {
$field: self.$field.saturating_sub(rhs.$field),
}
}
}
impl core::ops::Neg for $type {
type Output = Self;
fn neg(self) -> Self {
Self {
$field: self.$field.saturating_neg(),
}
}
}
};
}
quantity_debug!(Distance, "{} m / {} ft", meters, feet);
quantity_debug!(Speed, "{} kn / {} mph / {} km/h", knots, mph, kmh);
quantity_debug!(
Rainfall,
"{} mm / {} hundredths-inch",
millimeters,
hundredths_inch
);
quantity_debug!(Temperature, "{} C / {} F", celsius, fahrenheit);
quantity_debug!(
Pressure,
"{} hPa / {} hundredths-inHg",
hpa,
hundredths_inhg
);
quantity_debug!(Power, "{} W", watts);
quantity_arithmetic!(Distance, micrometers);
quantity_arithmetic!(Speed, millimeters_per_hour);
quantity_arithmetic!(Rainfall, micrometers);
quantity_arithmetic!(Pressure, millipascals);
quantity_arithmetic!(Power, microwatts);
impl core::fmt::Debug for Bearing {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"Bearing({} deg / {})",
self.degrees,
self.compass_point().abbreviation()
)
}
}
impl core::fmt::Debug for Humidity {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Humidity({}%)", self.percent)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn temperature_canonical_values_are_pinned() {
let cases = [
(Temperature::from_fahrenheit(-99), -3_275_000),
(Temperature::from_fahrenheit(32), 0),
(Temperature::from_fahrenheit(72), 1_000_000),
(Temperature::from_fahrenheit(999), 24_175_000),
(Temperature::from_celsius(21), 945_000),
(Temperature::from_celsius(100), 4_500_000),
(Temperature::from_millidegrees_celsius(21_400), 963_000),
];
for (temperature, units) in cases {
assert_eq!(temperature.units, units, "{temperature:?}");
}
assert_eq!(UNITS_PER_CELSIUS % 9, 0);
assert_eq!(UNITS_PER_CELSIUS % 1000, 0);
assert_eq!(UNITS_PER_CELSIUS * 5 / 9, UNITS_PER_FAHRENHEIT);
}
#[test]
fn div_round_goes_away_from_zero_on_a_tie() {
assert_eq!(div_round(5, 10), 1);
assert_eq!(div_round(-5, 10), -1);
assert_eq!(div_round(4, 10), 0);
assert_eq!(div_round(-4, 10), 0);
}
#[test]
fn narrow_saturates_instead_of_wrapping() {
assert_eq!(narrow(i64::from(i32::MAX) + 1), i32::MAX);
assert_eq!(narrow(i64::from(i32::MIN) - 1), i32::MIN);
assert_eq!(narrow(0), 0);
}
#[test]
fn compass_boundaries_land_where_the_sector_edges_are() {
assert_eq!(
Bearing::new(11).unwrap().compass_point(),
CompassPoint::North
);
assert_eq!(
Bearing::new(12).unwrap().compass_point(),
CompassPoint::NorthNortheast
);
assert_eq!(
Bearing::new(348).unwrap().compass_point(),
CompassPoint::NorthNorthwest
);
assert_eq!(
Bearing::new(349).unwrap().compass_point(),
CompassPoint::North
);
}
#[test]
fn every_compass_point_round_trips_through_its_own_bearing() {
for point in CompassPoint::ALL {
assert_eq!(point.bearing().compass_point(), point, "{point:?}");
}
}
struct Buf {
bytes: [u8; 64],
len: usize,
}
impl Buf {
fn new() -> Self {
Self {
bytes: [0; 64],
len: 0,
}
}
fn as_str(&self) -> &str {
core::str::from_utf8(&self.bytes[..self.len]).expect("ascii")
}
}
impl core::fmt::Write for Buf {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let end = self.len + s.len();
self.bytes
.get_mut(self.len..end)
.ok_or(core::fmt::Error)?
.copy_from_slice(s.as_bytes());
self.len = end;
Ok(())
}
}
fn debug_of(value: &dyn core::fmt::Debug) -> Buf {
use core::fmt::Write;
let mut buf = Buf::new();
write!(&mut buf, "{value:?}").expect("fits");
buf
}
#[test]
fn debug_output_names_both_unit_systems() {
assert_eq!(
debug_of(&Distance::from_meters(376)).as_str(),
"Distance(376 m / 1234 ft)"
);
assert_eq!(
debug_of(&Temperature::from_celsius(100)).as_str(),
"Temperature(100 C / 212 F)"
);
assert_eq!(
debug_of(&Bearing::new(88).expect("valid")).as_str(),
"Bearing(88 deg / E)"
);
assert_eq!(
debug_of(&Speed::from_knots(36)).as_str(),
"Speed(36 kn / 41 mph / 67 km/h)"
);
}
}