akupchinskiy opened a new pull request, #12049:
URL: https://github.com/apache/arrow-rs/pull/12049

   # Which issue does this PR close?
   
   - Closes #12048.
   
   # Rationale for this change
   
   Float-to-decimal casts computed `round(10^scale * x)` in `f64`. From 2^53 
on, the result is
   wrong: `295161.0` at scale 15 becomes `295160.999999999967232`. At scale 15 
and above this
   affects most values (see the issue).
   
   # What changes are included in this PR?
   
   In `arrow-cast/src/cast/decimal.rs`:
   
   **Fix.**
   - Below 2^53 the result is unchanged: the rounded floating-point product.
   - From 2^53 on, the shortest decimal representation of the input is scaled 
exactly in integer
     arithmetic. This is the representation arrow displays the float with, from 
`ryu` (already a
     dependency).
   - An integer below 2^53 is its own shortest representation, so it is scaled 
without formatting.
   
   This applies to `float_to_decimal`, `float_to_decimal_opt` and the array 
cast. The doc comment of
   `float_to_decimal` describes the switch.
   
   **Array cast structure, which also makes the common casts faster.**
   - For `precision <= 15`, every valid value is below 10^15 < 2^53, so a 
product of 2^53 or more is
     a precision overflow either way and no exact path is needed. The rounded 
product is compared
     with `10^precision - 1` in `f64` and then converted to `i64`. This also 
rejects NaN, infinities
     and native overflow.
   - Otherwise, a first pass handles products below 2^53 and integers. It has 
no calls in the
     per-value closure: the precision bounds are precomputed, and the `f64` to 
`i64` conversion
     replaces the `__fixdfti` call of the `f64` to `i128` one.
   - The cast is redone with the exact path only when some value needs it. 
Strict casts redo it on
     any failure, so they report the same error as before for the first failing 
row.
   
   Benchmarks: three new cases in `arrow/benches/cast_kernels.rs`, for 
`Decimal128(38, 15)` and
   `(38, 20)` casts of random doubles and of integer doubles.
   
   # Are these changes tested?
   
   Yes, in `arrow-cast/src/cast/decimal.rs`:
   
   - `test_float_to_decimal_large_products`: the cases from the issue. It also 
checks that values
     below 2^53 are unchanged (for example, `1.005` at scale 2 is still `100`), 
plus tiny, huge and
     non-finite inputs.
   - `test_float_to_decimal_matches_reference`: 20,000 random doubles (all four 
decimal widths,
     scales -40 to 79) and 20,000 doubles whose product lies near 2^53. Below 
2^53 they are checked
     against the old formula. From 2^53 on they are checked against an 
independent reference, which
     takes the significant-digit count from `core`'s `Display` and rounds the 
exact `{:.1100}`
     expansion half-to-even to it. One-off runs with 1M values each also passed.
   - `test_cast_float_array_to_decimal_matches_scalar` and
     `test_low_precision_float_array_boundaries` check that the array cast 
matches the scalar
     functions:
     - safe and strict casts, with nulls, NaN and infinities;
     - mixes of large and small products, across precisions and scales;
     - non-zero array offsets;
     - in strict casts, the same first error.
   
   Benchmarks (`cast_kernels`, `float(32|64) (integers )?to decimal`), Apple M2 
Pro, median time per
   array:
   
   | case | main | this PR | change |
   |---|---|---|---|
   | float64 to decimal128(32, 3), safe | 35.0 µs | 15.9 µs | -54% |
   | float32 to decimal32(7, 2), safe | 18.7 µs | 11.3 µs | -39% |
   | float32 to decimal32(7, 2), strict | 20.3 µs | 9.9 µs | -51% |
   | float64 to decimal128(20, 3), strict | 36.1 µs | 14.6 µs | -60% |
   | float64 to decimal256(40, -2), safe | 378 µs | 30.6 µs | -92% |
   | float64 to decimal64(3, 0), precision overflow | 30.3 µs | 15.2 µs | -50% |
   | float64 integers to decimal128(38, 15) (new) | 35.3 µs | 19.9 µs | -44% |
   | float64 to decimal128(38, 15) random (new) | 35.0 µs | 428 µs | +1123% |
   | float64 to decimal128(38, 20) random (new) | 35.8 µs | 428 µs | +1095% |
   
   The only slower cases are high-scale casts of non-integers, where the 
previous results were
   wrong (see the issue). There the cost is formatting the shortest digits with 
`ryu`. A `ryu` API
   that returns the decimal digits and exponent without formatting would remove 
most of it.
   
   # Are there any significant user-facing changes?
   
   Yes: casts whose scaled value is 2^53 or more now return the correct 
decimal. Results below 2^53
   do not change. No public API changes.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to