From 2aed05f38a61f28164697a023f7be1fa23c5f44a Mon Sep 17 00:00:00 2001 From: baishen Date: Thu, 26 Mar 2026 17:14:48 +0800 Subject: [PATCH 1/3] fix: `to_i64` and `to_u64` function support rounded numeric conversion --- src/functions/scalar.rs | 130 +++++++++++--- src/number.rs | 372 +++++++++++++++++++++++++++++++++------- tests/it/functions.rs | 85 ++++++++- 3 files changed, 491 insertions(+), 96 deletions(-) diff --git a/src/functions/scalar.rs b/src/functions/scalar.rs index b5c19a3..33552a1 100644 --- a/src/functions/scalar.rs +++ b/src/functions/scalar.rs @@ -460,9 +460,9 @@ impl RawJsonb<'_> { } } - /// Checks if the JSONB value is an integer that can be represented as an i64. + /// Checks if the JSONB value can be represented as an i64. /// - /// This function checks if the JSONB value is a number and can be converted to an `i64` without loss of information. + /// Decimal numbers are rounded to the nearest integer before the range check. /// /// # Arguments /// @@ -470,8 +470,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(true)` - If the value is an integer representable as an `i64`. - /// * `Ok(false)` - If the value is not an integer or cannot be represented as an `i64`. + /// * `Ok(true)` - If the value can be represented as an `i64`. + /// * `Ok(false)` - If the value cannot be represented as an `i64`. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -484,12 +484,16 @@ impl RawJsonb<'_> { /// let raw_i64 = i64_jsonb.as_raw(); /// assert!(raw_i64.is_i64().unwrap()); /// - /// let i64_jsonb = "-123456789012345678".parse::().unwrap(); + /// let i64_jsonb = "-123456789012345678.0".parse::().unwrap(); /// let raw_i64 = i64_jsonb.as_raw(); /// assert!(raw_i64.is_i64().unwrap()); /// - /// // Non-i64 values - /// let float_jsonb = "123.45".parse::().unwrap(); + /// let float_jsonb = "1.5e0".parse::().unwrap(); + /// let raw_float = float_jsonb.as_raw(); + /// assert!(!raw_float.is_i64().unwrap()); + /// + /// // Out-of-range values + /// let float_jsonb = "1e100".parse::().unwrap(); /// let raw_float = float_jsonb.as_raw(); /// assert!(!raw_float.is_i64().unwrap()); /// @@ -510,8 +514,9 @@ impl RawJsonb<'_> { /// Extracts an i64 integer from a JSONB value. /// /// This function attempts to extract an `i64` integer from the JSONB value. - /// If the JSONB value is a number and can be represented as an `i64` without loss of information, the integer value is returned. - /// Otherwise, `None` is returned. + /// Decimal numbers are converted only when their fractional part is zero. + /// Floating-point numbers are converted only when they already represent an + /// integer value. Otherwise, `None` is returned. /// /// # Arguments /// @@ -519,8 +524,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(Some(i64))` - If the value is an integer that can be represented as an `i64`. - /// * `Ok(None)` - If the value is not an integer or cannot be represented as an `i64`. + /// * `Ok(Some(i64))` - If the value can be represented as an `i64`. + /// * `Ok(None)` - If the value cannot be represented as an `i64`. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -533,8 +538,16 @@ impl RawJsonb<'_> { /// let raw_i64 = i64_jsonb.as_raw(); /// assert_eq!(raw_i64.as_i64().unwrap(), Some(123456789012345678)); /// + /// let decimal_jsonb = "123.0".parse::().unwrap(); + /// let raw_decimal = decimal_jsonb.as_raw(); + /// assert_eq!(raw_decimal.as_i64().unwrap(), Some(123)); + /// + /// let float_jsonb = "123.0".parse::().unwrap(); + /// let raw_float = float_jsonb.as_raw(); + /// assert_eq!(raw_float.as_i64().unwrap(), Some(123)); + /// /// // Non-i64 values - /// let float_jsonb = "123.45".parse::().unwrap(); + /// let float_jsonb = "123.1".parse::().unwrap(); /// let raw_float = float_jsonb.as_raw(); /// assert_eq!(raw_float.as_i64().unwrap(), None); /// @@ -573,6 +586,8 @@ impl RawJsonb<'_> { /// It prioritizes direct conversion from a number if possible. /// If the value is a boolean, it's converted to 1 (for `true`) or 0 (for `false`). /// If the value is a string that can be parsed as an `i64`, that parsed value is returned. + /// Otherwise, if the string can be parsed as a floating-point number, it is + /// rounded to the nearest integer before conversion. /// Otherwise, an error is returned. /// /// # Arguments @@ -608,8 +623,18 @@ impl RawJsonb<'_> { /// let str_jsonb = r#""123""#.parse::().unwrap(); /// assert_eq!(str_jsonb.as_raw().to_i64().unwrap(), 123); /// + /// // Decimal and float numbers are rounded before conversion + /// let decimal_jsonb = "123.5".parse::().unwrap(); + /// assert_eq!(decimal_jsonb.as_raw().to_i64().unwrap(), 124); + /// + /// let float_jsonb = "1.5e0".parse::().unwrap(); + /// assert_eq!(float_jsonb.as_raw().to_i64().unwrap(), 2); + /// + /// let str_jsonb = r#""1.5""#.parse::().unwrap(); + /// assert_eq!(str_jsonb.as_raw().to_i64().unwrap(), 2); + /// /// // Invalid conversions - /// let float_jsonb = "123.45".parse::().unwrap(); + /// let float_jsonb = "1e100".parse::().unwrap(); /// let result = float_jsonb.as_raw().to_i64(); /// assert!(result.is_err()); /// @@ -647,12 +672,12 @@ impl RawJsonb<'_> { } JsonbItem::Number(num) => { let value = num.as_number()?; - if let Some(v) = value.as_i64() { + if let Some(v) = value.to_i64() { return Ok(v); } } JsonbItem::String(s) => { - if let Ok(v) = s.parse::() { + if let Some(v) = parse_string_to_i64(&s) { return Ok(v); } } @@ -661,9 +686,11 @@ impl RawJsonb<'_> { Err(Error::InvalidCast) } - /// Checks if the JSONB value is an unsigned integer that can be represented as a u64. + /// Checks if the JSONB value can be represented as a u64. /// - /// This function checks if the JSONB value is a number and can be converted to a `u64` without loss of information. + /// Decimal numbers are converted only when their fractional part is zero. + /// Floating-point numbers are converted only when they already represent an + /// integer value. /// /// # Arguments /// @@ -671,8 +698,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(true)` - If the value is an unsigned integer representable as a `u64`. - /// * `Ok(false)` - If the value is not an unsigned integer or cannot be represented as a `u64`. + /// * `Ok(true)` - If the value can be represented as a `u64`. + /// * `Ok(false)` - If the value cannot be represented as a `u64`. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -689,8 +716,12 @@ impl RawJsonb<'_> { /// let raw_u64 = u64_jsonb.as_raw(); /// assert!(raw_u64.is_u64().unwrap()); /// + /// let float_jsonb = "123.0".parse::().unwrap(); + /// let raw_float = float_jsonb.as_raw(); + /// assert!(raw_float.is_u64().unwrap()); + /// /// // Non-u64 values - /// let float_jsonb = "123.45".parse::().unwrap(); + /// let float_jsonb = "123.1".parse::().unwrap(); /// let raw_float = float_jsonb.as_raw(); /// assert!(!raw_float.is_u64().unwrap()); /// @@ -731,7 +762,10 @@ impl RawJsonb<'_> { /// Extracts a u64 unsigned integer from a JSONB value. /// /// This function attempts to extract a `u64` unsigned integer from the JSONB value. - /// If the JSONB value is a number and can be represented as a `u64` without loss of information (i.e., it's a non-negative integer within the `u64` range), + /// Decimal numbers are converted only when their fractional part is zero. + /// Floating-point numbers are converted only when they already represent an + /// integer value. If the JSONB value is a number and can be represented as + /// a `u64`, /// the unsigned integer value is returned. Otherwise, `None` is returned. /// /// # Arguments @@ -740,8 +774,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(Some(u64))` - If the value is an unsigned integer that can be represented as a `u64`. - /// * `Ok(None)` - If the value is not an unsigned integer or cannot be represented as a `u64`. + /// * `Ok(Some(u64))` - If the value can be represented as a `u64`. + /// * `Ok(None)` - If the value cannot be represented as a `u64`. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -754,8 +788,16 @@ impl RawJsonb<'_> { /// let raw_u64 = u64_jsonb.as_raw(); /// assert_eq!(raw_u64.as_u64().unwrap(), Some(1234567890123456789)); /// + /// let decimal_jsonb = "123.0".parse::().unwrap(); + /// let raw_decimal = decimal_jsonb.as_raw(); + /// assert_eq!(raw_decimal.as_u64().unwrap(), Some(123)); + /// + /// let float_jsonb = "123.0".parse::().unwrap(); + /// let raw_float = float_jsonb.as_raw(); + /// assert_eq!(raw_float.as_u64().unwrap(), Some(123)); + /// /// // Non-u64 values - /// let float_jsonb = "123.45".parse::().unwrap(); + /// let float_jsonb = "123.1".parse::().unwrap(); /// let raw_float = float_jsonb.as_raw(); /// assert_eq!(raw_float.as_u64().unwrap(), None); /// @@ -806,6 +848,8 @@ impl RawJsonb<'_> { /// It prioritizes direct conversion from a number if possible. /// If the value is a boolean, it's converted to 1 (for `true`) or 0 (for `false`). /// If the value is a string that can be parsed as a `u64`, that parsed value is returned. + /// Otherwise, if the string can be parsed as a floating-point number, it is + /// rounded to the nearest integer before conversion. /// Otherwise, an error is returned. /// /// # Arguments @@ -815,7 +859,7 @@ impl RawJsonb<'_> { /// # Returns /// /// * `Ok(u64)` - The `u64` representation of the JSONB value. - /// * `Err(Error::InvalidCast)` - If the value cannot be converted to a `u64` (e.g., it's a floating-point number, a negative number, an array, an object, or a string that is not a valid unsigned integer). + /// * `Err(Error::InvalidCast)` - If the value cannot be converted to a `u64` (e.g., it's a negative number after rounding, an array, an object, or a string that is not a valid number). /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -841,8 +885,18 @@ impl RawJsonb<'_> { /// let str_jsonb = r#""123""#.parse::().unwrap(); /// assert_eq!(str_jsonb.as_raw().to_u64().unwrap(), 123); /// + /// // Decimal and float numbers are rounded before conversion + /// let decimal_jsonb = "123.5".parse::().unwrap(); + /// assert_eq!(decimal_jsonb.as_raw().to_u64().unwrap(), 124); + /// + /// let float_jsonb = "1.5e0".parse::().unwrap(); + /// assert_eq!(float_jsonb.as_raw().to_u64().unwrap(), 2); + /// + /// let str_jsonb = r#""1.5""#.parse::().unwrap(); + /// assert_eq!(str_jsonb.as_raw().to_u64().unwrap(), 2); + /// /// // Invalid conversions - /// let float_jsonb = "123.45".parse::().unwrap(); + /// let float_jsonb = "1e100".parse::().unwrap(); /// let result = float_jsonb.as_raw().to_u64(); /// assert!(result.is_err()); /// @@ -884,12 +938,12 @@ impl RawJsonb<'_> { } JsonbItem::Number(num) => { let value = num.as_number()?; - if let Some(v) = value.as_u64() { + if let Some(v) = value.to_u64() { return Ok(v); } } JsonbItem::String(s) => { - if let Ok(v) = s.parse::() { + if let Some(v) = parse_string_to_u64(&s) { return Ok(v); } } @@ -1937,3 +1991,23 @@ impl RawJsonb<'_> { } } } + +fn parse_string_to_i64(s: &str) -> Option { + if let Ok(v) = s.parse::() { + return Some(v); + } + + s.parse::() + .ok() + .and_then(|value| Number::Float64(value).to_i64()) +} + +fn parse_string_to_u64(s: &str) -> Option { + if let Ok(v) = s.parse::() { + return Some(v); + } + + s.parse::() + .ok() + .and_then(|value| Number::Float64(value).to_u64()) +} diff --git a/src/number.rs b/src/number.rs index 8058b68..9206519 100644 --- a/src/number.rs +++ b/src/number.rs @@ -22,6 +22,7 @@ use crate::error::Result; use crate::Error; use ethnum::i256; +use ethnum::U256; use ordered_float::OrderedFloat; use serde::de; use serde::de::Deserialize; @@ -400,42 +401,29 @@ impl Serialize for Number { impl Number { /// Returns the i128 representation of the number, if possible. /// - /// This method returns None if the number cannot be represented as an i64. + /// Decimal values are converted only when their fractional part is zero. + /// Floating-point values are converted only when they already represent an + /// integer value. This method returns `None` if the value cannot be + /// represented as an `i128`. pub fn as_i128(&self) -> Option { match self { Number::Int64(v) => Some(*v as i128), Number::UInt64(v) => Some(*v as i128), - Number::Float64(_) => None, - Number::Decimal64(v) => { - if v.scale == 0 { - Some(v.value as i128) - } else { - None - } - } - Number::Decimal128(v) => { - if v.scale == 0 { - Some(v.value) - } else { - None - } - } + Number::Float64(v) => exact_float_to_i128(*v), + Number::Decimal64(v) => exact_decimal_i128(v.value as i128, v.scale), + Number::Decimal128(v) => exact_decimal_i128(v.value, v.scale), Number::Decimal256(v) => { - if v.scale == 0 - && v.value >= i256::from(i128::MIN) - && v.value <= i256::from(i128::MAX) - { - Some(v.value.as_i128()) - } else { - None - } + exact_decimal_i256(v.value, v.scale).and_then(|value| i128::try_from(value).ok()) } } } /// Returns the i64 representation of the number, if possible. /// - /// This method returns None if the number cannot be represented as an i64. + /// Decimal values are converted only when their fractional part is zero. + /// Floating-point values are converted only when they already represent an + /// integer value. This method returns `None` if the value cannot be + /// represented as an `i64`. pub fn as_i64(&self) -> Option { match self { Number::Int64(v) => Some(*v), @@ -446,40 +434,26 @@ impl Number { None } } - Number::Float64(_) => None, - Number::Decimal64(v) => { - if v.scale == 0 { - Some(v.value) - } else { - None - } + Number::Float64(v) => { + exact_float_to_i128(*v).and_then(|value| i64::try_from(value).ok()) } + Number::Decimal64(v) => exact_decimal_i128(v.value as i128, v.scale) + .and_then(|value| i64::try_from(value).ok()), Number::Decimal128(v) => { - if v.scale == 0 - && v.value >= i128::from(i64::MIN) - && v.value <= i128::from(i64::MAX) - { - Some(v.value as i64) - } else { - None - } + exact_decimal_i128(v.value, v.scale).and_then(|value| i64::try_from(value).ok()) } Number::Decimal256(v) => { - if v.scale == 0 - && v.value >= i256::from(i64::MIN) - && v.value <= i256::from(i64::MAX) - { - Some(v.value.as_i64()) - } else { - None - } + exact_decimal_i256(v.value, v.scale).and_then(|value| i64::try_from(value).ok()) } } } /// Returns the u64 representation of the number, if possible. /// - /// This method returns None if the number cannot be represented as a u64. + /// Decimal values are converted only when their fractional part is zero. + /// Floating-point values are converted only when they already represent an + /// integer value. This method returns `None` if the value is negative or + /// cannot be represented as a `u64`. pub fn as_u64(&self) -> Option { match self { Number::Int64(v) => { @@ -490,31 +464,56 @@ impl Number { } } Number::UInt64(v) => Some(*v), - Number::Float64(_) => None, - Number::Decimal64(v) => { - if v.scale == 0 && v.value >= 0 { - Some(v.value as u64) - } else { - None - } + Number::Float64(v) => { + exact_float_to_i128(*v).and_then(|value| u64::try_from(value).ok()) } + Number::Decimal64(v) => exact_decimal_i128(v.value as i128, v.scale) + .and_then(|value| u64::try_from(value).ok()), Number::Decimal128(v) => { - if v.scale == 0 && v.value >= 0 && v.value <= i128::from(u64::MAX) { - Some(v.value as u64) - } else { - None - } + exact_decimal_i128(v.value, v.scale).and_then(|value| u64::try_from(value).ok()) } Number::Decimal256(v) => { - if v.scale == 0 && v.value >= i256::ZERO && v.value <= i256::from(u64::MAX) { - Some(v.value.as_u64()) - } else { - None - } + exact_decimal_i256(v.value, v.scale).and_then(|value| u64::try_from(value).ok()) + } + } + } + + /// Returns the i128 representation of the number after rounding if needed. + /// + /// Decimal and floating-point values are rounded to the nearest integer + /// before conversion. This method returns `None` if the rounded value + /// cannot be represented as an `i128`. + pub fn to_i128(&self) -> Option { + match self { + Number::Int64(v) => Some(*v as i128), + Number::UInt64(v) => Some(*v as i128), + Number::Float64(v) => round_float_to_i128(*v), + Number::Decimal64(v) => round_decimal_i128(v.value as i128, v.scale), + Number::Decimal128(v) => round_decimal_i128(v.value, v.scale), + Number::Decimal256(v) => { + round_decimal_i256(v.value, v.scale).and_then(|value| i128::try_from(value).ok()) } } } + /// Returns the i64 representation of the number after rounding if needed. + /// + /// Decimal and floating-point values are rounded to the nearest integer + /// before conversion. This method returns `None` if the rounded value + /// cannot be represented as an `i64`. + pub fn to_i64(&self) -> Option { + self.to_i128().and_then(|value| i64::try_from(value).ok()) + } + + /// Returns the u64 representation of the number after rounding if needed. + /// + /// Decimal and floating-point values are rounded to the nearest integer + /// before conversion. This method returns `None` if the rounded value + /// is negative or cannot be represented as a `u64`. + pub fn to_u64(&self) -> Option { + self.to_i128().and_then(|value| u64::try_from(value).ok()) + } + /// Returns the f64 representation of the number. /// /// This method always returns a value, but may lose precision for very large numbers. @@ -778,6 +777,120 @@ impl Number { } } +fn exact_float_to_i128(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + + format!("{value:.0}").parse::().ok() +} + +fn round_float_to_i128(value: f64) -> Option { + if !value.is_finite() { + return None; + } + + let rounded = value.round(); + format!("{rounded:.0}").parse::().ok() +} + +fn exact_decimal_i128(value: i128, scale: u8) -> Option { + if scale == 0 { + return Some(value); + } + + let Some(&divisor) = I128_POWERS_OF_10.get(scale as usize) else { + return if value == 0 { Some(0) } else { None }; + }; + + if value % divisor == 0 { + Some(value / divisor) + } else { + None + } +} + +fn round_decimal_i128(value: i128, scale: u8) -> Option { + if scale == 0 { + return Some(value); + } + + let Some(&divisor) = I128_POWERS_OF_10.get(scale as usize) else { + // 10^scale no longer fits in i128, so any i128-backed decimal rounds to 0. + return Some(0); + }; + + let quotient = value / divisor; + let remainder = value % divisor; + let abs_remainder = remainder.unsigned_abs(); + let divisor = divisor as u128; + let round_up = abs_remainder >= divisor - abs_remainder; + + if !round_up { + Some(quotient) + } else if value >= 0 { + quotient.checked_add(1) + } else { + quotient.checked_sub(1) + } +} + +fn exact_decimal_i256(value: i256, scale: u8) -> Option { + if scale == 0 { + return Some(value); + } + + let Some(divisor) = U256::new(10).checked_pow(scale as u32) else { + return if value == i256::ZERO { + Some(i256::ZERO) + } else { + None + }; + }; + + let abs_value = value.unsigned_abs(); + let quotient = abs_value / divisor; + let remainder = abs_value % divisor; + if remainder != U256::ZERO { + return None; + } + + let quotient = i256::try_from(quotient).ok()?; + if value >= i256::ZERO { + Some(quotient) + } else { + i256::ZERO.checked_sub(quotient) + } +} + +fn round_decimal_i256(value: i256, scale: u8) -> Option { + if scale == 0 { + return Some(value); + } + + let Some(divisor) = U256::new(10).checked_pow(scale as u32) else { + // 10^scale exceeds the i256 range by enough that the rounded result is always 0. + return Some(i256::ZERO); + }; + + let abs_value = value.unsigned_abs(); + let quotient = abs_value / divisor; + let remainder = abs_value % divisor; + let round_up = remainder >= divisor - remainder; + let rounded = if round_up { + quotient.checked_add(U256::new(1))? + } else { + quotient + }; + let rounded = i256::try_from(rounded).ok()?; + + if value >= i256::ZERO { + Some(rounded) + } else { + i256::ZERO.checked_sub(rounded) + } +} + impl Default for Number { /// Returns the default value for the Number enum. /// @@ -1632,4 +1745,131 @@ mod tests { Ordering::Equal ); } + + #[test] + fn test_exact_integer_casts() { + let decimal64 = Number::Decimal64(Decimal64 { + scale: 1, + value: 1230, + }); + assert_eq!(decimal64.as_i128(), Some(123)); + assert_eq!(decimal64.as_i64(), Some(123)); + assert_eq!(decimal64.as_u64(), Some(123)); + + let decimal128 = Number::Decimal128(Decimal128 { + scale: 1, + value: 1230, + }); + assert_eq!(decimal128.as_i128(), Some(123)); + assert_eq!(decimal128.as_i64(), Some(123)); + assert_eq!(decimal128.as_u64(), Some(123)); + + let negative_decimal128 = Number::Decimal128(Decimal128 { + scale: 1, + value: -1230, + }); + assert_eq!(negative_decimal128.as_i128(), Some(-123)); + assert_eq!(negative_decimal128.as_i64(), Some(-123)); + assert_eq!(negative_decimal128.as_u64(), None); + + let non_integer_decimal64 = Number::Decimal64(Decimal64 { + scale: 1, + value: 1234, + }); + assert_eq!(non_integer_decimal64.as_i128(), None); + assert_eq!(non_integer_decimal64.as_i64(), None); + assert_eq!(non_integer_decimal64.as_u64(), None); + + let decimal256 = Number::Decimal256(Decimal256 { + scale: 1, + value: i256::from(1230), + }); + assert_eq!(decimal256.as_i128(), Some(123)); + assert_eq!(decimal256.as_i64(), Some(123)); + assert_eq!(decimal256.as_u64(), Some(123)); + + let float = Number::Float64(123.0); + assert_eq!(float.as_i128(), Some(123)); + assert_eq!(float.as_i64(), Some(123)); + assert_eq!(float.as_u64(), Some(123)); + + let non_integer_float = Number::Float64(123.1); + assert_eq!(non_integer_float.as_i128(), None); + assert_eq!(non_integer_float.as_i64(), None); + assert_eq!(non_integer_float.as_u64(), None); + } + + #[test] + fn test_rounded_integer_casts() { + let decimal128 = Number::Decimal128(Decimal128 { + scale: 1, + value: 1235, + }); + assert_eq!(decimal128.to_i128(), Some(124)); + assert_eq!(decimal128.to_i64(), Some(124)); + assert_eq!(decimal128.to_u64(), Some(124)); + + let negative_decimal128 = Number::Decimal128(Decimal128 { + scale: 1, + value: -1235, + }); + assert_eq!(negative_decimal128.to_i128(), Some(-124)); + assert_eq!(negative_decimal128.to_i64(), Some(-124)); + assert_eq!(negative_decimal128.to_u64(), None); + + let small_negative_decimal64 = Number::Decimal64(Decimal64 { + scale: 1, + value: -4, + }); + assert_eq!(small_negative_decimal64.to_i64(), Some(0)); + assert_eq!(small_negative_decimal64.to_u64(), Some(0)); + + let decimal256 = Number::Decimal256(Decimal256 { + scale: 1, + value: i256::from(1234), + }); + assert_eq!(decimal256.to_i128(), Some(123)); + assert_eq!(decimal256.to_i64(), Some(123)); + assert_eq!(decimal256.to_u64(), Some(123)); + + let float = Number::Float64(123.4); + assert_eq!(float.to_i128(), Some(123)); + assert_eq!(float.to_i64(), Some(123)); + assert_eq!(float.to_u64(), Some(123)); + + let half_up = Number::Float64(123.5); + assert_eq!(half_up.to_i128(), Some(124)); + assert_eq!(half_up.to_i64(), Some(124)); + assert_eq!(half_up.to_u64(), Some(124)); + + let negative_half_up = Number::Float64(-123.5); + assert_eq!(negative_half_up.to_i128(), Some(-124)); + assert_eq!(negative_half_up.to_i64(), Some(-124)); + assert_eq!(negative_half_up.to_u64(), None); + + let small_negative = Number::Float64(-0.4); + assert_eq!(small_negative.to_i64(), Some(0)); + assert_eq!(small_negative.to_u64(), Some(0)); + + let nan = Number::Float64(f64::NAN); + assert_eq!(nan.to_i128(), None); + assert_eq!(nan.to_i64(), None); + assert_eq!(nan.to_u64(), None); + + let inf = Number::Float64(f64::INFINITY); + assert_eq!(inf.to_i128(), None); + assert_eq!(inf.to_i64(), None); + assert_eq!(inf.to_u64(), None); + + let large = Number::Float64(1e100); + assert_eq!(large.to_i128(), None); + assert_eq!(large.to_i64(), None); + assert_eq!(large.to_u64(), None); + + let overflowing_decimal128 = Number::Decimal128(Decimal128 { + scale: 1, + value: i128::from(i64::MAX) * 10 + 5, + }); + assert_eq!(overflowing_decimal128.to_i64(), None); + } } diff --git a/tests/it/functions.rs b/tests/it/functions.rs index a89d3da..e1c6cdb 100644 --- a/tests/it/functions.rs +++ b/tests/it/functions.rs @@ -677,6 +677,31 @@ fn test_as_type() { } } +#[test] +fn test_as_integer_type() { + let sources = vec![ + (r#"123"#, Some(123_i64), Some(123_u64)), + (r#"123.0"#, Some(123_i64), Some(123_u64)), + (r#"123.1"#, None, None), + (r#"1.5e0"#, None, None), + (r#"-1.0"#, Some(-1_i64), None), + (r#"-1.1"#, None, None), + ]; + + for (s, expect_i64, expect_u64) in sources { + let owned_jsonb = s.parse::().unwrap(); + let raw_jsonb = owned_jsonb.as_raw(); + + let res = raw_jsonb.as_i64(); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), expect_i64); + + let res = raw_jsonb.as_u64(); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), expect_u64); + } +} + #[test] fn test_to_type() { let sources = vec![ @@ -716,11 +741,43 @@ fn test_to_type() { ( r#"1.2"#, None, - None, - None, + Some(1_i64), + Some(1_u64), Some(1.2_f64), Some("1.2".to_string()), ), + ( + r#"1.5"#, + None, + Some(2_i64), + Some(2_u64), + Some(1.5_f64), + Some("1.5".to_string()), + ), + ( + r#"-1.5"#, + None, + Some(-2_i64), + None, + Some(-1.5_f64), + Some("-1.5".to_string()), + ), + ( + r#"1e-1"#, + None, + Some(0_i64), + Some(0_u64), + Some(0.1_f64), + Some("0.1".to_string()), + ), + ( + r#"1.5e0"#, + None, + Some(2_i64), + Some(2_u64), + Some(1.5_f64), + Some("1.5".to_string()), + ), ( r#""true""#, Some(true), @@ -745,6 +802,30 @@ fn test_to_type() { None, Some("abcd".to_string()), ), + ( + r#""123.1""#, + None, + Some(123_i64), + Some(123_u64), + Some(123.1_f64), + Some("123.1".to_string()), + ), + ( + r#""123.5""#, + None, + Some(124_i64), + Some(124_u64), + Some(123.5_f64), + Some("123.5".to_string()), + ), + ( + r#""-1.5""#, + None, + Some(-2_i64), + None, + Some(-1.5_f64), + Some("-1.5".to_string()), + ), ]; for (s, expect_bool, expect_i64, expect_u64, expect_f64, expect_str) in sources { From d7676cb3fb8f5f948801642ab800100d2159a8ea Mon Sep 17 00:00:00 2001 From: baishen Date: Fri, 27 Mar 2026 13:38:39 +0800 Subject: [PATCH 2/3] fix --- src/functions/scalar.rs | 72 ++++++++++++------------------- src/number.rs | 93 +++++++++++++++++++++++++++++------------ 2 files changed, 94 insertions(+), 71 deletions(-) diff --git a/src/functions/scalar.rs b/src/functions/scalar.rs index 33552a1..0385dd5 100644 --- a/src/functions/scalar.rs +++ b/src/functions/scalar.rs @@ -460,9 +460,9 @@ impl RawJsonb<'_> { } } - /// Checks if the JSONB value can be represented as an i64. + /// Checks whether the JSONB value is an exact `i64`. /// - /// Decimal numbers are rounded to the nearest integer before the range check. + /// Decimal and floating-point numbers must already be integral. /// /// # Arguments /// @@ -470,8 +470,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(true)` - If the value can be represented as an `i64`. - /// * `Ok(false)` - If the value cannot be represented as an `i64`. + /// * `Ok(true)` - If the value is an exact `i64`. + /// * `Ok(false)` - Otherwise. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -511,12 +511,10 @@ impl RawJsonb<'_> { self.as_i64().map(|v| v.is_some()) } - /// Extracts an i64 integer from a JSONB value. + /// Extracts an exact `i64` from a JSONB value. /// - /// This function attempts to extract an `i64` integer from the JSONB value. - /// Decimal numbers are converted only when their fractional part is zero. - /// Floating-point numbers are converted only when they already represent an - /// integer value. Otherwise, `None` is returned. + /// Decimal and floating-point numbers are accepted only when they already + /// represent an integer. /// /// # Arguments /// @@ -524,8 +522,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(Some(i64))` - If the value can be represented as an `i64`. - /// * `Ok(None)` - If the value cannot be represented as an `i64`. + /// * `Ok(Some(i64))` - If the value is an exact `i64`. + /// * `Ok(None)` - Otherwise. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -580,15 +578,10 @@ impl RawJsonb<'_> { } } - /// Converts a JSONB value to an i64 integer. + /// Converts a JSONB value to `i64`. /// - /// This function attempts to convert a JSONB value to an `i64` integer. - /// It prioritizes direct conversion from a number if possible. - /// If the value is a boolean, it's converted to 1 (for `true`) or 0 (for `false`). - /// If the value is a string that can be parsed as an `i64`, that parsed value is returned. - /// Otherwise, if the string can be parsed as a floating-point number, it is - /// rounded to the nearest integer before conversion. - /// Otherwise, an error is returned. + /// Numbers are rounded to the nearest integer. Booleans map to `1` and + /// `0`. Strings are parsed as `i64`, or as `f64` and then rounded. /// /// # Arguments /// @@ -596,7 +589,7 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(i64)` - The `i64` representation of the JSONB value. + /// * `Ok(i64)` - The converted value. /// * `Err(Error::InvalidCast)` - If the value cannot be converted to an `i64`. /// * `Err(Error)` - If the JSONB data is invalid. /// @@ -686,11 +679,9 @@ impl RawJsonb<'_> { Err(Error::InvalidCast) } - /// Checks if the JSONB value can be represented as a u64. + /// Checks whether the JSONB value is an exact `u64`. /// - /// Decimal numbers are converted only when their fractional part is zero. - /// Floating-point numbers are converted only when they already represent an - /// integer value. + /// Decimal and floating-point numbers must already be integral. /// /// # Arguments /// @@ -698,8 +689,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(true)` - If the value can be represented as a `u64`. - /// * `Ok(false)` - If the value cannot be represented as a `u64`. + /// * `Ok(true)` - If the value is an exact `u64`. + /// * `Ok(false)` - Otherwise. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -759,14 +750,10 @@ impl RawJsonb<'_> { self.as_u64().map(|v| v.is_some()) } - /// Extracts a u64 unsigned integer from a JSONB value. + /// Extracts an exact `u64` from a JSONB value. /// - /// This function attempts to extract a `u64` unsigned integer from the JSONB value. - /// Decimal numbers are converted only when their fractional part is zero. - /// Floating-point numbers are converted only when they already represent an - /// integer value. If the JSONB value is a number and can be represented as - /// a `u64`, - /// the unsigned integer value is returned. Otherwise, `None` is returned. + /// Decimal and floating-point numbers are accepted only when they already + /// represent a non-negative integer. /// /// # Arguments /// @@ -774,8 +761,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(Some(u64))` - If the value can be represented as a `u64`. - /// * `Ok(None)` - If the value cannot be represented as a `u64`. + /// * `Ok(Some(u64))` - If the value is an exact `u64`. + /// * `Ok(None)` - Otherwise. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples @@ -842,15 +829,10 @@ impl RawJsonb<'_> { } } - /// Converts a JSONB value to a u64 unsigned integer. + /// Converts a JSONB value to `u64`. /// - /// This function attempts to convert a JSONB value to a `u64` unsigned integer. - /// It prioritizes direct conversion from a number if possible. - /// If the value is a boolean, it's converted to 1 (for `true`) or 0 (for `false`). - /// If the value is a string that can be parsed as a `u64`, that parsed value is returned. - /// Otherwise, if the string can be parsed as a floating-point number, it is - /// rounded to the nearest integer before conversion. - /// Otherwise, an error is returned. + /// Numbers are rounded to the nearest integer. Booleans map to `1` and + /// `0`. Strings are parsed as `u64`, or as `f64` and then rounded. /// /// # Arguments /// @@ -858,8 +840,8 @@ impl RawJsonb<'_> { /// /// # Returns /// - /// * `Ok(u64)` - The `u64` representation of the JSONB value. - /// * `Err(Error::InvalidCast)` - If the value cannot be converted to a `u64` (e.g., it's a negative number after rounding, an array, an object, or a string that is not a valid number). + /// * `Ok(u64)` - The converted value. + /// * `Err(Error::InvalidCast)` - If conversion fails. /// * `Err(Error)` - If the JSONB data is invalid. /// /// # Examples diff --git a/src/number.rs b/src/number.rs index 9206519..6795af2 100644 --- a/src/number.rs +++ b/src/number.rs @@ -23,6 +23,8 @@ use crate::Error; use ethnum::i256; use ethnum::U256; +use num_traits::AsPrimitive; +use num_traits::Bounded; use ordered_float::OrderedFloat; use serde::de; use serde::de::Deserialize; @@ -409,7 +411,7 @@ impl Number { match self { Number::Int64(v) => Some(*v as i128), Number::UInt64(v) => Some(*v as i128), - Number::Float64(v) => exact_float_to_i128(*v), + Number::Float64(v) => exact_float_to_int(*v), Number::Decimal64(v) => exact_decimal_i128(v.value as i128, v.scale), Number::Decimal128(v) => exact_decimal_i128(v.value, v.scale), Number::Decimal256(v) => { @@ -434,9 +436,7 @@ impl Number { None } } - Number::Float64(v) => { - exact_float_to_i128(*v).and_then(|value| i64::try_from(value).ok()) - } + Number::Float64(v) => exact_float_to_int(*v), Number::Decimal64(v) => exact_decimal_i128(v.value as i128, v.scale) .and_then(|value| i64::try_from(value).ok()), Number::Decimal128(v) => { @@ -464,9 +464,7 @@ impl Number { } } Number::UInt64(v) => Some(*v), - Number::Float64(v) => { - exact_float_to_i128(*v).and_then(|value| u64::try_from(value).ok()) - } + Number::Float64(v) => exact_float_to_int(*v), Number::Decimal64(v) => exact_decimal_i128(v.value as i128, v.scale) .and_then(|value| u64::try_from(value).ok()), Number::Decimal128(v) => { @@ -487,7 +485,7 @@ impl Number { match self { Number::Int64(v) => Some(*v as i128), Number::UInt64(v) => Some(*v as i128), - Number::Float64(v) => round_float_to_i128(*v), + Number::Float64(v) => round_float_to_int(*v), Number::Decimal64(v) => round_decimal_i128(v.value as i128, v.scale), Number::Decimal128(v) => round_decimal_i128(v.value, v.scale), Number::Decimal256(v) => { @@ -777,21 +775,54 @@ impl Number { } } -fn exact_float_to_i128(value: f64) -> Option { +fn exact_float_to_int(value: f64) -> Option +where + T: Bounded + AsPrimitive, + f64: AsPrimitive, +{ if !value.is_finite() || value.fract() != 0.0 { return None; } - format!("{value:.0}").parse::().ok() + if fits_float_target_range::(value) { + Some(value.as_()) + } else { + None + } } -fn round_float_to_i128(value: f64) -> Option { +fn round_float_to_int(value: f64) -> Option +where + T: Bounded + AsPrimitive, + f64: AsPrimitive, +{ if !value.is_finite() { return None; } let rounded = value.round(); - format!("{rounded:.0}").parse::().ok() + if fits_float_target_range::(rounded) { + Some(rounded.as_()) + } else { + None + } +} + +// Use a half-open range so the positive bound can be expressed as `-min` for +// signed integers, which avoids relying on an imprecise `MAX as f64`. +#[inline] +fn fits_float_target_range(value: f64) -> bool +where + T: Bounded + AsPrimitive, +{ + let lower = T::min_value().as_(); + let upper_exclusive = if lower < 0.0 { + -lower + } else { + T::max_value().as_() + 1.0 + }; + + value >= lower && value < upper_exclusive } fn exact_decimal_i128(value: i128, scale: u8) -> Option { @@ -799,7 +830,7 @@ fn exact_decimal_i128(value: i128, scale: u8) -> Option { return Some(value); } - let Some(&divisor) = I128_POWERS_OF_10.get(scale as usize) else { + let Some(divisor) = I128_POWERS_OF_10.get(scale as usize) else { return if value == 0 { Some(0) } else { None }; }; @@ -815,7 +846,7 @@ fn round_decimal_i128(value: i128, scale: u8) -> Option { return Some(value); } - let Some(&divisor) = I128_POWERS_OF_10.get(scale as usize) else { + let Some(divisor) = I128_POWERS_OF_10.get(scale as usize) else { // 10^scale no longer fits in i128, so any i128-backed decimal rounds to 0. return Some(0); }; @@ -835,6 +866,16 @@ fn round_decimal_i128(value: i128, scale: u8) -> Option { } } +#[inline] +fn signed_i256_from_abs(abs: U256, negative: bool) -> Option { + let value = i256::try_from(abs).ok()?; + if negative { + i256::ZERO.checked_sub(value) + } else { + Some(value) + } +} + fn exact_decimal_i256(value: i256, scale: u8) -> Option { if scale == 0 { return Some(value); @@ -848,6 +889,7 @@ fn exact_decimal_i256(value: i256, scale: u8) -> Option { }; }; + let negative = value < i256::ZERO; let abs_value = value.unsigned_abs(); let quotient = abs_value / divisor; let remainder = abs_value % divisor; @@ -855,12 +897,7 @@ fn exact_decimal_i256(value: i256, scale: u8) -> Option { return None; } - let quotient = i256::try_from(quotient).ok()?; - if value >= i256::ZERO { - Some(quotient) - } else { - i256::ZERO.checked_sub(quotient) - } + signed_i256_from_abs(quotient, negative) } fn round_decimal_i256(value: i256, scale: u8) -> Option { @@ -873,6 +910,7 @@ fn round_decimal_i256(value: i256, scale: u8) -> Option { return Some(i256::ZERO); }; + let negative = value < i256::ZERO; let abs_value = value.unsigned_abs(); let quotient = abs_value / divisor; let remainder = abs_value % divisor; @@ -882,13 +920,8 @@ fn round_decimal_i256(value: i256, scale: u8) -> Option { } else { quotient }; - let rounded = i256::try_from(rounded).ok()?; - if value >= i256::ZERO { - Some(rounded) - } else { - i256::ZERO.checked_sub(rounded) - } + signed_i256_from_abs(rounded, negative) } impl Default for Number { @@ -1797,6 +1830,9 @@ mod tests { assert_eq!(non_integer_float.as_i128(), None); assert_eq!(non_integer_float.as_i64(), None); assert_eq!(non_integer_float.as_u64(), None); + + let u64_upper_bound = Number::Float64((u64::MAX as f64) + 1.0); + assert_eq!(u64_upper_bound.as_u64(), None); } #[test] @@ -1866,6 +1902,11 @@ mod tests { assert_eq!(large.to_i64(), None); assert_eq!(large.to_u64(), None); + let i64_upper_bound = Number::Float64(-(i64::MIN as f64)); + assert_eq!(i64_upper_bound.to_i64(), None); + let u64_upper_bound = Number::Float64((u64::MAX as f64) + 1.0); + assert_eq!(u64_upper_bound.to_u64(), None); + let overflowing_decimal128 = Number::Decimal128(Decimal128 { scale: 1, value: i128::from(i64::MAX) * 10 + 5, From 8d63a0d4d650ebc7a7b40833c29eb2bd1d7fbf95 Mon Sep 17 00:00:00 2001 From: b41sh Date: Fri, 27 Mar 2026 14:14:44 +0800 Subject: [PATCH 3/3] fix --- src/number.rs | 2 +- tests/it/functions.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/number.rs b/src/number.rs index 6795af2..3bae64c 100644 --- a/src/number.rs +++ b/src/number.rs @@ -854,7 +854,7 @@ fn round_decimal_i128(value: i128, scale: u8) -> Option { let quotient = value / divisor; let remainder = value % divisor; let abs_remainder = remainder.unsigned_abs(); - let divisor = divisor as u128; + let divisor = *divisor as u128; let round_up = abs_remainder >= divisor - abs_remainder; if !round_up { diff --git a/tests/it/functions.rs b/tests/it/functions.rs index e1c6cdb..49cd937 100644 --- a/tests/it/functions.rs +++ b/tests/it/functions.rs @@ -826,6 +826,22 @@ fn test_to_type() { Some(-1.5_f64), Some("-1.5".to_string()), ), + ( + r#""1.5e0""#, + None, + Some(2_i64), + Some(2_u64), + Some(1.5_f64), + Some("1.5e0".to_string()), + ), + ( + r#""-1.5e0""#, + None, + Some(-2_i64), + None, + Some(-1.5_f64), + Some("-1.5e0".to_string()), + ), ]; for (s, expect_bool, expect_i64, expect_u64, expect_f64, expect_str) in sources {