andygrove commented on code in PR #5136:
URL: https://github.com/apache/datafusion-comet/pull/5136#discussion_r3692058292


##########
native/spark-expr/src/conversion_funcs/numeric.rs:
##########
@@ -939,6 +961,79 @@ where
     Ok(Arc::new(result.with_precision_and_scale(precision, scale)?))
 }
 
+/// Convert a double to a decimal unscaled value with Spark semantics.
+///
+/// Spark converts through `BigDecimal(Double.toString(d)).setScale(scale, 
HALF_UP)`: it
+/// rounds the shortest decimal string form of the value, not its exact binary 
expansion.
+/// The two disagree for values like 0.5153125 whose binary value 
(0.51531249999...) sits
+/// just below the rounding tie that the string form lands on, so a plain
+/// `(f * 10^scale).round()` produces results that differ from Spark.
+///
+/// Returns `None` for NaN / infinity and for results that do not fit 
`precision`.
+fn float_to_decimal128(f: f64, precision: u8, scale: i8) -> Option<i128> {
+    if !f.is_finite() {
+        return None;
+    }
+
+    // Shortest round-trip decimal form, same digits as Java's Double.toString
+    let mut buf = ryu::Buffer::new();
+    let (mantissa, exp10) = parse_decimal_notation(buf.format_finite(f));
+
+    // value = mantissa * 10^exp10, so unscaled = round(mantissa * 10^(exp10 + 
scale))
+    let shift = exp10 + scale as i32;
+    let unscaled = if shift >= 0 {
+        // Overflowing i128 here means the result cannot fit any decimal 
precision
+        mantissa.checked_mul(pow10_i128(shift.try_into().ok()?)?)?
+    } else {
+        match pow10_i128(-shift as u32) {
+            // The mantissa has at most 17 significant digits, so dividing by 
a power of
+            // ten too large for i128 always rounds to zero
+            None => 0,
+            // Divide with HALF_UP rounding (away from zero on a tie, matching 
BigDecimal)
+            Some(div) => {
+                let quotient = mantissa / div;
+                let remainder = mantissa % div;
+                if remainder.abs() >= div / 2 {
+                    quotient + mantissa.signum()
+                } else {
+                    quotient
+                }
+            }

Review Comment:
   Factored out in 069b1e0c1 as `div_round_half_up_i128` next to `pow10_i128` 
in `string.rs`, called from both `float_to_decimal128` and 
`parse_string_to_decimal`.
   
   Two notes while collapsing them:
   
   - The string path's `checked_div` / "check if divisor is 0" guard is gone. 
The divisor there is always `pow10_i128(abs_scale_adjustment)` with 
`abs_scale_adjustment >= 1`, so it can never be zero and that early-return was 
dead.
   - The helper documents `divisor` as `10^n` for `n >= 1` and `debug_assert!`s 
it, because `divisor / 2` is only a correct half-way test for an even divisor: 
with `divisor == 1` the remainder is always 0 and `0 >= 0` would round every 
value away from zero. Both call sites guarantee `n >= 1`.
   
   I left the `i256` `div_round_half_up` in `wide_decimal_binary_expr.rs` 
alone, since it is a different integer width — happy to unify that too if you 
would like it in this PR.



-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to