haystack_core/kinds/
number.rs1use crate::codecs::shared;
2use crate::kinds::units::{UnitError, convert, unit_for};
3use std::fmt;
4use std::hash::{Hash, Hasher};
5
6#[derive(Debug, Clone)]
12pub struct Number {
13 pub val: f64,
14 pub unit: Option<String>,
15}
16
17impl Number {
18 pub fn new(val: f64, unit: Option<String>) -> Self {
19 Self { val, unit }
20 }
21
22 pub fn unitless(val: f64) -> Self {
23 Self { val, unit: None }
24 }
25
26 pub fn convert_to(&self, target_unit: &str) -> Result<Number, UnitError> {
32 let from = self
33 .unit
34 .as_deref()
35 .ok_or_else(|| UnitError::UnknownUnit("(none)".to_string()))?;
36 let converted = convert(self.val, from, target_unit)?;
37 let target_name = unit_for(target_unit)
38 .map(|u| u.name.clone())
39 .unwrap_or_else(|| target_unit.to_string());
40 Ok(Number::new(converted, Some(target_name)))
41 }
42}
43
44impl PartialEq for Number {
45 fn eq(&self, other: &Self) -> bool {
46 self.val.to_bits() == other.val.to_bits() && self.unit == other.unit
47 }
48}
49
50impl Eq for Number {}
51
52impl Hash for Number {
53 fn hash<H: Hasher>(&self, state: &mut H) {
54 self.val.to_bits().hash(state);
55 self.unit.hash(state);
56 }
57}
58
59impl fmt::Display for Number {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "{}", shared::format_number_val(self.val))?;
62 if let Some(ref u) = self.unit {
63 write!(f, "{u}")?;
64 }
65 Ok(())
66 }
67}
68
69impl PartialOrd for Number {
70 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
71 if self.unit != other.unit {
72 return None;
73 }
74 self.val.partial_cmp(&other.val)
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn number_unitless() {
84 let n = Number::unitless(72.5);
85 assert_eq!(n.val, 72.5);
86 assert_eq!(n.unit, None);
87 assert_eq!(n.to_string(), "72.5");
88 }
89
90 #[test]
91 fn number_with_unit() {
92 let n = Number::new(72.5, Some("°F".into()));
93 assert_eq!(n.to_string(), "72.5°F");
94 }
95
96 #[test]
97 fn number_integer_display() {
98 let n = Number::unitless(42.0);
99 assert_eq!(n.to_string(), "42");
100 }
101
102 #[test]
103 fn number_zero() {
104 let n = Number::unitless(0.0);
105 assert_eq!(n.to_string(), "0");
106 }
107
108 #[test]
109 fn number_negative() {
110 let n = Number::new(-23.45, Some("m²".into()));
111 assert_eq!(n.to_string(), "-23.45m²");
112 }
113
114 #[test]
115 fn number_scientific() {
116 let n = Number::new(5.4e8, Some("kW".into()));
117 let s = n.to_string();
119 assert!(s.contains("kW"));
120 }
121
122 #[test]
123 fn number_special_inf() {
124 assert_eq!(Number::unitless(f64::INFINITY).to_string(), "INF");
125 }
126
127 #[test]
128 fn number_special_neg_inf() {
129 assert_eq!(Number::unitless(f64::NEG_INFINITY).to_string(), "-INF");
130 }
131
132 #[test]
133 fn number_special_nan() {
134 assert_eq!(Number::unitless(f64::NAN).to_string(), "NaN");
135 }
136
137 #[test]
138 fn number_equality() {
139 let a = Number::new(72.5, Some("°F".into()));
140 let b = Number::new(72.5, Some("°F".into()));
141 let c = Number::new(72.5, Some("°C".into()));
142 assert_eq!(a, b);
143 assert_ne!(a, c);
144 }
145
146 #[test]
147 fn number_nan_equality() {
148 let a = Number::unitless(f64::NAN);
149 let b = Number::unitless(f64::NAN);
150 assert_eq!(a, b);
151 }
152
153 #[test]
154 fn number_ordering_same_unit() {
155 let a = Number::new(10.0, Some("°F".into()));
156 let b = Number::new(20.0, Some("°F".into()));
157 assert!(a < b);
158 }
159
160 #[test]
161 fn number_ordering_different_unit() {
162 let a = Number::new(10.0, Some("°F".into()));
163 let b = Number::new(20.0, Some("°C".into()));
164 assert_eq!(a.partial_cmp(&b), None);
165 }
166
167 #[test]
168 fn number_hashable() {
169 use std::collections::HashSet;
170 let mut set = HashSet::new();
171 set.insert(Number::unitless(42.0));
172 assert!(set.contains(&Number::unitless(42.0)));
173 }
174
175 #[test]
178 fn number_convert_to_celsius() {
179 let n = Number::new(212.0, Some("fahrenheit".into()));
180 let c = n.convert_to("celsius").unwrap();
181 assert!((c.val - 100.0).abs() < 0.01);
182 assert_eq!(c.unit.as_deref(), Some("celsius"));
183 }
184
185 #[test]
186 fn number_convert_to_by_symbol() {
187 let n = Number::new(0.0, Some("°C".into()));
188 let f = n.convert_to("°F").unwrap();
189 assert!((f.val - 32.0).abs() < 0.01);
190 assert_eq!(f.unit.as_deref(), Some("fahrenheit"));
191 }
192
193 #[test]
194 fn number_convert_to_unitless_error() {
195 let n = Number::unitless(42.0);
196 let err = n.convert_to("celsius").unwrap_err();
197 assert!(matches!(err, crate::kinds::units::UnitError::UnknownUnit(ref s) if s == "(none)"));
198 }
199
200 #[test]
201 fn number_convert_to_incompatible() {
202 let n = Number::new(100.0, Some("celsius".into()));
203 let err = n.convert_to("meter").unwrap_err();
204 assert!(matches!(
205 err,
206 crate::kinds::units::UnitError::IncompatibleUnits(_, _)
207 ));
208 }
209}