1#![forbid(unsafe_code)]
2#[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}