aws_smithy_types/
number.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! A number type that implements Javascript / JSON semantics.
7
8use crate::error::{TryFromNumberError, TryFromNumberErrorKind};
9#[cfg(all(
10    aws_sdk_unstable,
11    any(feature = "serde-serialize", feature = "serde-deserialize")
12))]
13use serde;
14
15/// A number type that implements Javascript / JSON semantics, modeled on serde_json:
16/// <https://docs.serde.rs/src/serde_json/number.rs.html#20-22>
17#[derive(Debug, Clone, Copy, PartialEq)]
18#[cfg_attr(
19    all(aws_sdk_unstable, feature = "serde-deserialize"),
20    derive(serde::Deserialize)
21)]
22#[cfg_attr(
23    all(aws_sdk_unstable, feature = "serde-serialize"),
24    derive(serde::Serialize)
25)]
26#[cfg_attr(
27    any(
28        all(aws_sdk_unstable, feature = "serde-deserialize"),
29        all(aws_sdk_unstable, feature = "serde-serialize")
30    ),
31    serde(untagged)
32)]
33pub enum Number {
34    /// Unsigned 64-bit integer value.
35    PosInt(u64),
36    /// Signed 64-bit integer value. The wrapped value is _always_ negative.
37    NegInt(i64),
38    /// 64-bit floating-point value.
39    Float(f64),
40}
41
42/* ANCHOR_END: document */
43
44impl Number {
45    /// Converts to an `f64` lossily.
46    /// Use `Number::try_from` to make the conversion only if it is not lossy.
47    pub fn to_f64_lossy(self) -> f64 {
48        match self {
49            Number::PosInt(v) => v as f64,
50            Number::NegInt(v) => v as f64,
51            Number::Float(v) => v,
52        }
53    }
54
55    /// Converts to an `f32` lossily.
56    /// Use `Number::try_from` to make the conversion only if it is not lossy.
57    pub fn to_f32_lossy(self) -> f32 {
58        match self {
59            Number::PosInt(v) => v as f32,
60            Number::NegInt(v) => v as f32,
61            Number::Float(v) => v as f32,
62        }
63    }
64}
65
66macro_rules! to_unsigned_integer_converter {
67    ($typ:ident, $styp:expr) => {
68        #[doc = "Converts to a `"]
69        #[doc = $styp]
70        #[doc = "`. This conversion fails if it is lossy."]
71        impl TryFrom<Number> for $typ {
72            type Error = TryFromNumberError;
73
74            fn try_from(value: Number) -> Result<Self, Self::Error> {
75                match value {
76                    Number::PosInt(v) => Ok(Self::try_from(v)?),
77                    Number::NegInt(v) => {
78                        Err(TryFromNumberErrorKind::NegativeToUnsignedLossyConversion(v).into())
79                    }
80                    Number::Float(v) => attempt_lossless!(v, $typ),
81                }
82            }
83        }
84    };
85
86    ($typ:ident) => {
87        to_unsigned_integer_converter!($typ, stringify!($typ));
88    };
89}
90
91macro_rules! to_signed_integer_converter {
92    ($typ:ident, $styp:expr) => {
93        #[doc = "Converts to a `"]
94        #[doc = $styp]
95        #[doc = "`. This conversion fails if it is lossy."]
96        impl TryFrom<Number> for $typ {
97            type Error = TryFromNumberError;
98
99            fn try_from(value: Number) -> Result<Self, Self::Error> {
100                match value {
101                    Number::PosInt(v) => Ok(Self::try_from(v)?),
102                    Number::NegInt(v) => Ok(Self::try_from(v)?),
103                    Number::Float(v) => attempt_lossless!(v, $typ),
104                }
105            }
106        }
107    };
108
109    ($typ:ident) => {
110        to_signed_integer_converter!($typ, stringify!($typ));
111    };
112}
113
114macro_rules! attempt_lossless {
115    ($value: expr, $typ: ty) => {{
116        let converted = $value as $typ;
117        if (converted as f64 == $value) {
118            Ok(converted)
119        } else {
120            Err(TryFromNumberErrorKind::FloatToIntegerLossyConversion($value).into())
121        }
122    }};
123}
124
125/// Converts to a `u64`. The conversion fails if it is lossy.
126impl TryFrom<Number> for u64 {
127    type Error = TryFromNumberError;
128
129    fn try_from(value: Number) -> Result<Self, Self::Error> {
130        match value {
131            Number::PosInt(v) => Ok(v),
132            Number::NegInt(v) => {
133                Err(TryFromNumberErrorKind::NegativeToUnsignedLossyConversion(v).into())
134            }
135            Number::Float(v) => attempt_lossless!(v, u64),
136        }
137    }
138}
139to_unsigned_integer_converter!(u32);
140to_unsigned_integer_converter!(u16);
141to_unsigned_integer_converter!(u8);
142
143impl TryFrom<Number> for i64 {
144    type Error = TryFromNumberError;
145
146    fn try_from(value: Number) -> Result<Self, Self::Error> {
147        match value {
148            Number::PosInt(v) => Ok(Self::try_from(v)?),
149            Number::NegInt(v) => Ok(v),
150            Number::Float(v) => attempt_lossless!(v, i64),
151        }
152    }
153}
154to_signed_integer_converter!(i32);
155to_signed_integer_converter!(i16);
156to_signed_integer_converter!(i8);
157
158/// Converts to an `f64`. The conversion fails if it is lossy.
159impl TryFrom<Number> for f64 {
160    type Error = TryFromNumberError;
161
162    fn try_from(value: Number) -> Result<Self, Self::Error> {
163        match value {
164            // Integers can only be represented with full precision in a float if they fit in the
165            // significand, which is 24 bits in `f32` and 53 bits in `f64`.
166            // https://github.com/rust-lang/rust/blob/58f11791af4f97572e7afd83f11cffe04bbbd12f/library/core/src/convert/num.rs#L151-L153
167            Number::PosInt(v) => {
168                if v <= (1 << 53) {
169                    Ok(v as Self)
170                } else {
171                    Err(TryFromNumberErrorKind::U64ToFloatLossyConversion(v).into())
172                }
173            }
174            Number::NegInt(v) => {
175                if (-(1 << 53)..=(1 << 53)).contains(&v) {
176                    Ok(v as Self)
177                } else {
178                    Err(TryFromNumberErrorKind::I64ToFloatLossyConversion(v).into())
179                }
180            }
181            Number::Float(v) => Ok(v),
182        }
183    }
184}
185
186/// Converts to an `f64`. The conversion fails if it is lossy.
187impl TryFrom<Number> for f32 {
188    type Error = TryFromNumberError;
189
190    fn try_from(value: Number) -> Result<Self, Self::Error> {
191        match value {
192            Number::PosInt(v) => {
193                if v <= (1 << 24) {
194                    Ok(v as Self)
195                } else {
196                    Err(TryFromNumberErrorKind::U64ToFloatLossyConversion(v).into())
197                }
198            }
199            Number::NegInt(v) => {
200                if (-(1 << 24)..=(1 << 24)).contains(&v) {
201                    Ok(v as Self)
202                } else {
203                    Err(TryFromNumberErrorKind::I64ToFloatLossyConversion(v).into())
204                }
205            }
206            Number::Float(v) => Err(TryFromNumberErrorKind::F64ToF32LossyConversion(v).into()),
207        }
208    }
209}
210
211#[cfg(test)]
212mod test {
213    use super::Number;
214    use crate::error::{TryFromNumberError, TryFromNumberErrorKind};
215
216    macro_rules! to_unsigned_converter_tests {
217        ($typ:ident) => {
218            assert_eq!($typ::try_from(Number::PosInt(69u64)).unwrap(), 69);
219
220            assert!(matches!(
221                $typ::try_from(Number::PosInt(($typ::MAX as u64) + 1u64)).unwrap_err(),
222                TryFromNumberError {
223                    kind: TryFromNumberErrorKind::OutsideIntegerRange(..)
224                }
225            ));
226
227            assert!(matches!(
228                $typ::try_from(Number::NegInt(-1i64)).unwrap_err(),
229                TryFromNumberError {
230                    kind: TryFromNumberErrorKind::NegativeToUnsignedLossyConversion(..)
231                }
232            ));
233
234            for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
235                assert!(matches!(
236                    $typ::try_from(Number::Float(val)).unwrap_err(),
237                    TryFromNumberError {
238                        kind: TryFromNumberErrorKind::FloatToIntegerLossyConversion(..)
239                    }
240                ));
241            }
242            assert_eq!($typ::try_from(Number::Float(25.0)).unwrap(), 25);
243        };
244    }
245
246    #[test]
247    fn to_u64() {
248        assert_eq!(u64::try_from(Number::PosInt(69u64)).unwrap(), 69u64);
249
250        assert!(matches!(
251            u64::try_from(Number::NegInt(-1i64)).unwrap_err(),
252            TryFromNumberError {
253                kind: TryFromNumberErrorKind::NegativeToUnsignedLossyConversion(..)
254            }
255        ));
256
257        for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
258            assert!(matches!(
259                u64::try_from(Number::Float(val)).unwrap_err(),
260                TryFromNumberError {
261                    kind: TryFromNumberErrorKind::FloatToIntegerLossyConversion(..)
262                }
263            ));
264        }
265    }
266
267    #[test]
268    fn to_u32() {
269        to_unsigned_converter_tests!(u32);
270    }
271
272    #[test]
273    fn to_u16() {
274        to_unsigned_converter_tests!(u16);
275    }
276
277    #[test]
278    fn to_u8() {
279        to_unsigned_converter_tests!(u8);
280    }
281
282    macro_rules! to_signed_converter_tests {
283        ($typ:ident) => {
284            assert_eq!($typ::try_from(Number::PosInt(69u64)).unwrap(), 69);
285            assert_eq!($typ::try_from(Number::NegInt(-69i64)).unwrap(), -69);
286
287            assert!(matches!(
288                $typ::try_from(Number::PosInt(($typ::MAX as u64) + 1u64)).unwrap_err(),
289                TryFromNumberError {
290                    kind: TryFromNumberErrorKind::OutsideIntegerRange(..)
291                }
292            ));
293
294            assert!(matches!(
295                $typ::try_from(Number::NegInt(($typ::MIN as i64) - 1i64)).unwrap_err(),
296                TryFromNumberError {
297                    kind: TryFromNumberErrorKind::OutsideIntegerRange(..)
298                }
299            ));
300
301            for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
302                assert!(matches!(
303                    u64::try_from(Number::Float(val)).unwrap_err(),
304                    TryFromNumberError {
305                        kind: TryFromNumberErrorKind::FloatToIntegerLossyConversion(..)
306                    }
307                ));
308            }
309
310            let range = || ($typ::MIN..=$typ::MAX);
311
312            for val in range().take(1024).chain(range().rev().take(1024)) {
313                assert_eq!(val, $typ::try_from(Number::Float(val as f64)).unwrap());
314                $typ::try_from(Number::Float((val as f64) + 0.1)).expect_err("not equivalent");
315            }
316        };
317    }
318
319    #[test]
320    fn to_i64() {
321        assert_eq!(i64::try_from(Number::PosInt(69u64)).unwrap(), 69);
322        assert_eq!(i64::try_from(Number::NegInt(-69i64)).unwrap(), -69);
323
324        for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
325            assert!(matches!(
326                u64::try_from(Number::Float(val)).unwrap_err(),
327                TryFromNumberError {
328                    kind: TryFromNumberErrorKind::FloatToIntegerLossyConversion(..)
329                }
330            ));
331        }
332        let range = || (i64::MIN..=i64::MAX);
333
334        for val in range().take(1024).chain(range().rev().take(1024)) {
335            // if we can actually represent the value
336            if ((val as f64) as i64) == val {
337                assert_eq!(val, i64::try_from(Number::Float(val as f64)).unwrap());
338            }
339            let fval = val as f64;
340            // at the limits of the range, we don't have this precision
341            if (fval + 0.1).fract() != 0.0 {
342                i64::try_from(Number::Float((val as f64) + 0.1)).expect_err("not equivalent");
343            }
344        }
345    }
346
347    #[test]
348    fn to_i32() {
349        to_signed_converter_tests!(i32);
350    }
351
352    #[test]
353    fn to_i16() {
354        to_signed_converter_tests!(i16);
355    }
356
357    #[test]
358    fn to_i8() {
359        to_signed_converter_tests!(i8);
360        i8::try_from(Number::Float(-3200000.0)).expect_err("overflow");
361        i8::try_from(Number::Float(32.1)).expect_err("imprecise");
362        i8::try_from(Number::Float(i8::MAX as f64 + 0.1)).expect_err("imprecise");
363        i8::try_from(Number::Float(f64::NAN)).expect_err("nan");
364        i8::try_from(Number::Float(f64::INFINITY)).expect_err("nan");
365    }
366
367    #[test]
368    fn to_f64() {
369        assert_eq!(f64::try_from(Number::PosInt(69u64)).unwrap(), 69f64);
370        assert_eq!(f64::try_from(Number::NegInt(-69i64)).unwrap(), -69f64);
371        assert_eq!(f64::try_from(Number::Float(-69f64)).unwrap(), -69f64);
372        assert!(f64::try_from(Number::Float(f64::NAN)).unwrap().is_nan());
373        assert_eq!(
374            f64::try_from(Number::Float(f64::INFINITY)).unwrap(),
375            f64::INFINITY
376        );
377        assert_eq!(
378            f64::try_from(Number::Float(f64::NEG_INFINITY)).unwrap(),
379            f64::NEG_INFINITY
380        );
381
382        let significand_max_u64: u64 = 1 << 53;
383        let significand_max_i64: i64 = 1 << 53;
384
385        assert_eq!(
386            f64::try_from(Number::PosInt(significand_max_u64)).unwrap(),
387            9007199254740992f64
388        );
389
390        assert_eq!(
391            f64::try_from(Number::NegInt(significand_max_i64)).unwrap(),
392            9007199254740992f64
393        );
394        assert_eq!(
395            f64::try_from(Number::NegInt(-significand_max_i64)).unwrap(),
396            -9007199254740992f64
397        );
398
399        assert!(matches!(
400            f64::try_from(Number::PosInt(significand_max_u64 + 1)).unwrap_err(),
401            TryFromNumberError {
402                kind: TryFromNumberErrorKind::U64ToFloatLossyConversion(..)
403            }
404        ));
405
406        assert!(matches!(
407            f64::try_from(Number::NegInt(significand_max_i64 + 1)).unwrap_err(),
408            TryFromNumberError {
409                kind: TryFromNumberErrorKind::I64ToFloatLossyConversion(..)
410            }
411        ));
412        assert!(matches!(
413            f64::try_from(Number::NegInt(-significand_max_i64 - 1)).unwrap_err(),
414            TryFromNumberError {
415                kind: TryFromNumberErrorKind::I64ToFloatLossyConversion(..)
416            }
417        ));
418    }
419
420    #[test]
421    fn to_f32() {
422        assert_eq!(f32::try_from(Number::PosInt(69u64)).unwrap(), 69f32);
423        assert_eq!(f32::try_from(Number::NegInt(-69i64)).unwrap(), -69f32);
424
425        let significand_max_u64: u64 = 1 << 24;
426        let significand_max_i64: i64 = 1 << 24;
427
428        assert_eq!(
429            f32::try_from(Number::PosInt(significand_max_u64)).unwrap(),
430            16777216f32
431        );
432
433        assert_eq!(
434            f32::try_from(Number::NegInt(significand_max_i64)).unwrap(),
435            16777216f32
436        );
437        assert_eq!(
438            f32::try_from(Number::NegInt(-significand_max_i64)).unwrap(),
439            -16777216f32
440        );
441
442        assert!(matches!(
443            f32::try_from(Number::PosInt(significand_max_u64 + 1)).unwrap_err(),
444            TryFromNumberError {
445                kind: TryFromNumberErrorKind::U64ToFloatLossyConversion(..)
446            }
447        ));
448
449        assert!(matches!(
450            f32::try_from(Number::NegInt(significand_max_i64 + 1)).unwrap_err(),
451            TryFromNumberError {
452                kind: TryFromNumberErrorKind::I64ToFloatLossyConversion(..)
453            }
454        ));
455        assert!(matches!(
456            f32::try_from(Number::NegInt(-significand_max_i64 - 1)).unwrap_err(),
457            TryFromNumberError {
458                kind: TryFromNumberErrorKind::I64ToFloatLossyConversion(..)
459            }
460        ));
461
462        for val in [69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
463            assert!(matches!(
464                f32::try_from(Number::Float(val)).unwrap_err(),
465                TryFromNumberError {
466                    kind: TryFromNumberErrorKind::F64ToF32LossyConversion(..)
467                }
468            ));
469        }
470    }
471
472    #[test]
473    fn to_f64_lossy() {
474        assert_eq!(Number::PosInt(69u64).to_f64_lossy(), 69f64);
475        assert_eq!(
476            Number::PosInt((1 << 53) + 1).to_f64_lossy(),
477            9007199254740992f64
478        );
479        assert_eq!(
480            Number::NegInt(-(1 << 53) - 1).to_f64_lossy(),
481            -9007199254740992f64
482        );
483    }
484
485    #[test]
486    fn to_f32_lossy() {
487        assert_eq!(Number::PosInt(69u64).to_f32_lossy(), 69f32);
488        assert_eq!(Number::PosInt((1 << 24) + 1).to_f32_lossy(), 16777216f32);
489        assert_eq!(Number::NegInt(-(1 << 24) - 1).to_f32_lossy(), -16777216f32);
490        assert_eq!(
491            Number::Float(1452089033.7674935).to_f32_lossy(),
492            1452089100f32
493        );
494    }
495
496    #[test]
497    #[cfg(all(
498        test,
499        aws_sdk_unstable,
500        feature = "serde-deserialize",
501        feature = "serde-serialize"
502    ))]
503    /// ensures that numbers are deserialized as expected
504    /// 0 <= PosInt
505    /// 0 > NegInt
506    /// non integer values == Float
507    fn number_serde() {
508        let n: Number = serde_json::from_str("1.1").unwrap();
509        assert_eq!(n, Number::Float(1.1));
510        let n: Number = serde_json::from_str("1").unwrap();
511        assert_eq!(n, Number::PosInt(1));
512        let n: Number = serde_json::from_str("0").unwrap();
513        assert_eq!(n, Number::PosInt(0));
514        let n: Number = serde_json::from_str("-1").unwrap();
515        assert_eq!(n, Number::NegInt(-1));
516
517        assert_eq!("1.1", serde_json::to_string(&Number::Float(1.1)).unwrap());
518        assert_eq!("1", serde_json::to_string(&Number::PosInt(1)).unwrap());
519        assert_eq!("0", serde_json::to_string(&Number::PosInt(0)).unwrap());
520        assert_eq!("-1", serde_json::to_string(&Number::NegInt(-1)).unwrap());
521    }
522}