Round a decimal value represented as its unscaled integer using HALF_UP rounding mode (ties round away from zero). This matches Spark's `RoundBase` behaviour for `DecimalType`, which calls `BigDecimal.setScale(scale, RoundingMode.HALF_UP)`. Decimals are stored as `(unscaled_value, precision, scale)` where the real value equals `unscaled_value * 10^(-scale)`. This function operates on the unscal
(
value: V,
input_scale: i8,
decimal_places: i32,
)
| 315 | /// round_decimal(2345_i128, 3, 2) → Ok(2350) |
| 316 | /// ``` |
| 317 | fn round_decimal<V: ArrowNativeTypeOp>( |
| 318 | value: V, |
| 319 | input_scale: i8, |
| 320 | decimal_places: i32, |
| 321 | ) -> Result<V> { |
| 322 | let diff = i64::from(input_scale) - i64::from(decimal_places); |
| 323 | if diff <= 0 { |
| 324 | // Nothing to round – the requested precision is finer than (or equal to) the |
| 325 | // stored scale. |
| 326 | return Ok(value); |
| 327 | } |
| 328 | |
| 329 | let diff = diff as u32; |
| 330 | |
| 331 | let one = V::ONE; |
| 332 | let two = V::from_usize(2).ok_or_else(|| { |
| 333 | (exec_err!("Internal error: could not create constant 2") as Result<(), _>) |
| 334 | .unwrap_err() |
| 335 | })?; |
| 336 | let ten = V::from_usize(10).ok_or_else(|| { |
| 337 | (exec_err!("Internal error: could not create constant 10") as Result<(), _>) |
| 338 | .unwrap_err() |
| 339 | })?; |
| 340 | |
| 341 | let Ok(factor) = ten.pow_checked(diff) else { |
| 342 | // 10^diff overflows the decimal type — the rounding position is beyond |
| 343 | // the representable range, so any value rounds to 0. |
| 344 | // This matches Spark's BigDecimal.setScale behavior where rounding to a |
| 345 | // scale far beyond the number's magnitude yields 0. |
| 346 | return Ok(V::ZERO); |
| 347 | }; |
| 348 | |
| 349 | let mut quotient = value.div_wrapping(factor); |
| 350 | let remainder = value.mod_wrapping(factor); |
| 351 | |
| 352 | // HALF_UP: round away from zero when remainder is exactly half |
| 353 | let threshold = factor.div_wrapping(two); |
| 354 | if remainder >= threshold { |
| 355 | quotient = quotient.add_checked(one).map_err(|_| { |
| 356 | (exec_err!("Overflow while rounding decimal") as Result<(), _>).unwrap_err() |
| 357 | })?; |
| 358 | } else if remainder <= threshold.neg_wrapping() { |
| 359 | quotient = quotient.sub_checked(one).map_err(|_| { |
| 360 | (exec_err!("Overflow while rounding decimal") as Result<(), _>).unwrap_err() |
| 361 | })?; |
| 362 | } |
| 363 | |
| 364 | // Re-scale the quotient back to `input_scale` so the returned unscaled integer is |
| 365 | // at the original scale. `factor` is already `10^diff` which is exactly the shift |
| 366 | // we need. |
| 367 | quotient.mul_checked(factor).map_err(|_| { |
| 368 | (exec_err!("Overflow while rounding decimal") as Result<(), _>).unwrap_err() |
| 369 | }) |
| 370 | } |
| 371 | |
| 372 | // --------------------------------------------------------------------------- |
| 373 | // Macros for array dispatch |
no test coverage detected
searching dependent graphs…