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"};