Casting decimal to Float64 / Float32 is not correctly rounded
- Dominant language
- Rust
- Stars
- 3.6k
- Forks
- 1.3k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 167
Description
### Describe the bug
`cast(decimal, Float64)` and `cast(decimal, Float32)` can return a float that is not the one nearest
to the decimal's exact value. `CAST(1.0 AS DECIMAL(38,37))` comes back as `0.9999999999999999`.
Both float targets go through
[`single_decimal_to_float_lossy`](https://github.com/apache/arrow-rs/blob/main/arrow-cast/src/cast/mod.rs#L86),
which computes `f(x) / 10_f64.powi(scale)`. That rounds twice, and there is a second, independent
double rounding on the `Float32` path, which narrows the `f64` result with `as f32`. Details in
*Expected behavior* below.
### To Reproduce
```rust
let array = Decimal128Array::from(vec![10i128.pow(37)])
.with_precision_and_scale(38, 37)
.unwrap();
let out = cast(&array, &DataType::Float64).unwrap();
assert_eq!(out.as_primitive::().value(0), 1.0); // fails: 0.9999999999999999
```
Cast to `Float64`:
| unscaled | scale | got | expected |
|---|---|---|---|
| `12345678901234567890` | 2 | `1.2345678901234566e17` | `1.2345678901234568e17` |
| `10^37` | 37 | `0.9999999999999999` | `1.0` |
| `123456789012345678901` | 20 | `1.234567890123457` | `1.2345678901234567` |
| `1` | 37 | `9.999999999999999e-38` | `1e-37` |
Cast to `Float32`, a separate defect — see *Expected behavior*:
| unscaled | scale | got | expected |
|---|---|---|---|
| `13631072500000000514758830` | 18 | `13631072.0` | `13631073.0` |
| `72073620000000000000000582908005` | 24 | `72073620.0` | `72073624.0` |
| `-3273316900000000000957536840` | 20 | `-32733168.0` | `-32733170.0` |
The `expected` column is what parsing the decimal's own text gives. The second `Float32` row holds
the value `72073616`, which `{:?}` prints as `72073620.0` — the shortest string that round-trips.
### Expected behavior
_No response_
### Additional context
Values with `|unscaled| < 2^53` and `scale <= 22` are unaffected, which is why this is not more
visible: an ordinary `DECIMAL(10,2)` column is fine. It shows up on wide decimals.
Happy to send a PR: keep the current arithmetic as a fast path where both operands are exactly
representable, so it rounds once, and use a correctly rounded conversion otherwise — with a separate
helper for `f32`, since its exact range is much smaller (`10^k` is exact in an `f32` only up to
`k = 10`).
Contributor guide
Research direction
Start in arrow-cast/src/cast/mod.rs at single_decimal_to_float_lossy and reproduce the Float64 and Float32 cases from the issue. Trace the current arithmetic and compare its results with the listed expected values; done means decimal casts produce the correctly rounded Float64 and Float32 results without regressing the unaffected fast-path cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- data
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100