haystack_core/kinds/
number.rs1use crate::codecs::shared;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4
5#[derive(Debug, Clone)]
11pub struct Number {
12 pub val: f64,
13 pub unit: Option<String>,
14}
15
16impl Number {
17 pub fn new(val: f64, unit: Option<String>) -> Self {
18 Self { val, unit }
19 }
20
21 pub fn unitless(val: f64) -> Self {
22 Self { val, unit: None }
23 }
24}
25
26impl PartialEq for Number {
27 fn eq(&self, other: &Self) -> bool {
28 self.val == other.val && self.unit == other.unit
29 }
30}
31
32impl Eq for Number {}
33
34impl Hash for Number {
35 fn hash<H: Hasher>(&self, state: &mut H) {
36 self.val.to_bits().hash(state);
37 self.unit.hash(state);
38 }
39}
40
41impl fmt::Display for Number {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{}", shared::format_number_val(self.val))?;
44 if let Some(ref u) = self.unit {
45 write!(f, "{u}")?;
46 }
47 Ok(())
48 }
49}
50
51impl PartialOrd for Number {
52 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
53 if self.unit != other.unit {
54 return None;
55 }
56 self.val.partial_cmp(&other.val)
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn number_unitless() {
66 let n = Number::unitless(72.5);
67 assert_eq!(n.val, 72.5);
68 assert_eq!(n.unit, None);
69 assert_eq!(n.to_string(), "72.5");
70 }
71
72 #[test]
73 fn number_with_unit() {
74 let n = Number::new(72.5, Some("°F".into()));
75 assert_eq!(n.to_string(), "72.5°F");
76 }
77
78 #[test]
79 fn number_integer_display() {
80 let n = Number::unitless(42.0);
81 assert_eq!(n.to_string(), "42");
82 }
83
84 #[test]
85 fn number_zero() {
86 let n = Number::unitless(0.0);
87 assert_eq!(n.to_string(), "0");
88 }
89
90 #[test]
91 fn number_negative() {
92 let n = Number::new(-23.45, Some("m²".into()));
93 assert_eq!(n.to_string(), "-23.45m²");
94 }
95
96 #[test]
97 fn number_scientific() {
98 let n = Number::new(5.4e8, Some("kW".into()));
99 let s = n.to_string();
101 assert!(s.contains("kW"));
102 }
103
104 #[test]
105 fn number_special_inf() {
106 assert_eq!(Number::unitless(f64::INFINITY).to_string(), "INF");
107 }
108
109 #[test]
110 fn number_special_neg_inf() {
111 assert_eq!(Number::unitless(f64::NEG_INFINITY).to_string(), "-INF");
112 }
113
114 #[test]
115 fn number_special_nan() {
116 assert_eq!(Number::unitless(f64::NAN).to_string(), "NaN");
117 }
118
119 #[test]
120 fn number_equality() {
121 let a = Number::new(72.5, Some("°F".into()));
122 let b = Number::new(72.5, Some("°F".into()));
123 let c = Number::new(72.5, Some("°C".into()));
124 assert_eq!(a, b);
125 assert_ne!(a, c);
126 }
127
128 #[test]
129 fn number_nan_inequality() {
130 let a = Number::unitless(f64::NAN);
131 let b = Number::unitless(f64::NAN);
132 assert_ne!(a, b);
133 }
134
135 #[test]
136 fn number_ordering_same_unit() {
137 let a = Number::new(10.0, Some("°F".into()));
138 let b = Number::new(20.0, Some("°F".into()));
139 assert!(a < b);
140 }
141
142 #[test]
143 fn number_ordering_different_unit() {
144 let a = Number::new(10.0, Some("°F".into()));
145 let b = Number::new(20.0, Some("°C".into()));
146 assert_eq!(a.partial_cmp(&b), None);
147 }
148
149 #[test]
150 fn number_hashable() {
151 use std::collections::HashSet;
152 let mut set = HashSet::new();
153 set.insert(Number::unitless(42.0));
154 assert!(set.contains(&Number::unitless(42.0)));
155 }
156}