HyukjinKwon commented on code in PR #59408:
URL: https://github.com/apache/spark/pull/59408#discussion_r4270322773


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/MathExpressionsSuite.scala:
##########
@@ -748,6 +749,185 @@ class MathExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     checkEvaluation(Truncate(Literal(1.23), Literal.create(null, 
IntegerType)), null)
   }
 
+  Seq("floor", "ceil").foreach { name =>
+    test(s"SPARK-60202: scaled $name preserves Double rounding boundaries") {
+      val cases = if (name == "floor") {
+        Seq(
+          (0.8999999999999999, 1, "0.8"),
+          (1.7999999999999998, 1, "1.7"),
+          (0.9999999999999999, 0, "0"),
+          (-0.9000000000000001, 1, "-1.0"))
+      } else {
+        Seq(
+          (0.9000000000000001, 1, "1.0"),
+          (-0.8999999999999999, 1, "-0.8"),
+          (-1.7999999999999998, 1, "-1.7"),
+          (-0.9999999999999999, 0, "0"))
+      }
+      cases.foreach { case (value, scale, expected) =>
+        val input = BoundReference(0, DoubleType, nullable = true)
+        val rounding = if (name == "floor") {
+          RoundFloor(input, Literal(scale))
+        } else {
+          RoundCeil(input, Literal(scale))
+        }
+        val expression = checkDataTypeAndCast(rounding)
+        assert(expression.dataType == DecimalType(16 + scale, scale))
+        checkEvaluation(expression, Decimal(BigDecimal(expected)), 
InternalRow(value))
+      }
+    }
+  }
+
+  test("SPARK-60202: scaled floor/ceil preserves Float rounding boundaries") {
+    val cases = Seq(
+      (java.lang.Math.nextDown(0.1f), "0.0", "0.1"),
+      (java.lang.Math.nextUp(0.9f), "0.9", "1.0"),
+      (-java.lang.Math.nextDown(0.1f), "-0.1", "0.0"),
+      (-java.lang.Math.nextUp(0.9f), "-1.0", "-0.9"))
+    cases.foreach { case (value, down, up) =>
+      val input = BoundReference(0, FloatType, nullable = true)
+      checkEvaluation(checkDataTypeAndCast(RoundFloor(input, Literal(1))),
+        Decimal(BigDecimal(down)), InternalRow(value))
+      checkEvaluation(checkDataTypeAndCast(RoundCeil(input, Literal(1))),
+        Decimal(BigDecimal(up)), InternalRow(value))
+    }
+  }
+
+  test("SPARK-60202: scaled floor/ceil preserves result types and explicit 
decimal casts") {
+    Seq(ByteType, ShortType, IntegerType, LongType, FloatType, 
DoubleType).foreach { dt =>
+      Seq(-3, 0, 1, 15, 20).foreach { scale =>
+        val input = BoundReference(0, dt, nullable = true)
+        val decimal = Cast(input, DecimalType.forType(dt))
+        val actual = Seq(RoundFloor(input, Literal(scale)), RoundCeil(input, 
Literal(scale)))
+        val previous = Seq(RoundFloor(decimal, Literal(scale)), 
RoundCeil(decimal, Literal(scale)))
+        actual.zip(previous).foreach { case (rounding, control) =>
+          assert(checkDataTypeAndCast(rounding).dataType == control.dataType)
+        }
+      }
+    }
+    val input = BoundReference(0, DoubleType, nullable = true)
+    val explicitCast = Cast(input, DecimalType(30, 15))
+    checkEvaluation(RoundFloor(explicitCast, Literal(1)), 
Decimal(BigDecimal("0.9")),
+      InternalRow(0.8999999999999999))
+    checkEvaluation(RoundCeil(explicitCast, Literal(1)), 
Decimal(BigDecimal("0.9")),
+      InternalRow(0.9000000000000001))
+  }
+
+  test("SPARK-60202: scaled floor/ceil preserves null, non-finite and input 
overflow behavior") {
+    for (ansi <- Seq(false, true); dt <- Seq(FloatType, DoubleType)) {
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> ansi.toString) {
+        def toInput(value: Double): Any = if (dt == FloatType) value.toFloat 
else value
+        val input = BoundReference(0, dt, nullable = true)
+        val expressions = Seq(RoundFloor(input, Literal(1)), RoundCeil(input, 
Literal(1)))
+          .map(checkDataTypeAndCast)
+        expressions.foreach { expression =>
+          checkEvaluation(expression, null, InternalRow(null))
+          Seq(Double.NaN, Double.PositiveInfinity, 
Double.NegativeInfinity).foreach { value =>
+            checkEvaluation(expression, null, InternalRow(toInput(value)))
+          }
+          Seq(0.0, -0.0).foreach { value =>
+            checkEvaluation(expression, Decimal(0), 
InternalRow(toInput(value)))
+          }
+          val limit = if (dt == FloatType) 1e7 else 1e15

Review Comment:
   **Nit (P3):** These `+/-limit` cases only cover the overflow side of the new 
`inputLimit` threshold in `roundFloatingPoint`. No floor/ceil test evaluates a 
large value inside the range, so if the limit were misderived (for example 
`10^(p - s - 1)`) or a value just below it were misclassified, every 
SPARK-60202 test would still pass while valid inputs in `[1e14, 1e15)` / `[1e6, 
1e7)` returned NULL or raised `NUMERIC_VALUE_OUT_OF_RANGE`. Could you add 
in-range cases next to these, e.g. `Math.nextDown(1e15)` and `9999999.0f` (plus 
negations) in both ANSI modes? A scale-0 Double case where 
`ceil(Math.nextDown(1e15), 0)` carries to `1000000000000000` would also pin the 
extra integral digit of the result type.
   
   **Verification:**
   - **Regression:** Lowering or misderiving the input-range threshold (for 
example using 10^(precision - scale - 1), or misclassifying values just below 
the limit) makes the new in-range assertions fail in interpreted and generated 
evaluation.
   - **Behavior:** Near-limit in-range Float and Double inputs produce the 
expected floor/ceil decimals with ANSI enabled and disabled, while the existing 
exactly-at-limit cases keep their NULL/error results.



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