diff --git a/CHANGELOG.next.toml b/CHANGELOG.next.toml index cee98e207f..ac2bd6bacc 100644 --- a/CHANGELOG.next.toml +++ b/CHANGELOG.next.toml @@ -117,3 +117,63 @@ There is a canonical and easier way to run smithy-rs on Lambda [see example]. references = ["smithy-rs#1551"] meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "server" } author = "hugobast" + +[[smithy-rs]] +message = """ +Lossy converters into integer types for `aws_smithy_types::Number` have been +removed. Lossy converters into floating point types for +`aws_smithy_types::Number` have been suffixed with `_lossy`. If you were +directly using the integer lossy converters, we recommend you use the safe +converters. +_Before:_ +```rust +fn f1(n: aws_smithy_types::Number) { + let foo: f32 = n.to_f32(); // Lossy conversion! + let bar: u32 = n.to_u32(); // Lossy conversion! +} +``` +_After:_ +```rust +fn f1(n: aws_smithy_types::Number) { + use std::convert::TryInto; // Unnecessary import if you're using Rust 2021 edition. + let foo: f32 = n.try_into().expect("lossy conversion detected"); // Or handle the error instead of panicking. + // You can still do lossy conversions, but only into floating point types. + let foo: f32 = n.to_f32_lossy(); + // To lossily convert into integer types, use an `as` cast directly. + let bar: u32 = n as u32; // Lossy conversion! +} +``` +""" +references = ["smithy-rs#1274"] +meta = { "breaking" = true, "tada" = false, "bug" = true, "target" = "all" } +author = "david-perez" + +[[aws-sdk-rust]] +message = """ +Lossy converters into integer types for `aws_smithy_types::Number` have been +removed. Lossy converters into floating point types for +`aws_smithy_types::Number` have been suffixed with `_lossy`. If you were +directly using the integer lossy converters, we recommend you use the safe +converters. +_Before:_ +```rust +fn f1(n: aws_smithy_types::Number) { + let foo: f32 = n.to_f32(); // Lossy conversion! + let bar: u32 = n.to_u32(); // Lossy conversion! +} +``` +_After:_ +```rust +fn f1(n: aws_smithy_types::Number) { + use std::convert::TryInto; // Unnecessary import if you're using Rust 2021 edition. + let foo: f32 = n.try_into().expect("lossy conversion detected"); // Or handle the error instead of panicking. + // You can still do lossy conversions, but only into floating point types. + let foo: f32 = n.to_f32_lossy(); + // To lossily convert into integer types, use an `as` cast directly. + let bar: u32 = n as u32; // Lossy conversion! +} +``` +""" +references = ["smithy-rs#1274"] +meta = { "breaking" = true, "tada" = false, "bug" = true } +author = "david-perez" diff --git a/aws/rust-runtime/aws-config/src/credential_process.rs b/aws/rust-runtime/aws-config/src/credential_process.rs index fc3409b401..ab4727b309 100644 --- a/aws/rust-runtime/aws-config/src/credential_process.rs +++ b/aws/rust-runtime/aws-config/src/credential_process.rs @@ -194,7 +194,12 @@ pub(crate) fn parse_credential_process_json_credentials( "Expiration": "2022-05-02T18:36:00+00:00" */ (key, Token::ValueNumber { value, .. }) if key.eq_ignore_ascii_case("Version") => { - version = Some(value.to_i32()) + version = Some(i32::try_from(*value).map_err(|err| { + InvalidJsonCredentials::InvalidField { + field: "Version", + err: err.into(), + } + })?); } (key, Token::ValueString { value, .. }) if key.eq_ignore_ascii_case("AccessKeyId") => { access_key_id = Some(value.to_unescaped()?) diff --git a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt index fbd1b3709e..dee9f50ada 100644 --- a/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt +++ b/codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt @@ -661,40 +661,14 @@ class ServerProtocolTestGenerator( FailingTest(RestJson, "RestJsonWithPayloadExpectsImpliedAccept", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonBodyMalformedBlobInvalidBase64_case1", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonBodyMalformedBlobInvalidBase64_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteMalformedValueRejected_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteMalformedValueRejected_case6", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteMalformedValueRejected_case8", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteMalformedValueRejected_case10", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteUnderflowOverflow_case0", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteUnderflowOverflow_case1", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteUnderflowOverflow_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyByteUnderflowOverflow_case3", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonWithBodyExpectsApplicationJsonContentType", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonWithPayloadExpectsImpliedContentType", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonWithPayloadExpectsModeledContentType", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonWithoutBodyExpectsEmptyContentType", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyIntegerMalformedValueRejected_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyIntegerMalformedValueRejected_case6", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyIntegerMalformedValueRejected_case8", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyIntegerMalformedValueRejected_case10", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyIntegerUnderflowOverflow_case0", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyIntegerUnderflowOverflow_case1", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonBodyMalformedListNullItem", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonBodyMalformedMapNullValue", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyLongMalformedValueRejected_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyLongMalformedValueRejected_case6", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyLongMalformedValueRejected_case8", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyLongMalformedValueRejected_case10", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonMalformedSetDuplicateItems", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonMalformedSetNullItem", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortMalformedValueRejected_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortMalformedValueRejected_case6", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortMalformedValueRejected_case8", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortMalformedValueRejected_case10", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortUnderflowOverflow_case0", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortUnderflowOverflow_case1", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortUnderflowOverflow_case2", TestType.MalformedRequest), - FailingTest(RestJson, "RestJsonBodyShortUnderflowOverflow_case3", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonHeaderMalformedStringInvalidBase64MediaType_case1", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonBodyTimestampDefaultRejectsMalformedEpochSeconds_case5", TestType.MalformedRequest), FailingTest(RestJson, "RestJsonBodyTimestampDefaultRejectsMalformedEpochSeconds_case7", TestType.MalformedRequest), diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt index c668a67906..aa7f082e04 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/JsonParserGenerator.kt @@ -278,8 +278,20 @@ class JsonParserGenerator( } private fun RustWriter.deserializeNumber(target: NumberShape) { - val symbol = symbolProvider.toSymbol(target) - rustTemplate("#{expect_number_or_null}(tokens.next())?.map(|v| v.to_#{T}())", "T" to symbol, *codegenScope) + if (target.isFloatShape) { + rustTemplate("#{expect_number_or_null}(tokens.next())?.map(|v| v.to_f32_lossy())", *codegenScope) + } else if (target.isDoubleShape) { + rustTemplate("#{expect_number_or_null}(tokens.next())?.map(|v| v.to_f64_lossy())", *codegenScope) + } else { + rustTemplate( + """ + #{expect_number_or_null}(tokens.next())? + .map(|v| v.try_into()) + .transpose()? + """, + *codegenScope, + ) + } } private fun RustWriter.deserializeTimestamp(shape: TimestampShape, member: MemberShape) { diff --git a/rust-runtime/aws-smithy-json/src/deserialize.rs b/rust-runtime/aws-smithy-json/src/deserialize.rs index 0d73d778b3..ac6d0b337c 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize.rs @@ -282,7 +282,7 @@ impl<'a> JsonTokenIterator<'a> { /// returns `(start_index, end_index, negative, floating)`, with `start_index` /// and `end_index` representing the slice of the stream that is the number, /// `negative` whether or not it is a negative number, and `floating` whether or not - /// it is a floating point number. + /// the number contains a decimal point and/or an exponent. fn scan_number(&mut self) -> (usize, usize, bool, bool) { let start_index = self.index; let negative = if self.peek_byte() == Some(b'-') { @@ -338,7 +338,7 @@ impl<'a> JsonTokenIterator<'a> { if negative > 0 { Number::Float(-(positive as f64)) } else { - Number::NegInt(negative as i64) + Number::NegInt(negative) } } else { Number::PosInt( diff --git a/rust-runtime/aws-smithy-json/src/deserialize/error.rs b/rust-runtime/aws-smithy-json/src/deserialize/error.rs index 809ee8f610..957effcc5b 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize/error.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize/error.rs @@ -82,3 +82,12 @@ impl From for Error { } } } + +impl From for Error { + fn from(_: aws_smithy_types::TryFromNumberError) -> Self { + Error { + reason: ErrorReason::InvalidNumber, + offset: None, + } + } +} diff --git a/rust-runtime/aws-smithy-json/src/deserialize/token.rs b/rust-runtime/aws-smithy-json/src/deserialize/token.rs index 0d437055fe..850861f6f6 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize/token.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize/token.rs @@ -217,7 +217,7 @@ pub fn expect_timestamp_or_null( ) -> Result, Error> { Ok(match timestamp_format { Format::EpochSeconds => { - expect_number_or_null(token)?.map(|v| DateTime::from_secs_f64(v.to_f64())) + expect_number_or_null(token)?.map(|v| DateTime::from_secs_f64(v.to_f64_lossy())) } Format::DateTime | Format::HttpDate => expect_string_or_null(token)? .map(|v| DateTime::from_str(v.as_escaped_str(), timestamp_format)) diff --git a/rust-runtime/aws-smithy-types/src/lib.rs b/rust-runtime/aws-smithy-types/src/lib.rs index 6e64a2aa4b..570e4e78fd 100644 --- a/rust-runtime/aws-smithy-types/src/lib.rs +++ b/rust-runtime/aws-smithy-types/src/lib.rs @@ -80,49 +80,455 @@ pub enum Document { /// #[derive(Debug, Clone, Copy, PartialEq)] pub enum Number { - /// Unsigned 64-bit integer value + /// Unsigned 64-bit integer value. PosInt(u64), - /// Signed 64-bit integer value + /// Signed 64-bit integer value. The wrapped value is _always_ negative. NegInt(i64), - /// 64-bit floating-point value + /// 64-bit floating-point value. Float(f64), } -macro_rules! to_num_fn { - ($name:ident, $typ:ident, $styp:expr) => { +/* ANCHOR_END: document */ + +impl Number { + /// Converts to an `f64` lossily. + /// Use `Number::try_from` to make the conversion only if it is not lossy. + pub fn to_f64_lossy(self) -> f64 { + match self { + Number::PosInt(v) => v as f64, + Number::NegInt(v) => v as f64, + Number::Float(v) => v as f64, + } + } + + /// Converts to an `f32` lossily. + /// Use `Number::try_from` to make the conversion only if it is not lossy. + pub fn to_f32_lossy(self) -> f32 { + match self { + Number::PosInt(v) => v as f32, + Number::NegInt(v) => v as f32, + Number::Float(v) => v as f32, + } + } +} + +/// The error type returned when conversion into an integer type or floating point type is lossy. +#[derive(Debug)] +pub enum TryFromNumberError { + /// Used when the conversion from an integer type into a smaller integer type would be lossy. + OutsideIntegerRange(std::num::TryFromIntError), + /// Used when the conversion from an `u64` into a floating point type would be lossy. + U64ToFloatLossyConversion(u64), + /// Used when the conversion from an `i64` into a floating point type would be lossy. + I64ToFloatLossyConversion(i64), + /// Used when attempting to convert an `f64` into an `f32`. + F64ToF32LossyConversion(f64), + /// Used when attempting to convert a decimal, infinite, or `NaN` floating point type into an + /// integer type. + FloatToIntegerLossyConversion(f64), + /// Used when attempting to convert a negative [`Number`] into an unsigned integer type. + NegativeToUnsignedLossyConversion(i64), +} + +impl std::fmt::Display for TryFromNumberError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TryFromNumberError::OutsideIntegerRange(err) => write!(f, "integer too large: {}", err), + TryFromNumberError::FloatToIntegerLossyConversion(v) => write!( + f, + "cannot convert floating point number {} into an integer", + v + ), + TryFromNumberError::NegativeToUnsignedLossyConversion(v) => write!( + f, + "cannot convert negative integer {} into an unsigned integer type", + v + ), + TryFromNumberError::U64ToFloatLossyConversion(v) => { + write!( + f, + "cannot convert {}u64 into a floating point type without precision loss", + v + ) + } + TryFromNumberError::I64ToFloatLossyConversion(v) => { + write!( + f, + "cannot convert {}i64 into a floating point type without precision loss", + v + ) + } + TryFromNumberError::F64ToF32LossyConversion(v) => { + write!(f, "will not attempt to convert {}f64 into a f32", v) + } + } + } +} + +impl std::error::Error for TryFromNumberError {} + +impl From for TryFromNumberError { + fn from(value: std::num::TryFromIntError) -> Self { + Self::OutsideIntegerRange(value) + } +} + +macro_rules! to_unsigned_integer_converter { + ($typ:ident, $styp:expr) => { #[doc = "Converts to a `"] #[doc = $styp] - #[doc = "`. This conversion may be lossy."] - pub fn $name(self) -> $typ { - match self { - Number::PosInt(val) => val as $typ, - Number::NegInt(val) => val as $typ, - Number::Float(val) => val as $typ, + #[doc = "`. This conversion fails if it is lossy."] + impl TryFrom for $typ { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::PosInt(v) => Ok(Self::try_from(v)?), + Number::NegInt(v) => Err(Self::Error::NegativeToUnsignedLossyConversion(v)), + Number::Float(v) => Err(Self::Error::FloatToIntegerLossyConversion(v)), + } } } }; - ($name:ident, $typ:ident) => { - to_num_fn!($name, $typ, stringify!($typ)); + ($typ:ident) => { + to_unsigned_integer_converter!($typ, stringify!($typ)); }; } -impl Number { - to_num_fn!(to_f32, f32); - to_num_fn!(to_f64, f64); - - to_num_fn!(to_i8, i8); - to_num_fn!(to_i16, i16); - to_num_fn!(to_i32, i32); - to_num_fn!(to_i64, i64); - - to_num_fn!(to_u8, u8); - to_num_fn!(to_u16, u16); - to_num_fn!(to_u32, u32); - to_num_fn!(to_u64, u64); +macro_rules! to_signed_integer_converter { + ($typ:ident, $styp:expr) => { + #[doc = "Converts to a `"] + #[doc = $styp] + #[doc = "`. This conversion fails if it is lossy."] + impl TryFrom for $typ { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::PosInt(v) => Ok(Self::try_from(v)?), + Number::NegInt(v) => Ok(Self::try_from(v)?), + Number::Float(v) => Err(Self::Error::FloatToIntegerLossyConversion(v)), + } + } + } + }; + + ($typ:ident) => { + to_signed_integer_converter!($typ, stringify!($typ)); + }; } -/* ANCHOR_END: document */ +/// Converts to a `u64`. The conversion fails if it is lossy. +impl TryFrom for u64 { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::PosInt(v) => Ok(v), + Number::NegInt(v) => Err(Self::Error::NegativeToUnsignedLossyConversion(v)), + Number::Float(v) => Err(Self::Error::FloatToIntegerLossyConversion(v)), + } + } +} +to_unsigned_integer_converter!(u32); +to_unsigned_integer_converter!(u16); +to_unsigned_integer_converter!(u8); + +impl TryFrom for i64 { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::PosInt(v) => Ok(Self::try_from(v)?), + Number::NegInt(v) => Ok(v), + Number::Float(v) => Err(Self::Error::FloatToIntegerLossyConversion(v)), + } + } +} +to_signed_integer_converter!(i32); +to_signed_integer_converter!(i16); +to_signed_integer_converter!(i8); + +/// Converts to an `f64`. The conversion fails if it is lossy. +impl TryFrom for f64 { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + // Integers can only be represented with full precision in a float if they fit in the + // significand, which is 24 bits in `f32` and 53 bits in `f64`. + // https://github.com/rust-lang/rust/blob/58f11791af4f97572e7afd83f11cffe04bbbd12f/library/core/src/convert/num.rs#L151-L153 + Number::PosInt(v) => { + if v <= (1 << 53) { + Ok(v as Self) + } else { + Err(Self::Error::U64ToFloatLossyConversion(v)) + } + } + Number::NegInt(v) => { + if -(1 << 53) <= v && v <= (1 << 53) { + Ok(v as Self) + } else { + Err(Self::Error::I64ToFloatLossyConversion(v)) + } + } + Number::Float(v) => Ok(v), + } + } +} + +/// Converts to an `f64`. The conversion fails if it is lossy. +impl TryFrom for f32 { + type Error = TryFromNumberError; + + fn try_from(value: Number) -> Result { + match value { + Number::PosInt(v) => { + if v <= (1 << 24) { + Ok(v as Self) + } else { + Err(Self::Error::U64ToFloatLossyConversion(v)) + } + } + Number::NegInt(v) => { + if -(1 << 24) <= v && v <= (1 << 24) { + Ok(v as Self) + } else { + Err(Self::Error::I64ToFloatLossyConversion(v)) + } + } + Number::Float(v) => Err(Self::Error::F64ToF32LossyConversion(v)), + } + } +} + +#[cfg(test)] +mod number { + use super::*; + + macro_rules! to_unsigned_converter_tests { + ($typ:ident) => { + assert_eq!($typ::try_from(Number::PosInt(69u64)).unwrap(), 69); + + assert!(matches!( + $typ::try_from(Number::PosInt(($typ::MAX as u64) + 1u64)).unwrap_err(), + TryFromNumberError::OutsideIntegerRange(..) + )); + + assert!(matches!( + $typ::try_from(Number::NegInt(-1i64)).unwrap_err(), + TryFromNumberError::NegativeToUnsignedLossyConversion(..) + )); + + for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(matches!( + $typ::try_from(Number::Float(val)).unwrap_err(), + TryFromNumberError::FloatToIntegerLossyConversion(..) + )); + } + }; + } + + #[test] + fn to_u64() { + assert_eq!(u64::try_from(Number::PosInt(69u64)).unwrap(), 69u64); + + assert!(matches!( + u64::try_from(Number::NegInt(-1i64)).unwrap_err(), + TryFromNumberError::NegativeToUnsignedLossyConversion(..) + )); + + for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(matches!( + u64::try_from(Number::Float(val)).unwrap_err(), + TryFromNumberError::FloatToIntegerLossyConversion(..) + )); + } + } + + #[test] + fn to_u32() { + to_unsigned_converter_tests!(u32); + } + + #[test] + fn to_u16() { + to_unsigned_converter_tests!(u16); + } + + #[test] + fn to_u8() { + to_unsigned_converter_tests!(u8); + } + + macro_rules! to_signed_converter_tests { + ($typ:ident) => { + assert_eq!($typ::try_from(Number::PosInt(69u64)).unwrap(), 69); + assert_eq!($typ::try_from(Number::NegInt(-69i64)).unwrap(), -69); + + assert!(matches!( + $typ::try_from(Number::PosInt(($typ::MAX as u64) + 1u64)).unwrap_err(), + TryFromNumberError::OutsideIntegerRange(..) + )); + + assert!(matches!( + $typ::try_from(Number::NegInt(($typ::MIN as i64) - 1i64)).unwrap_err(), + TryFromNumberError::OutsideIntegerRange(..) + )); + + for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(matches!( + u64::try_from(Number::Float(val)).unwrap_err(), + TryFromNumberError::FloatToIntegerLossyConversion(..) + )); + } + }; + } + + #[test] + fn to_i64() { + assert_eq!(i64::try_from(Number::PosInt(69u64)).unwrap(), 69); + assert_eq!(i64::try_from(Number::NegInt(-69i64)).unwrap(), -69); + + for val in [69.69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(matches!( + u64::try_from(Number::Float(val)).unwrap_err(), + TryFromNumberError::FloatToIntegerLossyConversion(..) + )); + } + } + + #[test] + fn to_i32() { + to_signed_converter_tests!(i32); + } + + #[test] + fn to_i16() { + to_signed_converter_tests!(i16); + } + + #[test] + fn to_i8() { + to_signed_converter_tests!(i8); + } + + #[test] + fn to_f64() { + assert_eq!(f64::try_from(Number::PosInt(69u64)).unwrap(), 69f64); + assert_eq!(f64::try_from(Number::NegInt(-69i64)).unwrap(), -69f64); + assert_eq!(f64::try_from(Number::Float(-69f64)).unwrap(), -69f64); + assert!(f64::try_from(Number::Float(f64::NAN)).unwrap().is_nan()); + assert_eq!( + f64::try_from(Number::Float(f64::INFINITY)).unwrap(), + f64::INFINITY + ); + assert_eq!( + f64::try_from(Number::Float(f64::NEG_INFINITY)).unwrap(), + f64::NEG_INFINITY + ); + + let significand_max_u64: u64 = 1 << 53; + let significand_max_i64: i64 = 1 << 53; + + assert_eq!( + f64::try_from(Number::PosInt(significand_max_u64)).unwrap(), + 9007199254740992f64 + ); + + assert_eq!( + f64::try_from(Number::NegInt(significand_max_i64)).unwrap(), + 9007199254740992f64 + ); + assert_eq!( + f64::try_from(Number::NegInt(-significand_max_i64)).unwrap(), + -9007199254740992f64 + ); + + assert!(matches!( + f64::try_from(Number::PosInt(significand_max_u64 + 1)).unwrap_err(), + TryFromNumberError::U64ToFloatLossyConversion(..) + )); + + assert!(matches!( + f64::try_from(Number::NegInt(significand_max_i64 + 1)).unwrap_err(), + TryFromNumberError::I64ToFloatLossyConversion(..) + )); + assert!(matches!( + f64::try_from(Number::NegInt(-significand_max_i64 - 1)).unwrap_err(), + TryFromNumberError::I64ToFloatLossyConversion(..) + )); + } + + #[test] + fn to_f32() { + assert_eq!(f32::try_from(Number::PosInt(69u64)).unwrap(), 69f32); + assert_eq!(f32::try_from(Number::NegInt(-69i64)).unwrap(), -69f32); + + let significand_max_u64: u64 = 1 << 24; + let significand_max_i64: i64 = 1 << 24; + + assert_eq!( + f32::try_from(Number::PosInt(significand_max_u64)).unwrap(), + 16777216f32 + ); + + assert_eq!( + f32::try_from(Number::NegInt(significand_max_i64)).unwrap(), + 16777216f32 + ); + assert_eq!( + f32::try_from(Number::NegInt(-significand_max_i64)).unwrap(), + -16777216f32 + ); + + assert!(matches!( + f32::try_from(Number::PosInt(significand_max_u64 + 1)).unwrap_err(), + TryFromNumberError::U64ToFloatLossyConversion(..) + )); + + assert!(matches!( + f32::try_from(Number::NegInt(significand_max_i64 + 1)).unwrap_err(), + TryFromNumberError::I64ToFloatLossyConversion(..) + )); + assert!(matches!( + f32::try_from(Number::NegInt(-significand_max_i64 - 1)).unwrap_err(), + TryFromNumberError::I64ToFloatLossyConversion(..) + )); + + for val in [69f64, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(matches!( + f32::try_from(Number::Float(val)).unwrap_err(), + TryFromNumberError::F64ToF32LossyConversion(..) + )); + } + } + + #[test] + fn to_f64_lossy() { + assert_eq!(Number::PosInt(69u64).to_f64_lossy(), 69f64); + assert_eq!( + Number::PosInt((1 << 53) + 1).to_f64_lossy(), + 9007199254740992f64 + ); + assert_eq!( + Number::NegInt(-(1 << 53) - 1).to_f64_lossy(), + -9007199254740992f64 + ); + } + + #[test] + fn to_f32_lossy() { + assert_eq!(Number::PosInt(69u64).to_f32_lossy(), 69f32); + assert_eq!(Number::PosInt((1 << 24) + 1).to_f32_lossy(), 16777216f32); + assert_eq!(Number::NegInt(-(1 << 24) - 1).to_f32_lossy(), -16777216f32); + assert_eq!( + Number::Float(1452089033.7674935).to_f32_lossy(), + 1452089100f32 + ); + } +} pub use error::Error;