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


##########
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
+                }
+            }
+        }
+    };
+
+    is_validate_decimal_precision(unscaled, precision).then_some(unscaled)
+}
+
+/// Parse ryu's `[-]digits[.digits][e[-]digits]` output into an integer 
mantissa and a
+/// base-10 exponent such that the value equals `mantissa * 10^exp10`. The 
mantissa of a
+/// shortest-form double has at most 17 significant digits so it cannot 
overflow i128.
+fn parse_decimal_notation(s: &str) -> (i128, i32) {
+    let (digits, exp10) = match s.split_once('e') {
+        Some((digits, exp)) => (digits, exp.parse::<i32>().expect("exponent 
from ryu")),
+        None => (s, 0),
+    };
+    let mut mantissa: i128 = 0;
+    let mut frac_digits = 0;
+    let mut in_fraction = false;
+    for b in digits.bytes() {
+        match b {
+            b'-' => {}
+            b'.' => in_fraction = true,
+            _ => {
+                mantissa = mantissa * 10 + (b - b'0') as i128;
+                if in_fraction {
+                    frac_digits += 1;
+                }
+            }
+        }
+    }
+    if digits.starts_with('-') {
+        mantissa = -mantissa;
+    }
+    (mantissa, exp10 - frac_digits)
+}

Review Comment:
   Rewritten in 069b1e0c1: `parse_decimal_notation` now strips the sign, splits 
on `.`, and calls `digits_to_i128` on each part, combining them via `integral * 
10^frac_digits + fractional` the same way `parse_string_to_decimal` does. 
`digits_to_i128` is now `pub(crate)`.
   
   One deliberate difference from your sketch: it returns `Option<(i128, i32)>` 
and `float_to_decimal128` propagates with `?`. `digits_to_i128` and the 
`10^frac_digits` combination are both fallible in the type system even though 
they cannot fail for ryu output, and propagating is better than the previous 
unchecked `mantissa * 10 + digit` (which would have wrapped) or an `expect` 
that panics inside a cast kernel. The doc comment states the invariant that 
makes the `None` arm unreachable.
   
   Also added `test_parse_decimal_notation_round_trips_ryu_output`, which 
asserts the mantissa/exponent round-trips back to the original `f64` for every 
shape ryu emits (plain integral, plain fractional, leading-zero fractional with 
up to 21 fractional digits, and both exponent forms) plus `f64::MAX`, 
`f64::MIN`, `f64::MIN_POSITIVE`, and the smallest subnormal. That is the direct 
guard on the rewrite — the existing differential test only compares the two 
paths against each other, so it would not have caught a parser change that 
moved both.



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