Skip to main content

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            N::PosInt(value) if value == i64::MIN.unsigned_abs() => N::NegInt(i64::MIN),
170            #[allow(clippy::cast_possible_wrap)]
171            N::PosInt(value) => N::NegInt(-(value as i64)),
172            N::NegInt(value) => N::from(-value),
173            N::Float(value) => N::Float(-value),
174        }
175    }
176}
177
178impl Add for N {
179    type Output = N;
180
181    fn add(self, rhs: Self) -> Self::Output {
182        match coerce(self, rhs) {
183            CoerceResult::PosInt(a, b) => N::PosInt(a + b),
184            CoerceResult::NegInt(a, b) => N::NegInt(a + b),
185            CoerceResult::Float(a, b) => N::from_finite_f64(a + b),
186        }
187    }
188}
189
190impl Sub for N {
191    type Output = N;
192
193    fn sub(self, rhs: Self) -> Self::Output {
194        match coerce(self, rhs) {
195            CoerceResult::PosInt(a, b) => {
196                if b > a {
197                    #[allow(clippy::cast_possible_wrap)]
198                    N::NegInt(a as i64 - b as i64)
199                } else {
200                    N::PosInt(a - b)
201                }
202            }
203            CoerceResult::NegInt(a, b) => N::from(a - b),
204            CoerceResult::Float(a, b) => N::from_finite_f64(a - b),
205        }
206    }
207}
208
209impl Mul for N {
210    type Output = N;
211
212    fn mul(self, rhs: Self) -> Self::Output {
213        match coerce(self, rhs) {
214            CoerceResult::PosInt(a, b) => N::PosInt(a * b),
215            CoerceResult::NegInt(a, b) => N::from(a * b),
216            CoerceResult::Float(a, b) => N::from_finite_f64(a * b),
217        }
218    }
219}
220
221impl Div for N {
222    type Output = N;
223
224    fn div(self, rhs: Self) -> Self::Output {
225        N::from_finite_f64(self.to_f64() / rhs.to_f64())
226    }
227}
228
229impl Rem for N {
230    type Output = N;
231
232    fn rem(self, rhs: Self) -> Self::Output {
233        match coerce(self, rhs) {
234            CoerceResult::PosInt(a, b) => N::PosInt(a % b),
235            CoerceResult::NegInt(a, b) => N::NegInt(a % b),
236            CoerceResult::Float(a, b) => N::from_finite_f64(a % b),
237        }
238    }
239}
240
241impl Number {
242    /// Creates a new `Number` from a `f64`. Returns `None` if the float is infinite or NaN.
243    ///
244    /// # Example
245    ///
246    /// ```
247    /// # use hcl_primitives::Number;
248    /// assert!(Number::from_f64(42.0).is_some());
249    /// assert!(Number::from_f64(f64::NAN).is_none());
250    /// assert!(Number::from_f64(f64::INFINITY).is_none());
251    /// assert!(Number::from_f64(f64::NEG_INFINITY).is_none());
252    /// ```
253    pub fn from_f64(f: f64) -> Option<Number> {
254        if f.is_finite() {
255            Some(Number::from_finite_f64(f))
256        } else {
257            None
258        }
259    }
260
261    pub(crate) fn from_finite_f64(f: f64) -> Number {
262        Number {
263            n: N::from_finite_f64(f),
264        }
265    }
266
267    /// Represents the `Number` as f64 if possible. Returns None otherwise.
268    #[inline]
269    pub fn as_f64(&self) -> Option<f64> {
270        Some(self.n.to_f64())
271    }
272
273    /// If the `Number` is an integer, represent it as i64 if possible. Returns None otherwise.
274    #[inline]
275    pub fn as_i64(&self) -> Option<i64> {
276        self.n.as_i64()
277    }
278
279    /// If the `Number` is an integer, represent it as u64 if possible. Returns None otherwise.
280    #[inline]
281    pub fn as_u64(&self) -> Option<u64> {
282        self.n.as_u64()
283    }
284
285    /// Returns true if the `Number` is a float.
286    ///
287    /// For any `Number` on which `is_f64` returns true, `as_f64` is guaranteed to return the
288    /// float value.
289    #[inline]
290    pub fn is_f64(&self) -> bool {
291        self.n.is_f64()
292    }
293
294    /// Returns true if the `Number` is an integer between `i64::MIN` and `i64::MAX`.
295    ///
296    /// For any `Number` on which `is_i64` returns true, `as_i64` is guaranteed to return the
297    /// integer value.
298    #[inline]
299    pub fn is_i64(&self) -> bool {
300        self.n.is_i64()
301    }
302
303    /// Returns true if the `Number` is an integer between zero and `u64::MAX`.
304    ///
305    /// For any `Number` on which `is_u64` returns true, `as_u64` is guaranteed to return the
306    /// integer value.
307    #[inline]
308    pub fn is_u64(&self) -> bool {
309        self.n.is_u64()
310    }
311
312    // Not public API. Used to generate better deserialization errors in `hcl-rs`.
313    #[cfg(feature = "serde")]
314    #[doc(hidden)]
315    #[cold]
316    pub fn unexpected(&self) -> Unexpected<'_> {
317        match self.n {
318            N::PosInt(v) => Unexpected::Unsigned(v),
319            N::NegInt(v) => Unexpected::Signed(v),
320            N::Float(v) => Unexpected::Float(v),
321        }
322    }
323}
324
325macro_rules! impl_from_unsigned {
326    ($($ty:ty),*) => {
327        $(
328            impl From<$ty> for Number {
329                #[inline]
330                fn from(u: $ty) -> Self {
331                    Number {
332                        #[allow(clippy::cast_lossless)]
333                        n: N::PosInt(u as u64)
334                    }
335                }
336            }
337        )*
338    };
339}
340
341macro_rules! impl_from_signed {
342    ($($ty:ty),*) => {
343        $(
344            impl From<$ty> for Number {
345                #[inline]
346                fn from(i: $ty) -> Self {
347                    Number {
348                        #[allow(clippy::cast_lossless)]
349                        n: N::from(i as i64)
350                    }
351                }
352            }
353        )*
354    };
355}
356
357macro_rules! impl_binary_ops {
358    ($($op:ty => $method:ident),*) => {
359        $(
360            impl $op for Number {
361                type Output = Number;
362
363                fn $method(self, rhs: Self) -> Self::Output {
364                    Number {
365                        n: self.n.$method(rhs.n)
366                    }
367                }
368            }
369        )*
370    };
371}
372
373impl_from_unsigned!(u8, u16, u32, u64, usize);
374impl_from_signed!(i8, i16, i32, i64, isize);
375impl_binary_ops!(Add => add, Sub => sub, Mul => mul, Div => div, Rem => rem);
376
377impl Neg for Number {
378    type Output = Number;
379
380    fn neg(self) -> Self::Output {
381        Number { n: -self.n }
382    }
383}
384
385impl fmt::Display for Number {
386    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
387        match self.n {
388            N::PosInt(v) => f.write_str(itoa::Buffer::new().format(v)),
389            N::NegInt(v) => f.write_str(itoa::Buffer::new().format(v)),
390            N::Float(v) => f.write_str(ryu::Buffer::new().format_finite(v)),
391        }
392    }
393}
394
395impl fmt::Debug for Number {
396    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
397        write!(f, "Number({self})")
398    }
399}
400
401#[cfg(feature = "serde")]
402impl serde::Serialize for Number {
403    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
404    where
405        S: serde::Serializer,
406    {
407        match self.n {
408            N::PosInt(v) => serializer.serialize_u64(v),
409            N::NegInt(v) => serializer.serialize_i64(v),
410            N::Float(v) => serializer.serialize_f64(v),
411        }
412    }
413}
414
415#[cfg(feature = "serde")]
416impl<'de> serde::Deserialize<'de> for Number {
417    fn deserialize<D>(deserializer: D) -> Result<Number, D::Error>
418    where
419        D: serde::Deserializer<'de>,
420    {
421        struct NumberVisitor;
422
423        impl serde::de::Visitor<'_> for NumberVisitor {
424            type Value = Number;
425
426            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
427                formatter.write_str("an HCL number")
428            }
429
430            fn visit_i64<E>(self, value: i64) -> Result<Number, E> {
431                Ok(value.into())
432            }
433
434            fn visit_u64<E>(self, value: u64) -> Result<Number, E> {
435                Ok(value.into())
436            }
437
438            fn visit_f64<E>(self, value: f64) -> Result<Number, E>
439            where
440                E: serde::de::Error,
441            {
442                Number::from_f64(value).ok_or_else(|| serde::de::Error::custom("not an HCL number"))
443            }
444        }
445
446        deserializer.deserialize_any(NumberVisitor)
447    }
448}
449
450#[cfg(feature = "serde")]
451impl<'de> serde::Deserializer<'de> for Number {
452    type Error = super::Error;
453
454    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
455    where
456        V: serde::de::Visitor<'de>,
457    {
458        match self.n {
459            N::PosInt(i) => visitor.visit_u64(i),
460            N::NegInt(i) => visitor.visit_i64(i),
461            N::Float(f) => visitor.visit_f64(f),
462        }
463    }
464
465    serde::forward_to_deserialize_any! {
466        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
467        bytes byte_buf option unit unit_struct newtype_struct seq tuple
468        tuple_struct enum map struct identifier ignored_any
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    macro_rules! float {
477        ($f:expr) => {
478            Number::from_finite_f64($f)
479        };
480    }
481
482    macro_rules! int {
483        ($i:expr) => {
484            Number::from($i)
485        };
486    }
487
488    macro_rules! assert_op {
489        ($expr:expr, $expected:expr, $check:ident) => {
490            let result = $expr;
491            assert_eq!(result, $expected, "incorrect number op result");
492            assert!(result.$check());
493        };
494    }
495
496    #[test]
497    fn neg() {
498        assert_op!(-int!(1u64), int!(-1i64), is_i64);
499        assert_op!(-float!(1.5), float!(-1.5), is_f64);
500        assert_op!(-float!(1.0), int!(-1i64), is_i64);
501        assert_op!(-int!(9_223_372_036_854_775_808u64), int!(i64::MIN), is_i64);
502    }
503
504    #[test]
505    fn add() {
506        assert_op!(int!(1i64) + int!(2u64), int!(3), is_u64);
507        assert_op!(float!(1.5) + float!(1.5), int!(3), is_u64);
508        assert_op!(float!(1.5) + int!(-1i64), float!(0.5), is_f64);
509        assert_op!(int!(-1i64) + int!(-2i64), int!(-3i64), is_i64);
510    }
511
512    #[test]
513    fn sub() {
514        assert_op!(int!(1i64) - int!(2u64), int!(-1i64), is_i64);
515        assert_op!(int!(-1i64) - int!(-2i64), int!(1u64), is_u64);
516        assert_op!(float!(1.5) - float!(1.5), int!(0), is_u64);
517        assert_op!(float!(1.5) - int!(-1i64), float!(2.5), is_f64);
518    }
519
520    #[test]
521    fn mul() {
522        assert_op!(int!(-1i64) * int!(2u64), int!(-2i64), is_i64);
523        assert_op!(int!(-1i64) * int!(-2i64), int!(2u64), is_u64);
524        assert_op!(float!(1.5) * float!(1.5), float!(2.25), is_f64);
525        assert_op!(float!(1.5) * int!(-1i64), float!(-1.5), is_f64);
526    }
527
528    #[test]
529    fn div() {
530        assert_op!(int!(1u64) / int!(2u64), float!(0.5), is_f64);
531        assert_op!(float!(4.1) / float!(2.0), float!(2.05), is_f64);
532        assert_op!(int!(4u64) / int!(2u64), int!(2u64), is_u64);
533        assert_op!(int!(-4i64) / int!(2u64), int!(-2i64), is_i64);
534        assert_op!(float!(4.0) / float!(2.0), int!(2), is_u64);
535        assert_op!(float!(-4.0) / float!(2.0), int!(-2), is_i64);
536    }
537
538    #[test]
539    fn rem() {
540        assert_op!(int!(3u64) % int!(2u64), int!(1u64), is_u64);
541        assert_op!(
542            float!(4.1) % float!(2.0),
543            float!(0.099_999_999_999_999_64),
544            is_f64
545        );
546        assert_op!(int!(4u64) % int!(2u64), int!(0u64), is_u64);
547        assert_op!(int!(-4i64) % int!(3u64), int!(-1i64), is_i64);
548        assert_op!(float!(4.0) % float!(2.0), int!(0), is_u64);
549        assert_op!(float!(-4.0) % float!(3.0), int!(-1), is_i64);
550    }
551}