use core::fmt;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum QuantityError {
NanValue,
}
impl fmt::Display for QuantityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QuantityError::NanValue => f.write_str("quantity value must not be NaN"),
}
}
}
macro_rules! quantity {
(
$quantity:ident, $unit:ident {
$($variant:ident = $si_factor:expr),+ $(,)?
}
) => {
#[derive(Clone, Copy, PartialOrd, PartialEq, Debug)]
pub struct $quantity {
value: f64,
}
#[derive(Clone, Copy)]
pub enum $unit {
$($variant,)+
}
impl $quantity {
pub fn new(value: f64, unit: $unit) -> Result<Self, QuantityError> {
if value.is_nan() {
return Err(QuantityError::NanValue);
}
Ok(Self::new_unchecked(value, unit))
}
pub(crate) const fn new_unchecked(value: f64, unit: $unit) -> Self {
Self {
value: value * unit.as_si_base(),
}
}
pub const fn to(&self, unit: $unit) -> f64 {
self.value / unit.as_si_base()
}
pub const fn zero() -> Self {
Self {
value: 0f64,
}
}
pub fn min(self, rhs: Self) -> Self {
Self {
value: self.value.min(rhs.value),
}
}
pub fn max(self, rhs: Self) -> Self {
Self {
value: self.value.max(rhs.value),
}
}
}
impl $unit {
const fn as_si_base(&self) -> f64 {
match self {
$($unit::$variant => $si_factor,)+
}
}
}
impl core::ops::Add for $quantity {
type Output = $quantity;
fn add(self, rhs: $quantity) -> Self::Output {
$quantity {
value: self.value + rhs.value,
}
}
}
impl core::ops::Sub for $quantity {
type Output = $quantity;
fn sub(self, rhs: $quantity) -> Self::Output {
$quantity {
value: self.value - rhs.value,
}
}
}
impl core::ops::Mul<f64> for $quantity {
type Output = $quantity;
fn mul(self, rhs: f64) -> Self::Output {
$quantity {
value: self.value * rhs,
}
}
}
impl core::ops::Mul<$quantity> for f64 {
type Output = $quantity;
fn mul(self, rhs: $quantity) -> Self::Output {
$quantity {
value: self * rhs.value,
}
}
}
impl core::ops::Div<f64> for $quantity {
type Output = $quantity;
fn div(self, rhs: f64) -> Self::Output {
$quantity {
value: self.value / rhs,
}
}
}
impl core::ops::AddAssign for $quantity {
fn add_assign(&mut self, rhs: $quantity) {
self.value += rhs.value;
}
}
impl core::ops::SubAssign for $quantity {
fn sub_assign(&mut self, rhs: $quantity) {
self.value -= rhs.value;
}
}
impl core::ops::MulAssign<f64> for $quantity {
fn mul_assign(&mut self, rhs: f64) {
self.value *= rhs;
}
}
impl core::ops::DivAssign<f64> for $quantity {
fn div_assign(&mut self, rhs: f64) {
self.value /= rhs;
}
}
};
}
macro_rules! quantity_mul {
($lhs:ident * $rhs:ident => $output:ident) => {
impl core::ops::Mul<$rhs> for $lhs {
type Output = $output;
fn mul(self, rhs: $rhs) -> Self::Output {
$output {
value: self.value * rhs.value,
}
}
}
};
}
macro_rules! quantity_mul_to_scalar {
($lhs:ident * $rhs:ident) => {
impl core::ops::Mul<$rhs> for $lhs {
type Output = f64;
fn mul(self, rhs: $rhs) -> Self::Output {
self.value * rhs.value
}
}
};
}
macro_rules! quantity_div {
($lhs:ident / $rhs:ident => $output:ident) => {
impl core::ops::Div<$rhs> for $lhs {
type Output = $output;
fn div(self, rhs: $rhs) -> Self::Output {
$output {
value: self.value / rhs.value,
}
}
}
};
}
macro_rules! quantity_div_to_scalar {
($lhs:ident / $rhs:ident) => {
impl core::ops::Div<$rhs> for $lhs {
type Output = f64;
fn div(self, rhs: $rhs) -> Self::Output {
self.value / rhs.value
}
}
};
}
macro_rules! quantity_scalar_div {
(f64 / $rhs:ident => $output:ident) => {
impl core::ops::Div<$rhs> for f64 {
type Output = $output;
fn div(self, rhs: $rhs) -> Self::Output {
$output {
value: self / rhs.value,
}
}
}
};
}
quantity! {
Time, TimeUnit {
Seconds = 1.0,
Minutes = 60.,
Hours = 3600.,
Milliseconds = 0.001,
}
}
quantity! {
Pressure, PressureUnit {
Pascal = 1.0,
Bar = 1E5,
Atmosphere = 101_325.0,
Psi = 6_894.757,
MPa = 1E6,
}
}
quantity! {
PressureRate, PressureRateUnit {
PascalPerSecond = 1.0,
BarPerSecond = 1E5,
}
}
quantity! {
Frequency, FrequencyUnit {
Hertz = 1.0,
}
}
quantity_mul_to_scalar!(Time * Frequency);
quantity_mul_to_scalar!(Frequency * Time);
quantity_mul!(Pressure * Frequency => PressureRate);
quantity_mul!(Frequency * Pressure => PressureRate);
quantity_mul!(PressureRate * Time => Pressure);
quantity_mul!(Time * PressureRate => Pressure);
quantity_div!(Pressure / Time => PressureRate);
quantity_div!(Pressure / PressureRate => Time);
quantity_div!(PressureRate / Pressure => Frequency);
quantity_div!(PressureRate / Frequency => Pressure);
quantity_div_to_scalar!(Time / Time);
quantity_div_to_scalar!(Pressure / Pressure);
quantity_div_to_scalar!(PressureRate / PressureRate);
quantity_div_to_scalar!(Frequency / Frequency);
quantity_scalar_div!(f64 / Time => Frequency);
#[cfg(test)]
mod tests {
use super::*;
const FIVE_MINUTES: Time = Time::new_unchecked(5.0, TimeUnit::Minutes);
const FIVE_MINUTES_IN_SECONDS: f64 = FIVE_MINUTES.to(TimeUnit::Seconds);
const ONE_BAR: Pressure = Pressure::new_unchecked(1.0, PressureUnit::Bar);
const ONE_BAR_IN_PASCAL: f64 = ONE_BAR.to(PressureUnit::Pascal);
fn assert_close(actual: f64, expected: f64, epsilon: f64) {
let diff = if actual > expected {
actual - expected
} else {
expected - actual
};
assert!(
diff <= epsilon,
"expected {actual} to be within {epsilon} of {expected}"
);
}
#[test]
fn creates_quantities_in_const_contexts() {
assert_eq!(FIVE_MINUTES_IN_SECONDS, 300.0);
assert_eq!(ONE_BAR_IN_PASCAL, 100_000.0);
}
#[test]
fn rejects_nan_quantity_values() {
assert_eq!(
Time::new(f64::NAN, TimeUnit::Seconds),
Err(QuantityError::NanValue)
);
assert_eq!(
Pressure::new(f64::NAN, PressureUnit::Bar),
Err(QuantityError::NanValue)
);
assert_eq!(
PressureRate::new(f64::NAN, PressureRateUnit::BarPerSecond),
Err(QuantityError::NanValue)
);
assert_eq!(
Frequency::new(f64::NAN, FrequencyUnit::Hertz),
Err(QuantityError::NanValue)
);
}
#[test]
fn converts_all_time_and_pressure_units_to_base_units() {
assert_eq!(
Time::new_unchecked(1.0, TimeUnit::Hours).to(TimeUnit::Seconds),
3600.0
);
assert_eq!(
Time::new_unchecked(2500.0, TimeUnit::Milliseconds).to(TimeUnit::Seconds),
2.5
);
assert_eq!(
Pressure::new_unchecked(1.0, PressureUnit::Atmosphere).to(PressureUnit::Pascal),
101_325.0
);
assert_close(
Pressure::new_unchecked(2.0, PressureUnit::Psi).to(PressureUnit::Pascal),
13_789.514,
1E-9,
);
assert_eq!(
Pressure::new_unchecked(1.5, PressureUnit::MPa).to(PressureUnit::Pascal),
1_500_000.0
);
}
#[test]
fn adds_quantities() {
let time = Time::new_unchecked(30.0, TimeUnit::Seconds)
+ Time::new_unchecked(1.0, TimeUnit::Minutes);
let pressure = Pressure::new_unchecked(1.0, PressureUnit::Bar)
+ Pressure::new_unchecked(50_000.0, PressureUnit::Pascal);
let pressure_rate = PressureRate::new_unchecked(1.0, PressureRateUnit::BarPerSecond)
+ PressureRate::new_unchecked(50_000.0, PressureRateUnit::PascalPerSecond);
let frequency = Frequency::new_unchecked(1.0, FrequencyUnit::Hertz)
+ Frequency::new_unchecked(2.0, FrequencyUnit::Hertz);
assert_eq!(time.to(TimeUnit::Seconds), 90.0);
assert_eq!(pressure.to(PressureUnit::Pascal), 150_000.0);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
150_000.0
);
assert_eq!(frequency.to(FrequencyUnit::Hertz), 3.0);
}
#[test]
fn subtracts_quantities() {
let time = Time::new_unchecked(2.0, TimeUnit::Minutes)
- Time::new_unchecked(30.0, TimeUnit::Seconds);
let pressure = Pressure::new_unchecked(2.0, PressureUnit::Bar)
- Pressure::new_unchecked(50_000.0, PressureUnit::Pascal);
let pressure_rate = PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond)
- PressureRate::new_unchecked(50_000.0, PressureRateUnit::PascalPerSecond);
let frequency = Frequency::new_unchecked(5.0, FrequencyUnit::Hertz)
- Frequency::new_unchecked(2.0, FrequencyUnit::Hertz);
assert_eq!(time.to(TimeUnit::Seconds), 90.0);
assert_eq!(pressure.to(PressureUnit::Pascal), 150_000.0);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
150_000.0
);
assert_eq!(frequency.to(FrequencyUnit::Hertz), 3.0);
}
#[test]
fn gets_min_and_max_quantities() {
let shorter = Time::new_unchecked(30.0, TimeUnit::Seconds);
let longer = Time::new_unchecked(1.0, TimeUnit::Minutes);
let lower = Pressure::new_unchecked(50_000.0, PressureUnit::Pascal);
let higher = Pressure::new_unchecked(1.0, PressureUnit::Bar);
let slower = PressureRate::new_unchecked(50_000.0, PressureRateUnit::PascalPerSecond);
let faster = PressureRate::new_unchecked(1.0, PressureRateUnit::BarPerSecond);
let lower_frequency = Frequency::new_unchecked(1.0, FrequencyUnit::Hertz);
let higher_frequency = Frequency::new_unchecked(2.0, FrequencyUnit::Hertz);
assert_eq!(shorter.min(longer).to(TimeUnit::Seconds), 30.0);
assert_eq!(shorter.max(longer).to(TimeUnit::Seconds), 60.0);
assert_eq!(lower.min(higher).to(PressureUnit::Pascal), 50_000.0);
assert_eq!(lower.max(higher).to(PressureUnit::Pascal), 100_000.0);
assert_eq!(
slower.min(faster).to(PressureRateUnit::PascalPerSecond),
50_000.0
);
assert_eq!(
slower.max(faster).to(PressureRateUnit::PascalPerSecond),
100_000.0
);
assert_eq!(
lower_frequency
.min(higher_frequency)
.to(FrequencyUnit::Hertz),
1.0
);
assert_eq!(
lower_frequency
.max(higher_frequency)
.to(FrequencyUnit::Hertz),
2.0
);
}
#[test]
fn adds_and_subtracts_assign_quantities() {
let mut time = Time::new_unchecked(1.0, TimeUnit::Minutes);
time += Time::new_unchecked(30.0, TimeUnit::Seconds);
time -= Time::new_unchecked(15.0, TimeUnit::Seconds);
let mut pressure = Pressure::new_unchecked(1.0, PressureUnit::Bar);
pressure += Pressure::new_unchecked(50_000.0, PressureUnit::Pascal);
pressure -= Pressure::new_unchecked(25_000.0, PressureUnit::Pascal);
let mut pressure_rate = PressureRate::new_unchecked(1.0, PressureRateUnit::BarPerSecond);
pressure_rate += PressureRate::new_unchecked(50_000.0, PressureRateUnit::PascalPerSecond);
pressure_rate -= PressureRate::new_unchecked(25_000.0, PressureRateUnit::PascalPerSecond);
let mut frequency = Frequency::new_unchecked(1.0, FrequencyUnit::Hertz);
frequency += Frequency::new_unchecked(3.0, FrequencyUnit::Hertz);
frequency -= Frequency::new_unchecked(1.5, FrequencyUnit::Hertz);
assert_eq!(time.to(TimeUnit::Seconds), 75.0);
assert_eq!(pressure.to(PressureUnit::Pascal), 125_000.0);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
125_000.0
);
assert_eq!(frequency.to(FrequencyUnit::Hertz), 2.5);
}
#[test]
fn multiplies_and_divides_quantities_by_scalars() {
let time = Time::new_unchecked(1.0, TimeUnit::Minutes) * 2.0;
let time_commuted = 2.0 * Time::new_unchecked(1.0, TimeUnit::Minutes);
let pressure = Pressure::new_unchecked(2.0, PressureUnit::Bar) / 4.0;
let pressure_commuted = 3.0 * Pressure::new_unchecked(2.0, PressureUnit::Bar);
let pressure_rate = PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond) * 3.0;
let pressure_rate_commuted =
3.0 * PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond);
let frequency = Frequency::new_unchecked(8.0, FrequencyUnit::Hertz) / 2.0;
let frequency_commuted = 3.0 * Frequency::new_unchecked(8.0, FrequencyUnit::Hertz);
assert_eq!(time.to(TimeUnit::Seconds), 120.0);
assert_eq!(time_commuted.to(TimeUnit::Seconds), 120.0);
assert_eq!(pressure.to(PressureUnit::Pascal), 50_000.0);
assert_eq!(pressure_commuted.to(PressureUnit::Pascal), 600_000.0);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
600_000.0
);
assert_eq!(
pressure_rate_commuted.to(PressureRateUnit::PascalPerSecond),
600_000.0
);
assert_eq!(frequency.to(FrequencyUnit::Hertz), 4.0);
assert_eq!(frequency_commuted.to(FrequencyUnit::Hertz), 24.0);
}
#[test]
fn multiplies_and_divides_assign_quantities_by_scalars() {
let mut time = Time::new_unchecked(1.0, TimeUnit::Minutes);
time *= 2.0;
time /= 4.0;
let mut pressure = Pressure::new_unchecked(2.0, PressureUnit::Bar);
pressure *= 3.0;
pressure /= 2.0;
let mut pressure_rate = PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond);
pressure_rate *= 2.0;
pressure_rate /= 4.0;
let mut frequency = Frequency::new_unchecked(8.0, FrequencyUnit::Hertz);
frequency *= 3.0;
frequency /= 4.0;
assert_eq!(time.to(TimeUnit::Seconds), 30.0);
assert_eq!(pressure.to(PressureUnit::Pascal), 300_000.0);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
100_000.0
);
assert_eq!(frequency.to(FrequencyUnit::Hertz), 6.0);
}
#[test]
fn multiplies_related_quantities() {
let cycles = Time::new_unchecked(3.0, TimeUnit::Seconds)
* Frequency::new_unchecked(2.0, FrequencyUnit::Hertz);
let cycles_commuted = Frequency::new_unchecked(2.0, FrequencyUnit::Hertz)
* Time::new_unchecked(3.0, TimeUnit::Seconds);
let pressure_rate = Pressure::new_unchecked(2.0, PressureUnit::Bar)
* Frequency::new_unchecked(3.0, FrequencyUnit::Hertz);
let pressure_rate_commuted = Frequency::new_unchecked(3.0, FrequencyUnit::Hertz)
* Pressure::new_unchecked(2.0, PressureUnit::Bar);
let pressure = PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond)
* Time::new_unchecked(3.0, TimeUnit::Seconds);
let pressure_commuted = Time::new_unchecked(3.0, TimeUnit::Seconds)
* PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond);
assert_eq!(cycles, 6.0);
assert_eq!(cycles_commuted, 6.0);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
600_000.0
);
assert_eq!(
pressure_rate_commuted.to(PressureRateUnit::PascalPerSecond),
600_000.0
);
assert_eq!(pressure.to(PressureUnit::Pascal), 600_000.0);
assert_eq!(pressure_commuted.to(PressureUnit::Pascal), 600_000.0);
}
#[test]
fn divides_related_quantities() {
let pressure_rate = Pressure::new_unchecked(6.0, PressureUnit::Bar)
/ Time::new_unchecked(3.0, TimeUnit::Seconds);
let time = Pressure::new_unchecked(6.0, PressureUnit::Bar)
/ PressureRate::new_unchecked(2.0, PressureRateUnit::BarPerSecond);
let frequency = PressureRate::new_unchecked(6.0, PressureRateUnit::BarPerSecond)
/ Pressure::new_unchecked(3.0, PressureUnit::Bar);
let frequency_from_scalar = 6.0 / Time::new_unchecked(3.0, TimeUnit::Seconds);
let pressure = PressureRate::new_unchecked(6.0, PressureRateUnit::BarPerSecond)
/ Frequency::new_unchecked(2.0, FrequencyUnit::Hertz);
assert_eq!(
pressure_rate.to(PressureRateUnit::PascalPerSecond),
200_000.0
);
assert_eq!(time.to(TimeUnit::Seconds), 3.0);
assert_eq!(frequency.to(FrequencyUnit::Hertz), 2.0);
assert_eq!(frequency_from_scalar.to(FrequencyUnit::Hertz), 2.0);
assert_eq!(pressure.to(PressureUnit::Pascal), 300_000.0);
}
#[test]
fn divides_same_quantities_to_scalars() {
assert_eq!(
Time::new_unchecked(6.0, TimeUnit::Seconds)
/ Time::new_unchecked(3.0, TimeUnit::Seconds),
2.0
);
assert_eq!(
Pressure::new_unchecked(6.0, PressureUnit::Bar)
/ Pressure::new_unchecked(3.0, PressureUnit::Bar),
2.0
);
assert_eq!(
PressureRate::new_unchecked(6.0, PressureRateUnit::BarPerSecond)
/ PressureRate::new_unchecked(3.0, PressureRateUnit::BarPerSecond),
2.0
);
assert_eq!(
Frequency::new_unchecked(6.0, FrequencyUnit::Hertz)
/ Frequency::new_unchecked(3.0, FrequencyUnit::Hertz),
2.0
);
}
}