apache / apache/arrow-rs

Casting decimal to Float64 / Float32 is not correctly rounded

Open
#10,508 0 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.