This is an automated email from the ASF dual-hosted git repository.

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 3da02008f9 [Fix][Transform-V2] Preserve exact numeric array extrema 
(#12364)
3da02008f9 is described below

commit 3da02008f96d5a1b6351ffd5762d364553086c8e
Author: Goutam Adwant <[email protected]>
AuthorDate: Wed Oct 21 06:22:50 2026 -0700

    [Fix][Transform-V2] Preserve exact numeric array extrema (#12364)
    
    Signed-off-by: Goutam Adwant <[email protected]>
---
 .../introduction/concepts/incompatible-changes.md  |  20 +++
 docs/en/transforms/sql-functions.md                |  20 +++
 .../introduction/concepts/incompatible-changes.md  |  16 +++
 docs/zh/transforms/sql-functions.md                |  14 +++
 .../sql_transform/func_array_max_min.conf          |  52 +++++++-
 .../sql/zeta/functions/ArrayFunction.java          |  76 ++++++++---
 .../transform/sql/SQLNumericFunctionsTest.java     |  50 ++++++++
 .../sql/zeta/functions/ArrayFunctionTest.java      | 140 +++++++++++++++++++++
 8 files changed, 363 insertions(+), 25 deletions(-)

diff --git a/docs/en/introduction/concepts/incompatible-changes.md 
b/docs/en/introduction/concepts/incompatible-changes.md
index 49ead2cc22..1bc8c98f36 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -152,6 +152,26 @@ The Zeta SQL ARRAY function now declares TINYINT elements 
as `ARRAY<TINYINT>`, m
     (`existingConfigMap`) and restrict the member port (5801) with a 
`NetworkPolicy`. Restart the pods
     after upgrading so the new configuration is applied.
 
+### SQL ARRAY_MAX and ARRAY_MIN Precision
+
+- **Behavior correction: exact integral and DECIMAL array extrema**
+  - **Affected component**: `seatunnel-transforms-v2`, `Sql` transform with
+    `engine = ZETA` (default) or `INTERNAL`, on Zeta, Flink and Spark 
execution engines.
+  - **Description**: Arrays containing only Byte, Short, Integer, Long and 
BigDecimal
+    values are compared exactly, including mixtures of integral and DECIMAL 
values.
+    For example, `ARRAY_MAX` over BIGINT values `[9007199254740992, 
9007199254740993]`
+    now selects `9007199254740993`. A Long `1` and BigDecimal
+    `1.00000000000000000001` no longer compare equal. If any Float, Double or 
other
+    Number subtype (including BigInteger) is present, the whole array retains
+    legacy double-based ordering and its possible precision loss.
+  - **Impact**: Jobs relying on rounded comparisons may select different 
values.
+    The selected original element retains its type and DECIMAL scale, so a 
different
+    result may also have a different original scale. Equal values retain the 
first
+    element. Declared output types, input coercion, null handling and 
floating-point
+    ordering are unchanged.
+  - **Migration Guide**: No configuration or state migration is required. 
Review
+    downstream expectations that depended on the previous rounded results.
+
 ### Redis Authentication
 
 - Redis sources and sinks now authenticate as the configured nonblank `user` 
in both `SINGLE` and
diff --git a/docs/en/transforms/sql-functions.md 
b/docs/en/transforms/sql-functions.md
index ab54fb975e..025ea939cd 100644
--- a/docs/en/transforms/sql-functions.md
+++ b/docs/en/transforms/sql-functions.md
@@ -443,6 +443,22 @@ ACOS(D)
 
 The MAX function returns the maximum value of the expression.
 
+For the `Sql` transform with `engine = ZETA` (the default) or `INTERNAL`, 
integral
+arrays (including BIGINT), DECIMAL arrays and mixtures of those values are
+compared without conversion to floating point. This applies on Zeta, Flink and
+Spark execution engines; the SQL engine option is separate from the execution
+engine. The result retains the selected
+element's type and, for DECIMAL, its scale. Null elements are ignored; a null,
+empty or all-null array returns null. Equal values retain the first element.
+FLOAT and DOUBLE arrays retain their existing ordering: NaN is greater than all
+other values, and positive zero is greater than negative zero.
+Arrays containing only Byte, Short, Integer and Long values use exact integral
+comparison. If BigDecimal is also present, those integral values are compared 
as
+exact decimals. If any Float, Double or other Number subtype (including
+BigInteger) is present, the whole array retains the existing double-based
+comparison and can lose precision for large or close values. This does not 
change
+array construction, declared element types or input coercion.
+
 Example:
 
 ARRAY_MAX(I)
@@ -453,6 +469,10 @@ ARRAY_MAX(I)
 
 The MIN function returns the minimum value of the expression.
 
+The `Sql` transform with `engine = ZETA` or `INTERNAL` uses the same precision,
+null handling and ordering rules as
+`ARRAY_MAX`, selecting the minimum element instead.
+
 Example:
 
 ARRAY_MIN(I)
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md 
b/docs/zh/introduction/concepts/incompatible-changes.md
index bb356271da..45a9f1c2a0 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -111,6 +111,22 @@ Zeta SQL ARRAY 函数现在将 TINYINT 元素声明为 `ARRAY<TINYINT>`,与实
   并使用 `schema_save_mode = "ERROR_WHEN_SCHEMA_NOT_EXIST"` 保留该表结构。不要对手动定义的目标表使用
   `RECREATE_SCHEMA`。任意指定索引前缀长度可能拒绝前缀相同但完整值不同的源主键,因此无法保持原有主键语义。
 
+### SQL ARRAY_MAX 和 ARRAY_MIN 精度
+
+- **行为修正:整数和 DECIMAL 数组极值的精确比较**
+  - **影响范围**:`seatunnel-transforms-v2`,使用 `engine = ZETA`(默认值)或
+    `INTERNAL` 的 `Sql` 转换,适用于 Zeta、Flink 和 Spark 执行引擎。
+  - **变更说明**:仅包含 Byte、Short、Integer、Long 和 BigDecimal 值的数组采用精确比较,
+    包括整数和 DECIMAL 值混合的情况。例如,对 BIGINT 值
+    `[9007199254740992, 9007199254740993]` 使用 `ARRAY_MAX`,现在选择 
`9007199254740993`。
+    Long 值 `1` 与 BigDecimal 值 `1.00000000000000000001` 不再被视为相等。
+    只要包含 Float、Double 或其他 Number 子类型(包括 BigInteger),整个数组仍采用
+    原有的 double 排序,可能损失精度。
+  - **影响**:依赖舍入比较结果的作业可能选择不同的值。结果保留所选原始元素的类型和
+    DECIMAL 小数位数,因此不同的结果也可能具有不同的原始小数位数。值相等时保留第一个
+    元素。声明的输出类型、输入类型转换、null 处理和浮点排序规则保持不变。
+  - **迁移指南**:不需要迁移配置或状态。请检查依赖旧舍入结果的下游预期值。
+
 ### Redis 认证
 
 - Redis Source 和 Sink 现在会在 `SINGLE` 和 `CLUSTER` 模式下以非空白的 `user` 指定的用户认证。
diff --git a/docs/zh/transforms/sql-functions.md 
b/docs/zh/transforms/sql-functions.md
index 99278882a7..262564dabb 100644
--- a/docs/zh/transforms/sql-functions.md
+++ b/docs/zh/transforms/sql-functions.md
@@ -444,6 +444,17 @@ ACOS(D)
 
 MAX 函数返回表达式的最大值。
 
+`Sql` 转换使用 `engine = ZETA`(默认值)或 `INTERNAL` 时,整数数组(包括 BIGINT)、
+DECIMAL 数组及这两类值混合的数组在比较时不会转换为浮点数。这适用于 Zeta、Flink 和
+Spark 执行引擎;SQL 引擎选项与执行引擎是不同的概念。
+结果保留所选元素的类型,以及 DECIMAL 元素的小数位数。忽略 null 元素;数组为 null、
+空数组或所有元素均为 null 时返回 null。值相等时保留第一个元素。
+FLOAT 和 DOUBLE 数组保持现有排序规则:NaN 大于其他所有值,正零大于负零。
+仅包含 Byte、Short、Integer 和 Long 值的数组使用精确整数比较。
+同时包含 BigDecimal 时,这些整数值按精确十进制数进行比较。
+只要包含 Float、Double 或其他 Number 子类型(包括 BigInteger),整个数组仍使用原有的
+double 比较,较大或相近的值可能损失精度。此行为不改变数组构造、声明的元素类型或输入类型转换。
+
 示例:
 
 ARRAY_MAX(I)
@@ -454,6 +465,9 @@ ARRAY_MAX(I)
 
 MIN 函数返回表达式的最小值。
 
+`Sql` 转换使用 `engine = ZETA` 或 `INTERNAL` 时,采用与 `ARRAY_MAX` 相同的精度、null
+处理和排序规则,但选择最小元素。
+
 示例:
 
 ARRAY_MIN(I)
diff --git 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_array_max_min.conf
 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_array_max_min.conf
index 1c7b66bb88..65655cb093 100644
--- 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_array_max_min.conf
+++ 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform/func_array_max_min.conf
@@ -32,12 +32,23 @@ source {
         c_string = string
         c_num_array = "array<int>"
         c_string_array = "array<string>"
+        c_long_array = "array<bigint>"
+        c_first_decimal = "decimal(38,20)"
+        c_second_decimal = "decimal(38,20)"
       }
     }
     rows = [
       {
         kind = INSERT
-        fields = ["c_string",[1,2,3], ["a","b","c"]]
+        fields = ["c_string",[1,2,3], ["a","b","c"],
+          [9007199254740992,9007199254740993],
+          "1.00000000000000000001","1.00000000000000000002"]
+      },
+      {
+        kind = INSERT
+        fields = ["c_string",[3,2,1], ["c","b","a"],
+          [9007199254740993,9007199254740992],
+          "1.00000000000000000002","1.00000000000000000001"]
       }
     ]
   }
@@ -46,13 +57,24 @@ source {
  transform {
    Sql {
      plugin_input = "fake"
+     plugin_output = "arrays"
+     query = """select c_string, c_num_array, c_string_array, c_long_array,
+                     ARRAY(c_first_decimal, c_second_decimal) as 
c_decimal_array
+                     from fake"""
+   }
+   Sql {
+     plugin_input = "arrays"
      plugin_output = "fake1"
      query = """select c_string,
                      ARRAY_MAX(c_num_array) as c_num_max_array,
                      ARRAY_MIN(c_num_array) as c_num_min_array,
                      ARRAY_MAX(c_string_array) as c_string_max_array,
-                     ARRAY_MIN(c_string_array) as c_string_min_array
-                     from fake1"""
+                     ARRAY_MIN(c_string_array) as c_string_min_array,
+                     ARRAY_MAX(c_long_array) as c_long_max_array,
+                     ARRAY_MIN(c_long_array) as c_long_min_array,
+                     ARRAY_MAX(c_decimal_array) as c_decimal_max_array,
+                     ARRAY_MIN(c_decimal_array) as c_decimal_min_array
+                     from arrays"""
    }
  }
 
@@ -64,11 +86,11 @@ sink {
         row_rules = [
           {
             rule_type = MIN_ROW
-            rule_value = 1
+            rule_value = 2
           },
           {
             rule_type = MAX_ROW
-            rule_value = 1
+            rule_value = 2
           }
         ],
         field_rules = [
@@ -106,6 +128,26 @@ sink {
            field_value = [
              {equals_to = "a"}
            ]
+         },
+         {
+           field_name = "c_long_max_array"
+           field_type = "bigint"
+           field_value = [{equals_to = 9007199254740993}]
+         },
+         {
+           field_name = "c_long_min_array"
+           field_type = "bigint"
+           field_value = [{equals_to = 9007199254740992}]
+         },
+         {
+           field_name = "c_decimal_max_array"
+           field_type = "decimal(38,20)"
+           field_value = [{equals_to = "1.00000000000000000002"}]
+         },
+         {
+           field_name = "c_decimal_min_array"
+           field_type = "decimal(38,20)"
+           field_value = [{equals_to = "1.00000000000000000001"}]
          }
         ]
       }
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunction.java
index 75f7dc9407..8a5669e644 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunction.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunction.java
@@ -27,6 +27,7 @@ import 
org.apache.seatunnel.transform.exception.TransformException;
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.Function;
 
+import java.math.BigDecimal;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -50,17 +51,9 @@ public class ArrayFunction {
             return null;
         }
         if (firstNonNullValue instanceof String) {
-            return Arrays.stream(dataList)
-                    .filter(Objects::nonNull)
-                    .map(String.class::cast)
-                    .max(String::compareTo)
-                    .orElse(null);
+            return extremum(dataList, String.class, String::compareTo, true);
         } else if (firstNonNullValue instanceof Number) {
-            return Arrays.stream(dataList)
-                    .filter(Objects::nonNull)
-                    .map(Number.class::cast)
-                    .max(Comparator.comparingDouble(Number::doubleValue))
-                    .orElse(null);
+            return extremum(dataList, Number.class, 
numericComparator(dataList), true);
         }
         Map<String, String> params = new HashMap<>();
         params.put("identifier", "ArrayFunction");
@@ -83,17 +76,9 @@ public class ArrayFunction {
             return null;
         }
         if (firstNonNullValue instanceof String) {
-            return Arrays.stream(dataList)
-                    .filter(Objects::nonNull)
-                    .map(String.class::cast)
-                    .min(String::compareTo)
-                    .orElse(null);
+            return extremum(dataList, String.class, String::compareTo, false);
         } else if (firstNonNullValue instanceof Number) {
-            return Arrays.stream(dataList)
-                    .filter(Objects::nonNull)
-                    .map(Number.class::cast)
-                    .min(Comparator.comparingDouble(Number::doubleValue))
-                    .orElse(null);
+            return extremum(dataList, Number.class, 
numericComparator(dataList), false);
         }
         Map<String, String> params = new HashMap<>();
         params.put("identifier", "ArrayFunction");
@@ -102,6 +87,57 @@ public class ArrayFunction {
         throw new TransformException(CommonErrorCode.UNSUPPORTED_DATA_TYPE, 
params);
     }
 
+    /** Select an original element, retaining the first element when values 
compare equally. */
+    private static <T> T extremum(
+            Object[] values, Class<T> type, Comparator<T> comparator, boolean 
maximum) {
+        T result = null;
+        for (Object value : values) {
+            if (value == null) {
+                continue;
+            }
+            T candidate = type.cast(value);
+            if (result == null) {
+                result = candidate;
+            } else {
+                int comparison = comparator.compare(candidate, result);
+                if (maximum ? comparison > 0 : comparison < 0) {
+                    result = candidate;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Select one ordering for the whole array, ignoring nulls: exact long 
comparison for any mix of
+     * Byte, Short, Integer and Long, exact decimal comparison when those 
types mix with BigDecimal,
+     * and the existing double comparison if any Float, Double or other Number 
subtype (including
+     * BigInteger) is present. Whole-array classification avoids mixing 
inconsistent pairwise
+     * orders.
+     */
+    private static Comparator<Number> numericComparator(Object[] values) {
+        boolean integral = true;
+        for (Object value : values) {
+            if (value == null) {
+                continue;
+            }
+            boolean integralValue =
+                    value instanceof Byte
+                            || value instanceof Short
+                            || value instanceof Integer
+                            || value instanceof Long;
+            if (!integralValue && !(value instanceof BigDecimal)) {
+                return Comparator.comparingDouble(Number::doubleValue);
+            }
+            integral &= integralValue;
+        }
+        if (integral) {
+            return Comparator.comparingLong(Number::longValue);
+        }
+        return (left, right) ->
+                
NumericFunction.toBigDecimal(left).compareTo(NumericFunction.toBigDecimal(right));
+    }
+
     public static Object[] array(List<Object> args) {
         if (args == null || args.isEmpty()) {
             return new Object[0];
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericFunctionsTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericFunctionsTest.java
index f73f83ea8c..bc6ab24d74 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericFunctionsTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericFunctionsTest.java
@@ -22,6 +22,7 @@ import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
 import org.apache.seatunnel.api.table.type.ArrayType;
 import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.DecimalType;
 import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
@@ -30,6 +31,7 @@ import 
org.apache.seatunnel.transform.exception.TransformException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigDecimal;
 import java.util.Collections;
 import java.util.List;
 
@@ -239,6 +241,54 @@ public class SQLNumericFunctionsTest {
         Assertions.assertNull(outRow.getField(1));
     }
 
+    @Test
+    public void testArrayExtremaPreserveExactValuesAndSchema() {
+        DecimalType decimalType = new DecimalType(38, 20);
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"longs", "decimals"},
+                        new SeaTunnelDataType[] {
+                            ArrayType.LONG_ARRAY_TYPE, 
ArrayType.of(decimalType)
+                        });
+        CatalogTable table = CatalogTableUtil.getCatalogTable("test", rowType);
+        SQLTransform transform =
+                new SQLTransform(
+                        ReadonlyConfig.fromMap(
+                                Collections.singletonMap(
+                                        "query",
+                                        "select ARRAY_MAX(longs) as max_l, 
ARRAY_MIN(longs) as min_l, "
+                                                + "ARRAY_MAX(decimals) as 
max_d, ARRAY_MIN(decimals) as min_d from dual")),
+                        table);
+        Assertions.assertArrayEquals(
+                new SeaTunnelDataType[] {
+                    BasicType.LONG_TYPE, BasicType.LONG_TYPE, decimalType, 
decimalType
+                },
+                
transform.getProducedCatalogTable().getSeaTunnelRowType().getFieldTypes());
+        Long low = 9007199254740992L;
+        Long high = 9007199254740993L;
+        BigDecimal lowDecimal = new BigDecimal("1.00000000000000000001");
+        BigDecimal highDecimal = new BigDecimal("1.00000000000000000002");
+        for (SeaTunnelRow row :
+                new SeaTunnelRow[] {
+                    new SeaTunnelRow(
+                            new Object[] {
+                                new Long[] {null, low, high},
+                                new BigDecimal[] {highDecimal, lowDecimal, 
null}
+                            }),
+                    new SeaTunnelRow(
+                            new Object[] {
+                                new Long[] {high, low, null},
+                                new BigDecimal[] {null, lowDecimal, 
highDecimal}
+                            })
+                }) {
+            SeaTunnelRow result = transform.transformRow(row).get(0);
+            Assertions.assertSame(high, result.getField(0));
+            Assertions.assertSame(low, result.getField(1));
+            Assertions.assertSame(highDecimal, result.getField(2));
+            Assertions.assertSame(lowDecimal, result.getField(3));
+        }
+    }
+
     @Test
     public void testTrigonometricFunctions() {
         SeaTunnelRowType rowType =
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunctionTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunctionTest.java
index 6d60690ebb..8c398cd471 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunctionTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/ArrayFunctionTest.java
@@ -35,7 +35,10 @@ import net.sf.jsqlparser.expression.LongValue;
 import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
 import net.sf.jsqlparser.schema.Column;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 class ArrayFunctionTest {
@@ -92,6 +95,143 @@ class ArrayFunctionTest {
         Assertions.assertEquals(1, min);
     }
 
+    @Test
+    void testArrayExtremaPreserveLongPrecision() {
+        assertExtremaInBothOrders(9007199254740992L, 9007199254740993L);
+        assertExtremaInBothOrders(-9007199254740993L, -9007199254740992L);
+        assertExtremaInBothOrders(Long.MAX_VALUE - 1, Long.MAX_VALUE);
+        assertExtremaInBothOrders(Long.MIN_VALUE, Long.MIN_VALUE + 1);
+        assertExtremaInBothOrders(Long.MIN_VALUE, Long.MAX_VALUE);
+    }
+
+    @Test
+    void testArrayExtremaPreserveDecimalPrecision() {
+        assertExtremaInBothOrders(
+                new BigDecimal("1.00000000000000000001"), new 
BigDecimal("1.00000000000000000002"));
+        assertExtremaInBothOrders(
+                new BigDecimal("-1.00000000000000000002"),
+                new BigDecimal("-1.00000000000000000001"));
+        assertExtremaInBothOrders(
+                new BigDecimal("1E+30"), new 
BigDecimal("1E+30").add(BigDecimal.ONE));
+        assertExtremaInBothOrders(new BigDecimal("1E-400"), new 
BigDecimal("2E-400"));
+        assertExtremaInBothOrders(new BigDecimal("1E+400"), new 
BigDecimal("2E+400"));
+    }
+
+    @Test
+    void testArrayExtremaPreserveMixedIntegralDecimalPrecision() {
+        Assertions.assertAll(
+                () -> assertExtremaInBothOrders(1L, new 
BigDecimal("1.00000000000000000001")),
+                () -> assertExtremaInBothOrders(new 
BigDecimal("-1.00000000000000000001"), -1L),
+                () ->
+                        assertExtremaInBothOrders(
+                                Long.MAX_VALUE, new 
BigDecimal("9223372036854775807.1")),
+                () ->
+                        assertExtremaInBothOrders(
+                                new BigDecimal("-9223372036854775808.1"), 
Long.MIN_VALUE),
+                () -> assertExtremaInBothOrders(0L, new BigDecimal("1E-400")),
+                () -> assertExtremaInBothOrders((byte) 1, new 
BigDecimal("1.00000000000000000001")),
+                () ->
+                        assertExtremaInBothOrders(
+                                (short) 1, new 
BigDecimal("1.00000000000000000001")),
+                () -> assertExtremaInBothOrders(1, new 
BigDecimal("1.00000000000000000001")));
+    }
+
+    @Test
+    void testArrayExtremaKeepFirstEqualElement() {
+        BigDecimal first = new BigDecimal("1.00");
+        BigDecimal second = new BigDecimal("1.0");
+        for (Object[] values :
+                new Object[][] {
+                    {null, first, second},
+                    {second, first, null},
+                    {null, 1L, first, 1},
+                    {first, 1, 1L, null},
+                    {null, (byte) 1, (short) 1, 1, 1L}
+                }) {
+            Object expected = values[0] == null ? values[1] : values[0];
+            Assertions.assertSame(
+                    expected, 
ArrayFunction.arrayMax(Collections.singletonList(values)));
+            Assertions.assertSame(
+                    expected, 
ArrayFunction.arrayMin(Collections.singletonList(values)));
+        }
+    }
+
+    @Test
+    void testArrayExtremaPreserveFloatingPointOrdering() {
+        assertExtremaInBothOrders(-0.0d, 0.0d);
+        assertExtremaInBothOrders(Double.NEGATIVE_INFINITY, 
Double.POSITIVE_INFINITY);
+        assertExtremaInBothOrders(Double.POSITIVE_INFINITY, Double.NaN);
+        assertExtremaInBothOrders(-0.0f, 0.0f);
+        assertExtremaInBothOrders(Float.NEGATIVE_INFINITY, 
Float.POSITIVE_INFINITY);
+        assertExtremaInBothOrders(Float.POSITIVE_INFINITY, Float.NaN);
+    }
+
+    @Test
+    void testArrayExtremaPreserveIntegralWrappers() {
+        assertExtremaInBothOrders(Byte.MIN_VALUE, Byte.MAX_VALUE);
+        assertExtremaInBothOrders(Short.MIN_VALUE, Short.MAX_VALUE);
+        assertExtremaInBothOrders(Integer.MIN_VALUE, Integer.MAX_VALUE);
+        assertExtremaInBothOrders(1, 9007199254740993L);
+    }
+
+    @Test
+    void testArrayExtremaPreserveMixedFloatingPointComparison() {
+        Long first = 9007199254740992L;
+        Long second = 9007199254740993L;
+        Double floating = 9007199254740992d;
+        for (Object[] values :
+                new Object[][] {
+                    {first, second, floating}, {second, floating, first}, 
{floating, first, second}
+                }) {
+            Assertions.assertSame(
+                    values[0], 
ArrayFunction.arrayMax(Collections.singletonList(values)));
+            Assertions.assertSame(
+                    values[0], 
ArrayFunction.arrayMin(Collections.singletonList(values)));
+        }
+        BigDecimal decimal = new BigDecimal("1.00000000000000000001");
+        for (Object[] values :
+                new Object[][] {
+                    {decimal, 1d}, {1d, decimal}, {1L, decimal, 1d}, {decimal, 
1L, 1d}
+                }) {
+            Assertions.assertSame(
+                    values[0], 
ArrayFunction.arrayMax(Collections.singletonList(values)));
+            Assertions.assertSame(
+                    values[0], 
ArrayFunction.arrayMin(Collections.singletonList(values)));
+        }
+    }
+
+    @Test
+    void testArrayExtremaPreserveUnknownNumberComparison() {
+        BigInteger integer = new BigInteger("9007199254740993");
+        BigDecimal decimal = new BigDecimal("9007199254740992");
+        Long integral = 9007199254740992L;
+        for (Object[] values :
+                new Object[][] {
+                    {integer, decimal, integral},
+                    {decimal, integral, integer},
+                    {integral, integer, decimal}
+                }) {
+            Assertions.assertSame(
+                    values[0], 
ArrayFunction.arrayMax(Collections.singletonList(values)));
+            Assertions.assertSame(
+                    values[0], 
ArrayFunction.arrayMin(Collections.singletonList(values)));
+        }
+    }
+
+    private void assertExtremaInBothOrders(Number min, Number max) {
+        for (Object[] values : new Object[][] {{null, min, max}, {max, min, 
null}}) {
+            Object[] original = values.clone();
+            Assertions.assertAll(
+                    () ->
+                            Assertions.assertSame(
+                                    max, 
ArrayFunction.arrayMax(Collections.singletonList(values))),
+                    () ->
+                            Assertions.assertSame(
+                                    min, 
ArrayFunction.arrayMin(Collections.singletonList(values))),
+                    () -> Assertions.assertArrayEquals(original, values));
+        }
+    }
+
     @Test
     void testArrayMaxAndMinWithStrings() {
         Object[] values = new Object[] {"a", "c", "b"};

Reply via email to