Skip to content

Commit d634ac8

Browse files
kosiewscovich
andauthored
Implement full-range i256::to_f64 to eliminate ±∞ saturation for Decimal256 → Float64 casts (#7986)
# Which issue does this PR close? Closes #7985 --- # Rationale for this change The existing Decimal256 → Float64 conversion was changed to **saturate** out-of-range values to `±INFINITY` (PR #7887) in order to avoid panics. However, every 256-bit signed integer actually fits within the exponent range of an IEEE-754 `f64` (±2¹⁰²³), so we can always produce a **finite** `f64`, only sacrificing mantissa precision. By overriding `i256::to_f64` to split the full 256-bit magnitude into high/low 128-bit halves, recombine as ```text (high as f64) * 2^128 + (low as f64) ``` and reapply the sign (special-casing i256::MIN), we: - Eliminate both panics and infinite results - Match Rust’s built-in (i128) as f64 rounding (ties-to-even) - Simplify casting logic—no saturating helpers or extra flags required # What changes are included in this PR? - Added full-range fn to_f64(&self) -> Option<f64> for i256, using checked_abs() + to_parts() + recombination - Removed fallback through 64-bit to_i64()/to_u64() and .unwrap() - Replaced the old decimal256_to_f64 saturating helper with a thin wrapper around the new i256::to_f64() (always returns Some) - Updated Decimal256 → Float64 cast sites to call the new helper ## Tests - Reworked “overflow” tests to assert finite & correctly signed results for i256::MAX and i256::MIN - Added typical-value tests; removed expectations of ∞/-∞ # Are there any user-facing changes? Behavior change: - Very large or small Decimal256 values no longer become +∞/-∞. - They now map to very large—but finite—f64 values (rounded to nearest mantissa). ## API impact: No public API signatures changed. Conversion remains lossy by design; users relying on saturation-to-infinity will observe different (more faithful) behavior. --------- Co-authored-by: Ryan Johnson <[email protected]>
1 parent cbadec7 commit d634ac8

File tree

2 files changed

+57
-19
lines changed

2 files changed

+57
-19
lines changed

arrow-buffer/src/bigint/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,20 @@ impl ToPrimitive for i256 {
821821
}
822822
}
823823

824+
fn to_f64(&self) -> Option<f64> {
825+
let mag = if let Some(u) = self.checked_abs() {
826+
let (low, high) = u.to_parts();
827+
(high as f64) * 2_f64.powi(128) + (low as f64)
828+
} else {
829+
// self == MIN
830+
2_f64.powi(255)
831+
};
832+
if *self < i256::ZERO {
833+
Some(-mag)
834+
} else {
835+
Some(mag)
836+
}
837+
}
824838
fn to_u64(&self) -> Option<u64> {
825839
let as_i128 = self.low as i128;
826840

@@ -1264,4 +1278,29 @@ mod tests {
12641278
}
12651279
}
12661280
}
1281+
1282+
#[test]
1283+
fn test_decimal256_to_f64_typical_values() {
1284+
let v = i256::from_i128(42_i128);
1285+
assert_eq!(v.to_f64().unwrap(), 42.0);
1286+
1287+
let v = i256::from_i128(-123456789012345678i128);
1288+
assert_eq!(v.to_f64().unwrap(), -123456789012345678.0);
1289+
}
1290+
1291+
#[test]
1292+
fn test_decimal256_to_f64_large_positive_value() {
1293+
let max_f = f64::MAX;
1294+
let big = i256::from_f64(max_f * 2.0).unwrap_or(i256::MAX);
1295+
let out = big.to_f64().unwrap();
1296+
assert!(out.is_finite() && out.is_sign_positive());
1297+
}
1298+
1299+
#[test]
1300+
fn test_decimal256_to_f64_large_negative_value() {
1301+
let max_f = f64::MAX;
1302+
let big_neg = i256::from_f64(-(max_f * 2.0)).unwrap_or(i256::MIN);
1303+
let out = big_neg.to_f64().unwrap();
1304+
assert!(out.is_finite() && out.is_sign_negative());
1305+
}
12671306
}

arrow-cast/src/cast/mod.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ pub fn cast_with_options(
907907
scale,
908908
from_type,
909909
to_type,
910-
|x: i256| decimal256_to_f64(x),
910+
|x: i256| x.to_f64().expect("All i256 values fit in f64"),
911911
cast_options,
912912
)
913913
}
@@ -2009,17 +2009,6 @@ where
20092009
}
20102010
}
20112011

2012-
/// Convert a [`i256`] to `f64` saturating to infinity on overflow.
2013-
fn decimal256_to_f64(v: i256) -> f64 {
2014-
v.to_f64().unwrap_or_else(|| {
2015-
if v.is_negative() {
2016-
f64::NEG_INFINITY
2017-
} else {
2018-
f64::INFINITY
2019-
}
2020-
})
2021-
}
2022-
20232012
fn cast_to_decimal<D, M>(
20242013
array: &dyn Array,
20252014
base: M,
@@ -2453,6 +2442,7 @@ where
24532442
#[cfg(test)]
24542443
mod tests {
24552444
use super::*;
2445+
use arrow_buffer::i256;
24562446
use arrow_buffer::{Buffer, IntervalDayTime, NullBuffer};
24572447
use chrono::NaiveDate;
24582448
use half::f16;
@@ -8688,26 +8678,26 @@ mod tests {
86888678
);
86898679
}
86908680
#[test]
8691-
fn test_cast_decimal256_to_f64_overflow() {
8692-
// Test positive overflow (positive infinity)
8681+
fn test_cast_decimal256_to_f64_no_overflow() {
8682+
// Test casting i256::MAX: should produce a large finite positive value
86938683
let array = vec![Some(i256::MAX)];
86948684
let array = create_decimal256_array(array, 76, 2).unwrap();
86958685
let array = Arc::new(array) as ArrayRef;
86968686

86978687
let result = cast(&array, &DataType::Float64).unwrap();
86988688
let result = result.as_primitive::<Float64Type>();
8699-
assert!(result.value(0).is_infinite());
8700-
assert!(result.value(0) > 0.0); // Positive infinity
8689+
assert!(result.value(0).is_finite());
8690+
assert!(result.value(0) > 0.0); // Positive result
87018691

8702-
// Test negative overflow (negative infinity)
8692+
// Test casting i256::MIN: should produce a large finite negative value
87038693
let array = vec![Some(i256::MIN)];
87048694
let array = create_decimal256_array(array, 76, 2).unwrap();
87058695
let array = Arc::new(array) as ArrayRef;
87068696

87078697
let result = cast(&array, &DataType::Float64).unwrap();
87088698
let result = result.as_primitive::<Float64Type>();
8709-
assert!(result.value(0).is_infinite());
8710-
assert!(result.value(0) < 0.0); // Negative infinity
8699+
assert!(result.value(0).is_finite());
8700+
assert!(result.value(0) < 0.0); // Negative result
87118701
}
87128702

87138703
#[test]
@@ -8738,6 +8728,15 @@ mod tests {
87388728
assert_eq!("3123460", decimal_arr.value_as_string(2));
87398729
}
87408730

8731+
#[test]
8732+
fn decimal128_min_max_to_f64() {
8733+
// Ensure Decimal128 i128::MIN/MAX round-trip cast
8734+
let min128 = i128::MIN;
8735+
let max128 = i128::MAX;
8736+
assert_eq!(min128 as f64, min128 as f64);
8737+
assert_eq!(max128 as f64, max128 as f64);
8738+
}
8739+
87418740
#[test]
87428741
fn test_cast_numeric_to_decimal128_negative() {
87438742
let decimal_type = DataType::Decimal128(38, -1);

0 commit comments

Comments
 (0)