hcl_primitives/
number.rs

1//! HCL number representation.
2
3use core::cmp::Ordering;
4use core::fmt;
5use core::hash::{Hash, Hasher};
6use core::ops::{Add, Div, Mul, Neg, Rem, Sub};
7#[cfg(feature = "serde")]
8use serde::de::Unexpected;
9
10enum CoerceResult {
11    PosInt(u64, u64),
12    NegInt(i64, i64),
13    Float(f64, f64),
14}
15
16// Coerce two numbers to a common type suitable for binary operations.
17fn coerce(a: N, b: N) -> CoerceResult {
18    match (a, b) {
19        (N::PosInt(a), N::PosInt(b)) => CoerceResult::PosInt(a, b),
20        (N::NegInt(a), N::NegInt(b)) => CoerceResult::NegInt(a, b),
21        (N::Float(a), N::Float(b)) => CoerceResult::Float(a, b),
22        (a, b) => CoerceResult::Float(a.to_f64(), b.to_f64()),
23    }
24}
25
26/// Represents an HCL number.
27#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
28pub struct Number {
29    n: N,
30}
31
32#[derive(Clone, Copy)]
33enum N {
34    PosInt(u64),
35    /// Always less than zero.
36    NegInt(i64),
37    /// Always finite.
38    Float(f64),
39}
40
41impl N {
42    fn from_finite_f64(value: f64) -> N {
43        debug_assert!(value.is_finite());
44
45        #[cfg(feature = "std")]
46        let no_fraction = value.fract() == 0.0;
47
48        // `core::f64` does not have the `fract()` method.
49        #[cfg(not(feature = "std"))]
50        #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
51        let no_fraction = value - (value as i64 as f64) == 0.0;
52
53        if no_fraction {
54            #[allow(clippy::cast_possible_truncation)]
55            N::from(value as i64)
56        } else {
57            N::Float(value)
58        }
59    }
60
61    fn as_i64(&self) -> Option<i64> {
62        match *self {
63            N::PosInt(n) => i64::try_from(n).ok(),
64            N::NegInt(n) => Some(n),
65            N::Float(_) => None,
66        }
67    }
68
69    fn as_u64(&self) -> Option<u64> {
70        match *self {
71            N::PosInt(n) => Some(n),
72            N::NegInt(n) => u64::try_from(n).ok(),
73            N::Float(_) => None,
74        }
75    }
76
77    fn to_f64(self) -> f64 {
78        #[allow(clippy::cast_precision_loss)]
79        match self {
80            N::PosInt(n) => n as f64,
81            N::NegInt(n) => n as f64,
82            N::Float(n) => n,
83        }
84    }
85
86    fn is_f64(&self) -> bool {
87        match self {
88            N::Float(_) => true,
89            N::PosInt(_) | N::NegInt(_) => false,
90        }
91    }
92
93    fn is_i64(&self) -> bool {
94        match self {
95            N::NegInt(_) => true,
96            N::PosInt(_) | N::Float(_) => false,
97        }
98    }
99
100    fn is_u64(&self) -> bool {
101        match self {
102            N::PosInt(_) => true,
103            N::NegInt(_) | N::Float(_) => false,
104        }
105    }
106}
107
108impl PartialEq for N {
109    fn eq(&self, other: &Self) -> bool {
110        match coerce(*self, *other) {
111            CoerceResult::PosInt(a, b) => a == b,
112            CoerceResult::NegInt(a, b) => a == b,
113            CoerceResult::Float(a, b) => a == b,
114        }
115    }
116}
117
118// N is `Eq` because we ensure that the wrapped f64 is always finite.
119impl Eq for N {}
120
121impl PartialOrd for N {
122    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
123        match coerce(*self, *other) {
124            CoerceResult::PosInt(a, b) => a.partial_cmp(&b),
125            CoerceResult::NegInt(a, b) => a.partial_cmp(&b),
126            CoerceResult::Float(a, b) => a.partial_cmp(&b),
127        }
128    }
129}
130
131impl Hash for N {
132    fn hash<H>(&self, h: &mut H)
133    where
134        H: Hasher,
135    {
136        match *self {
137            N::PosInt(n) => n.hash(h),
138            N::NegInt(n) => n.hash(h),
139            N::Float(n) => {
140                if n == 0.0f64 {
141                    // There are 2 zero representations, +0 and -0, which
142                    // compare equal but have different bits. We use the +0 hash
143                    // for both so that hash(+0) == hash(-0).
144                    0.0f64.to_bits().hash(h);
145                } else {
146                    n.to_bits().hash(h);
147                }
148            }
149        }
150    }
151}
152
153impl From<i64> for N {
154    fn from(i: i64) -> Self {
155        if i < 0 {
156            N::NegInt(i)
157        } else {
158            #[allow(clippy::cast_sign_loss)]
159            N::PosInt(i as u64)
160        }
161    }
162}
163
164impl Neg for N {
165    type Output = N;
166
167    fn neg(self) -> Self::Output {
168        match self {
169            #[allow(clippy::cast_possible_wrap)]
170            N::PosInt(value) => N::NegInt(-(value as i64)),
171            N::NegInt(value) => N::from(-value),
172            N::Float(value) => N::Float(-value),
173        }
174    }
175}
176
177impl Add for N {
178    type Output = N;
179
180    fn add(self, rhs: Self) -> Self::Output {
181        match coerce(self, rhs) {
182            CoerceResult::PosInt(a, b) => N::PosInt(a + b),
183            CoerceResult::NegInt(a, b) => N::NegInt(a + b),
184            CoerceResult::Float(a, b) => N::from_finite_f64(a + b),
185        }
186    }
187}
188
189impl Sub for N {
190    type Output = N;
191
192    fn sub(self, rhs: Self) -> Self::Output {
193        match coerce(self, rhs) {
194            CoerceResult::PosInt(a, b) => {
195                if b > a {
196                    #[allow(clippy::cast_possible_wrap)]
197                    N::NegInt(a as i64 - b as i64)
198                } else {
199                    N::PosInt(a - b)
200                }
201            }
202            CoerceResult::NegInt(a, b) => N::from(a - b),
203            CoerceResult::Float(a, b) => N::from_finite_f64(a - b),
204        }
205    }
206}
207
208impl Mul for N {
209    type Output = N;
210
211    fn mul(self, rhs: Self) -> Self::Output {
212        match coerce(self, rhs) {
213            CoerceResult::PosInt(a, b) => N::PosInt(a * b),
214            CoerceResult::NegInt(a, b) => N::from(a * b),
215            CoerceResult::Float(a, b) => N::from_finite_f64(a * b),
216        }
217    }
218}
219
220impl Div for N {
221    type Output = N;
222
223    fn div(self, rhs: Self) -> Self::Output {
224        N::from_finite_f64(self.to_f64() / rhs.to_f64())
225    }
226}
227
228impl Rem for N {
229    type Output = N;
230
231    fn rem(self, rhs: Self) -> Self::Output {
232        match coerce(self, rhs) {
233            CoerceResult::PosInt(a, b) => N::PosInt(a % b),
234            CoerceResult::NegInt(a, b) => N::NegInt(a % b),
235            CoerceResult::Float(a, b) => N::from_finite_f64(a % b),
236        }
237    }
238}
239
240impl Number {
241    /// Creates a new `Number` from a `f64`. Returns `None` if the float is infinite or NaN.
242    ///
243    /// # Example
244    ///
245    /// ```
246    /// # use hcl_primitives::Number;
247    /// assert!(Number::from_f64(42.0).is_some());
248    /// assert!(Number::from_f64(f64::NAN).is_none());
249    /// assert!(Number::from_f64(f64::INFINITY).is_none());
250    /// assert!(Number::from_f64(f64::NEG_INFINITY).is_none());
251    /// ```
252    pub fn from_f64(f: f64) -> Option<Number> {
253        if f.is_finite() {
254            Some(Number::from_finite_f64(f))
255        } else {
256            None
257        }
258    }
259
260    pub(crate) fn from_finite_f64(f: f64) -> Number {
261        Number {
262            n: N::from_finite_f64(f),
263        }
264    }
265
266    /// Represents the `Number` as f64 if possible. Returns None otherwise.
267    #[inline]
268    pub fn as_f64(&self) -> Option<f64> {
269        Some(self.n.to_f64())
270    }
271
272    /// If the `Number` is an integer, represent it as i64 if possible. Returns None otherwise.
273    #[inline]
274    pub fn as_i64(&self) -> Option<i64> {
275        self.n.as_i64()
276    }
277
278    /// If the `Number` is an integer, represent it as u64 if possible. Returns None otherwise.
279    #[inline]
280    pub fn as_u64(&self) -> Option<u64> {
281        self.n.as_u64()
282    }
283
284    /// Returns true if the `Number` is a float.
285    ///
286    /// For any `Number` on which `is_f64` returns true, `as_f64` is guaranteed to return the
287    /// float value.
288    #[inline]
289    pub fn is_f64(&self) -> bool {
290        self.n.is_f64()
291    }
292
293    /// Returns true if the `Number` is an integer between `i64::MIN` and `i64::MAX`.
294    ///
295    /// For any `Number` on which `is_i64` returns true, `as_i64` is guaranteed to return the
296    /// integer value.
297    #[inline]
298    pub fn is_i64(&self) -> bool {
299        self.n.is_i64()
300    }
301
302    /// Returns true if the `Number` is an integer between zero and `u64::MAX`.
303    ///
304    /// For any `Number` on which `is_u64` returns true, `as_u64` is guaranteed to return the
305    /// integer value.
306    #[inline]
307    pub fn is_u64(&self) -> bool {
308        self.n.is_u64()
309    }
310
311    // Not public API. Used to generate better deserialization errors in `hcl-rs`.
312    #[cfg(feature = "serde")]
313    #[doc(hidden)]
314    #[cold]
315    pub fn unexpected(&self) -> Unexpected<'_> {
316        match self.n {
317            N::PosInt(v) => Unexpected::Unsigned(v),
318            N::NegInt(v) => Unexpected::Signed(v),
319            N::Float(v) => Unexpected::Float(v),
320        }
321    }
322}
323
324macro_rules! impl_from_unsigned {
325    ($($ty:ty),*) => {
326        $(
327            impl From<$ty> for Number {
328                #[inline]
329                fn from(u: $ty) -> Self {
330                    Number {
331                        #[allow(clippy::cast_lossless)]
332                        n: N::PosInt(u as u64)
333                    }
334                }
335            }
336        )*
337    };
338}
339
340macro_rules! impl_from_signed {
341    ($($ty:ty),*) => {
342        $(
343            impl From<$ty> for Number {
344                #[inline]
345                fn from(i: $ty) -> Self {
346                    Number {
347                        #[allow(clippy::cast_lossless)]
348                        n: N::from(i as i64)
349                    }
350                }
351            }
352        )*
353    };
354}
355
356macro_rules! impl_binary_ops {
357    ($($op:ty => $method:ident),*) => {
358        $(
359            impl $op for Number {
360                type Output = Number;
361
362                fn $method(self, rhs: Self) -> Self::Output {
363                    Number {
364                        n: self.n.$method(rhs.n)
365                    }
366                }
367            }
368        )*
369    };
370}
371
372impl_from_unsigned!(u8, u16, u32, u64, usize);
373impl_from_signed!(i8, i16, i32, i64, isize);
374impl_binary_ops!(Add => add, Sub => sub, Mul => mul, Div => div, Rem => rem);
375
376impl Neg for Number {
377    type Output = Number;
378
379    fn neg(self) -> Self::Output {
380        Number { n: -self.n }
381    }
382}
383
384impl fmt::Display for Number {
385    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
386        match self.n {
387            N::PosInt(v) => f.write_str(itoa::Buffer::new().format(v)),
388            N::NegInt(v) => f.write_str(itoa::Buffer::new().format(v)),
389            N::Float(v) => f.write_str(ryu::Buffer::new().format_finite(v)),
390        }
391    }
392}
393
394impl fmt::Debug for Number {
395    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
396        write!(f, "Number({self})")
397    }
398}
399
400#[cfg(feature = "serde")]
401impl serde::Serialize for Number {
402    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
403    where
404        S: serde::Serializer,
405    {
406        match self.n {
407            N::PosInt(v) => serializer.serialize_u64(v),
408            N::NegInt(v) => serializer.serialize_i64(v),
409            N::Float(v) => serializer.serialize_f64(v),
410        }
411    }
412}
413
414#[cfg(feature = "serde")]
415impl<'de> serde::Deserialize<'de> for Number {
416    fn deserialize<D>(deserializer: D) -> Result<Number, D::Error>
417    where
418        D: serde::Deserializer<'de>,
419    {
420        struct NumberVisitor;
421
422        impl serde::de::Visitor<'_> for NumberVisitor {
423            type Value = Number;
424
425            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
426                formatter.write_str("an HCL number")
427            }
428
429            fn visit_i64<E>(self, value: i64) -> Result<Number, E> {
430                Ok(value.into())
431            }
432
433            fn visit_u64<E>(self, value: u64) -> Result<Number, E> {
434                Ok(value.into())
435            }
436
437            fn visit_f64<E>(self, value: f64) -> Result<Number, E>
438            where
439                E: serde::de::Error,
440            {
441                Number::from_f64(value).ok_or_else(|| serde::de::Error::custom("not an HCL number"))
442            }
443        }
444
445        deserializer.deserialize_any(NumberVisitor)
446    }
447}
448
449#[cfg(feature = "serde")]
450impl<'de> serde::Deserializer<'de> for Number {
451    type Error = super::Error;
452
453    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
454    where
455        V: serde::de::Visitor<'de>,
456    {
457        match self.n {
458            N::PosInt(i) => visitor.visit_u64(i),
459            N::NegInt(i) => visitor.visit_i64(i),
460            N::Float(f) => visitor.visit_f64(f),
461        }
462    }
463
464    serde::forward_to_deserialize_any! {
465        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
466        bytes byte_buf option unit unit_struct newtype_struct seq tuple
467        tuple_struct enum map struct identifier ignored_any
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    macro_rules! float {
476        ($f:expr) => {
477            Number::from_finite_f64($f)
478        };
479    }
480
481    macro_rules! int {
482        ($i:expr) => {
483            Number::from($i)
484        };
485    }
486
487    macro_rules! assert_op {
488        ($expr:expr, $expected:expr, $check:ident) => {
489            let result = $expr;
490            assert_eq!(result, $expected, "incorrect number op result");
491            assert!(result.$check());
492        };
493    }
494
495    #[test]
496    fn neg() {
497        assert_op!(-int!(1u64), int!(-1i64), is_i64);
498        assert_op!(-float!(1.5), float!(-1.5), is_f64);
499        assert_op!(-float!(1.0), int!(-1i64), is_i64);
500    }
501
502    #[test]
503    fn add() {
504        assert_op!(int!(1i64) + int!(2u64), int!(3), is_u64);
505        assert_op!(float!(1.5) + float!(1.5), int!(3), is_u64);
506        assert_op!(float!(1.5) + int!(-1i64), float!(0.5), is_f64);
507        assert_op!(int!(-1i64) + int!(-2i64), int!(-3i64), is_i64);
508    }
509
510    #[test]
511    fn sub() {
512        assert_op!(int!(1i64) - int!(2u64), int!(-1i64), is_i64);
513        assert_op!(int!(-1i64) - int!(-2i64), int!(1u64), is_u64);
514        assert_op!(float!(1.5) - float!(1.5), int!(0), is_u64);
515        assert_op!(float!(1.5) - int!(-1i64), float!(2.5), is_f64);
516    }
517
518    #[test]
519    fn mul() {
520        assert_op!(int!(-1i64) * int!(2u64), int!(-2i64), is_i64);
521        assert_op!(int!(-1i64) * int!(-2i64), int!(2u64), is_u64);
522        assert_op!(float!(1.5) * float!(1.5), float!(2.25), is_f64);
523        assert_op!(float!(1.5) * int!(-1i64), float!(-1.5), is_f64);
524    }
525
526    #[test]
527    fn div() {
528        assert_op!(int!(1u64) / int!(2u64), float!(0.5), is_f64);
529        assert_op!(float!(4.1) / float!(2.0), float!(2.05), is_f64);
530        assert_op!(int!(4u64) / int!(2u64), int!(2u64), is_u64);
531        assert_op!(int!(-4i64) / int!(2u64), int!(-2i64), is_i64);
532        assert_op!(float!(4.0) / float!(2.0), int!(2), is_u64);
533        assert_op!(float!(-4.0) / float!(2.0), int!(-2), is_i64);
534    }
535
536    #[test]
537    fn rem() {
538        assert_op!(int!(3u64) % int!(2u64), int!(1u64), is_u64);
539        assert_op!(
540            float!(4.1) % float!(2.0),
541            float!(0.099_999_999_999_999_64),
542            is_f64
543        );
544        assert_op!(int!(4u64) % int!(2u64), int!(0u64), is_u64);
545        assert_op!(int!(-4i64) % int!(3u64), int!(-1i64), is_i64);
546        assert_op!(float!(4.0) % float!(2.0), int!(0), is_u64);
547        assert_op!(float!(-4.0) % float!(3.0), int!(-1), is_i64);
548    }
549}