Skip to main content

haystack_core/kinds/
number.rs

1use crate::codecs::shared;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4
5/// Haystack Number — a 64-bit float with optional unit string.
6///
7/// Equality requires both `val` and `unit` to match.
8/// NaN != NaN (IEEE 754 semantics).
9/// Display uses compact format: no trailing zeros, unit appended directly.
10#[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        // Rust's default Display for large floats
100        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}