diff --git a/vortex-array/src/scalar/typed_view/decimal/scalar.rs b/vortex-array/src/scalar/typed_view/decimal/scalar.rs index 4113b625ecd..052c2934379 100644 --- a/vortex-array/src/scalar/typed_view/decimal/scalar.rs +++ b/vortex-array/src/scalar/typed_view/decimal/scalar.rs @@ -94,9 +94,6 @@ impl<'a> DecimalScalar<'a> { DType::Primitive(ptype, nullability) => { // Cast decimal to primitive type if let Some(decimal_value) = &self.decimal_value { - // Convert decimal value to primitive, accounting for scale - let scale_factor = 10_i128.pow(self.decimal_type.scale() as u32); - // Convert to i128 for calculation let scaled_value = match_each_decimal_value!(decimal_value, |v| { NumToPrimitive::to_i128(v).ok_or_else(|| { @@ -108,7 +105,12 @@ impl<'a> DecimalScalar<'a> { // either formally prove this is all correct or use more checked methods. // Apply scale to get the actual value. - let actual_value = scaled_value as f64 / scale_factor as f64; + let scale = i32::from(self.decimal_type.scale()); + let actual_value = if scale >= 0 { + scaled_value as f64 / 10f64.powi(scale) + } else { + scaled_value as f64 * 10f64.powi(-scale) + }; // Cast to target primitive type. Note that the `as` keyword does **MORE** than // a simple bitcast / memory transmuation. diff --git a/vortex-array/src/scalar/typed_view/decimal/tests.rs b/vortex-array/src/scalar/typed_view/decimal/tests.rs index 85029fc38f7..7a441425c02 100644 --- a/vortex-array/src/scalar/typed_view/decimal/tests.rs +++ b/vortex-array/src/scalar/typed_view/decimal/tests.rs @@ -12,6 +12,7 @@ use crate::dtype::DType; use crate::dtype::DecimalDType; use crate::dtype::DecimalType; use crate::dtype::MAX_PRECISION; +use crate::dtype::MAX_SCALE; use crate::dtype::NativeDecimalType; use crate::dtype::Nullability; use crate::dtype::PType; @@ -1421,3 +1422,61 @@ fn test_fits_in_precision_mixed_decimal_value_types() { assert!(DecimalValue::I128(99999).fits_in_precision(dtype)); assert!(!DecimalValue::I256(i256::from_i128(100000)).fits_in_precision(dtype)); } + +fn cast_decimal_to_f64(precision: u8, scale: i8, stored: i64) -> VortexResult { + let scalar = Scalar::decimal( + DecimalValue::I64(stored), + DecimalDType::new(precision, scale), + Nullability::NonNullable, + ); + let casted = scalar.cast(&DType::Primitive(PType::F64, Nullability::NonNullable))?; + (&casted).try_into() +} + +#[rstest] +#[case::negative_scale(5, -5, 1, 100_000.0)] +#[case::extreme_negative_scale(1, -30, 1, 1e30)] +#[case::scale_past_i128(MAX_PRECISION, 39, 1, 1e-39)] +#[case::max_scale(MAX_PRECISION, MAX_SCALE, 1, 1e-76)] +#[case::negative_value_negative_scale(5, -5, -1, -100_000.0)] +fn cast_decimal_to_f64_spans_the_scale_domain( + #[case] precision: u8, + #[case] scale: i8, + #[case] stored: i64, + #[case] expected: f64, +) -> VortexResult<()> { + let value = cast_decimal_to_f64(precision, scale, stored)?; + // `10^scale` is not exact in `f64` above `10^22`. + assert!( + (value - expected).abs() <= expected.abs() * 1e-15, + "decimal({precision},{scale}) storing {stored} cast to {value}, expected {expected}" + ); + Ok(()) +} + +#[rstest] +#[case(10, 2, 12345, 123.45)] +#[case(10, 0, -7, -7.0)] +#[case(20, 6, 1_500_000, 1.5)] +fn cast_decimal_to_f64_is_exact_for_common_scales( + #[case] precision: u8, + #[case] scale: i8, + #[case] stored: i64, + #[case] expected: f64, +) -> VortexResult<()> { + assert_eq!(cast_decimal_to_f64(precision, scale, stored)?, expected); + Ok(()) +} + +#[test] +fn cast_decimal_to_narrow_float_uses_the_same_scale_factor() -> VortexResult<()> { + let scalar = Scalar::decimal( + DecimalValue::I64(1), + DecimalDType::new(5, -5), + Nullability::NonNullable, + ); + let f32_value: f32 = + (&scalar.cast(&DType::Primitive(PType::F32, Nullability::NonNullable))?).try_into()?; + assert_eq!(f32_value, 100_000.0); + Ok(()) +}