Skip to main content

use_density/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive material density helpers.
3//!
4//! Initial calculations assume SI units unless otherwise documented.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_density::{Density, density, mass_from_density, volume_from_density};
10//!
11//! let density_value = Density::new(1_000.0).unwrap();
12//!
13//! assert_eq!(density_value.kg_per_m3(), 1_000.0);
14//! assert_eq!(density(10.0, 0.5).unwrap(), 20.0);
15//! assert_eq!(mass_from_density(1_000.0, 0.01).unwrap(), 10.0);
16//! assert_eq!(volume_from_density(10.0, 1_000.0).unwrap(), 0.01);
17//! ```
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct Density {
21    kg_per_m3: f64,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DensityError {
26    InvalidDensity,
27    InvalidMass,
28    InvalidVolume,
29}
30
31fn validate_density(value: f64) -> Result<f64, DensityError> {
32    if !value.is_finite() || value <= 0.0 {
33        Err(DensityError::InvalidDensity)
34    } else {
35        Ok(value)
36    }
37}
38
39fn validate_mass(mass_kg: f64) -> Result<f64, DensityError> {
40    if !mass_kg.is_finite() || mass_kg < 0.0 {
41        Err(DensityError::InvalidMass)
42    } else {
43        Ok(mass_kg)
44    }
45}
46
47fn validate_volume(volume_m3: f64) -> Result<f64, DensityError> {
48    if !volume_m3.is_finite() || volume_m3 <= 0.0 {
49        Err(DensityError::InvalidVolume)
50    } else {
51        Ok(volume_m3)
52    }
53}
54
55impl Density {
56    pub fn new(kg_per_m3: f64) -> Result<Self, DensityError> {
57        Ok(Self {
58            kg_per_m3: validate_density(kg_per_m3)?,
59        })
60    }
61
62    #[must_use]
63    pub fn kg_per_m3(&self) -> f64 {
64        self.kg_per_m3
65    }
66}
67
68pub fn density(mass_kg: f64, volume_m3: f64) -> Result<f64, DensityError> {
69    Ok(validate_mass(mass_kg)? / validate_volume(volume_m3)?)
70}
71
72pub fn mass_from_density(density_kg_per_m3: f64, volume_m3: f64) -> Result<f64, DensityError> {
73    Ok(validate_density(density_kg_per_m3)? * validate_volume(volume_m3)?)
74}
75
76pub fn volume_from_density(mass_kg: f64, density_kg_per_m3: f64) -> Result<f64, DensityError> {
77    Ok(validate_mass(mass_kg)? / validate_density(density_kg_per_m3)?)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::{Density, DensityError, density, mass_from_density, volume_from_density};
83
84    #[test]
85    fn computes_density_related_values() {
86        let density_value = Density::new(7_850.0).unwrap();
87        let computed_density = density(15.7, 0.002).unwrap();
88        let computed_mass = mass_from_density(7_850.0, 0.002).unwrap();
89        let computed_volume = volume_from_density(15.7, 7_850.0).unwrap();
90
91        assert_eq!(density_value.kg_per_m3(), 7_850.0);
92        assert!((computed_density - 7_850.0).abs() < 1.0e-12);
93        assert!((computed_mass - 15.7).abs() < 1.0e-12);
94        assert!((computed_volume - 0.002).abs() < 1.0e-12);
95    }
96
97    #[test]
98    fn allows_zero_mass_where_valid() {
99        assert_eq!(density(0.0, 1.0).unwrap(), 0.0);
100        assert_eq!(volume_from_density(0.0, 1_000.0).unwrap(), 0.0);
101    }
102
103    #[test]
104    fn rejects_invalid_density_inputs() {
105        assert_eq!(Density::new(0.0), Err(DensityError::InvalidDensity));
106        assert_eq!(
107            mass_from_density(f64::NAN, 1.0),
108            Err(DensityError::InvalidDensity)
109        );
110        assert_eq!(
111            volume_from_density(1.0, -5.0),
112            Err(DensityError::InvalidDensity)
113        );
114    }
115
116    #[test]
117    fn rejects_invalid_mass_and_volume_inputs() {
118        assert_eq!(density(-1.0, 1.0), Err(DensityError::InvalidMass));
119        assert_eq!(density(1.0, 0.0), Err(DensityError::InvalidVolume));
120        assert_eq!(
121            mass_from_density(1_000.0, f64::INFINITY),
122            Err(DensityError::InvalidVolume)
123        );
124    }
125}