From 978f8ffb7c5ed95110fccd9c2e1557b5daa9a02a Mon Sep 17 00:00:00 2001 From: suyx1999 Date: Fri, 27 Mar 2026 15:15:08 +0800 Subject: [PATCH 1/3] Fix two UDFs Percentile and Quantile --- library-udf/pom.xml | 24 +- .../iotdb/library/dprofile/UDAFQuantile.java | 36 +-- .../dprofile/util/ExactOrderStatistics.java | 53 ++-- .../iotdb/library/dprofile/util/GKArray.java | 15 +- .../library/dprofile/UDAFPercentileTest.java | 260 ++++++++++++++++++ .../library/dprofile/UDAFQuantileTest.java | 61 ++++ .../ExactOrderStatisticsPercentileTest.java | 85 ++++++ .../library/dprofile/util/GKArrayTest.java | 61 ++++ 8 files changed, 525 insertions(+), 70 deletions(-) create mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java create mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java create mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java create mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java diff --git a/library-udf/pom.xml b/library-udf/pom.xml index e188f5c7fd242..7337fa7670ebf 100644 --- a/library-udf/pom.xml +++ b/library-udf/pom.xml @@ -24,7 +24,7 @@ org.apache.iotdb iotdb-parent - 2.0.7-SNAPSHOT + 1.3.0-SNAPSHOT library-udf IoTDB: UDF @@ -34,14 +34,14 @@ - org.apache.tsfile - common - ${tsfile.version} + org.apache.iotdb + common-api + 1.3.0-SNAPSHOT org.apache.iotdb udf-api - 2.0.7-SNAPSHOT + 1.3.0-SNAPSHOT org.slf4j @@ -58,18 +58,23 @@ org.apache.commons commons-math3 - ${commons-math3.version} org.apache.commons commons-lang3 - ${commons-lang3.version} com.github.wendykierp JTransforms + + + com.github.ggalmazor + lt_downsampling_java8 + + 0.0.6 + com.google.guava guava @@ -79,6 +84,11 @@ junit test + + org.mockito + mockito-core + test + diff --git a/library-udf/src/main/java/org/apache/iotdb/library/dprofile/UDAFQuantile.java b/library-udf/src/main/java/org/apache/iotdb/library/dprofile/UDAFQuantile.java index 047beafe1fc6a..5487f7bb27382 100644 --- a/library-udf/src/main/java/org/apache/iotdb/library/dprofile/UDAFQuantile.java +++ b/library-udf/src/main/java/org/apache/iotdb/library/dprofile/UDAFQuantile.java @@ -86,38 +86,26 @@ public void terminate(PointCollector collector) throws Exception { case DOUBLE: collector.putDouble(0, res); break; - case TIMESTAMP: - case DATE: - case TEXT: - case STRING: - case BLOB: - case BOOLEAN: default: break; } } - private long dataToLong(Object data) { - long result; + private long dataToLong(double res) { switch (dataType) { case INT32: - return (int) data; + return (int) res; case FLOAT: - result = Float.floatToIntBits((float) data); - return (float) data >= 0f ? result : result ^ Long.MAX_VALUE; + float f = (float) res; + long flBits = Float.floatToIntBits(f); + return f >= 0f ? flBits : flBits ^ Long.MAX_VALUE; case INT64: - return (long) data; + return (long) res; case DOUBLE: - result = Double.doubleToLongBits((double) data); - return (double) data >= 0d ? result : result ^ Long.MAX_VALUE; - case BLOB: - case BOOLEAN: - case STRING: - case TEXT: - case DATE: - case TIMESTAMP: + long d = Double.doubleToLongBits(res); + return res >= 0d ? d : d ^ Long.MAX_VALUE; default: - return (long) data; + return (long) res; } } @@ -131,12 +119,6 @@ private double longToResult(long result) { return Double.longBitsToDouble(result); case INT64: case INT32: - case DATE: - case TEXT: - case STRING: - case BOOLEAN: - case BLOB: - case TIMESTAMP: default: return (result); } diff --git a/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatistics.java b/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatistics.java index e1f0baa7c0602..47ca5e2b12fd8 100644 --- a/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatistics.java +++ b/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatistics.java @@ -31,7 +31,14 @@ import java.io.IOException; import java.util.NoSuchElementException; -/** Util for computing median, MAD, percentile. */ +/** + * Util for computing median, MAD, percentile. + * + *

Percentile / quantile ({@link #getPercentile}) uses discrete nearest-rank: for sorted + * size {@code n} and {@code phi} in (0, 1], take 1-based rank {@code k = ceil(n * phi)} and 0-based + * index {@code k - 1}, clamped to {@code [0, n - 1]}. No interpolation; {@code phi = 0.5} is not + * required to match {@link #getMedian}. + */ public class ExactOrderStatistics { private final Type dataType; @@ -55,12 +62,6 @@ public ExactOrderStatistics(Type type) throws UDFInputSeriesDataTypeNotValidExce case DOUBLE: doubleArrayList = new DoubleArrayList(); break; - case STRING: - case TEXT: - case BOOLEAN: - case BLOB: - case DATE: - case TIMESTAMP: default: // This will not happen. throw new UDFInputSeriesDataTypeNotValidException( @@ -88,12 +89,6 @@ public void insert(Row row) throws UDFInputSeriesDataTypeNotValidException, IOEx doubleArrayList.add(vd); } break; - case DATE: - case TIMESTAMP: - case BLOB: - case BOOLEAN: - case TEXT: - case STRING: default: // This will not happen. throw new UDFInputSeriesDataTypeNotValidException( @@ -111,12 +106,6 @@ public double getMedian() throws UDFInputSeriesDataTypeNotValidException { return getMedian(floatArrayList); case DOUBLE: return getMedian(doubleArrayList); - case TEXT: - case STRING: - case BOOLEAN: - case BLOB: - case TIMESTAMP: - case DATE: default: // This will not happen. throw new UDFInputSeriesDataTypeNotValidException( @@ -199,12 +188,6 @@ public double getMad() throws UDFInputSeriesDataTypeNotValidException { return getMad(floatArrayList); case DOUBLE: return getMad(doubleArrayList); - case TIMESTAMP: - case DATE: - case BLOB: - case BOOLEAN: - case STRING: - case TEXT: default: // This will not happen. throw new UDFInputSeriesDataTypeNotValidException( @@ -251,12 +234,18 @@ public static double getMad(LongArrayList nums) { } } + /** Discrete nearest-rank index into sorted data of length {@code n}; see class Javadoc. */ + private static int discreteNearestRankIndex(int n, double phi) { + int idx = (int) Math.ceil(n * phi) - 1; + return Math.max(0, Math.min(n - 1, idx)); + } + public static float getPercentile(FloatArrayList nums, double phi) { if (nums.isEmpty()) { throw new NoSuchElementException(); } else { nums.sortThis(); - return nums.get((int) Math.ceil(nums.size() * phi)); + return nums.get(discreteNearestRankIndex(nums.size(), phi)); } } @@ -265,7 +254,7 @@ public static double getPercentile(DoubleArrayList nums, double phi) { throw new NoSuchElementException(); } else { nums.sortThis(); - return nums.get((int) Math.ceil(nums.size() * phi)); + return nums.get(discreteNearestRankIndex(nums.size(), phi)); } } @@ -279,12 +268,6 @@ public String getPercentile(double phi) throws UDFInputSeriesDataTypeNotValidExc return Float.toString(getPercentile(floatArrayList, phi)); case DOUBLE: return Double.toString(getPercentile(doubleArrayList, phi)); - case STRING: - case TEXT: - case BOOLEAN: - case BLOB: - case DATE: - case TIMESTAMP: default: // This will not happen. throw new UDFInputSeriesDataTypeNotValidException( @@ -297,7 +280,7 @@ public static int getPercentile(IntArrayList nums, double phi) { throw new NoSuchElementException(); } else { nums.sortThis(); - return nums.get((int) Math.ceil(nums.size() * phi)); + return nums.get(discreteNearestRankIndex(nums.size(), phi)); } } @@ -306,7 +289,7 @@ public static long getPercentile(LongArrayList nums, double phi) { throw new NoSuchElementException(); } else { nums.sortThis(); - return nums.get((int) Math.ceil(nums.size() * phi)); + return nums.get(discreteNearestRankIndex(nums.size(), phi)); } } } diff --git a/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/GKArray.java b/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/GKArray.java index 1870bdfb7a4c2..7dbcc934e7860 100644 --- a/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/GKArray.java +++ b/library-udf/src/main/java/org/apache/iotdb/library/dprofile/util/GKArray.java @@ -124,6 +124,19 @@ private void compress(List additionalEntries) { i++; + } else if (i >= additionalEntries.size()) { + // Only sketch entries left (must check before comparing additionalEntries.get(i)). + if (j + 1 < entries.size() + && entries.get(j).g + entries.get(j + 1).g + entries.get(j + 1).delta + <= removalThreshold) { + // Removable from sketch. + entries.get(j + 1).g += entries.get(j).g; + } else { + mergedEntries.add(entries.get(j)); + } + + j++; + } else if (additionalEntries.get(i).v < entries.get(j).v) { if (additionalEntries.get(i).g + entries.get(j).g + entries.get(j).delta <= removalThreshold) { @@ -136,7 +149,7 @@ private void compress(List additionalEntries) { i++; - } else { // the same as i == additionalEntries.size() + } else { if (j + 1 < entries.size() && entries.get(j).g + entries.get(j + 1).g + entries.get(j + 1).delta <= removalThreshold) { diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java new file mode 100644 index 0000000000000..fb7721278aa47 --- /dev/null +++ b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.library.dprofile; + +import org.apache.iotdb.udf.api.access.Row; +import org.apache.iotdb.udf.api.collector.PointCollector; +import org.apache.iotdb.udf.api.customizer.config.UDTFConfigurations; +import org.apache.iotdb.udf.api.customizer.parameter.UDFParameters; +import org.apache.iotdb.udf.api.type.Type; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.time.ZoneId; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** Tests {@link UDAFPercentile} exact (error=0) and approximate (GK sketch) paths. */ +public class UDAFPercentileTest { + + @Test + public void exactDoubleDiscreteNearestRankMedian() throws Exception { + // Sorted 1..5, phi=0.5 -> ceil(5*0.5)-1 = 2 -> value 3.0 + runExactDouble(new double[] {5, 1, 4, 2, 3}, new long[] {50, 10, 40, 20, 30}, "0.5", 3.0, 30L); + } + + /** User-style check: ordered inputs 1..5, rank=0.5 -> discrete rank 3. */ + @Test + public void exactSequentialOneToFiveRankHalfReturnsThree() throws Exception { + runExactDouble(new double[] {1, 2, 3, 4, 5}, new long[] {10, 20, 30, 40, 50}, "0.5", 3.0, 30L); + } + + @Test + public void exactDoubleRankOne() throws Exception { + // n=5, phi=1 -> ceil(5)-1 = 4 -> max value 5 + runExactDouble(new double[] {1, 2, 3, 4, 5}, new long[] {1, 2, 3, 4, 5}, "1", 5.0, 5L); + } + + @Test + public void exactIntDiscreteNearestRank() throws Exception { + UDAFPercentile udf = new UDAFPercentile(); + Map attrs = new HashMap<>(); + attrs.put("error", "0"); + attrs.put("rank", "0.5"); + UDFParameters params = + new UDFParameters( + Collections.singletonList("s"), Collections.singletonList(Type.INT32), attrs); + UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); + udf.beforeStart(params, config); + + Row row = Mockito.mock(Row.class); + AtomicInteger rowIdx = new AtomicInteger(0); + int[] values = {5, 1, 4, 2, 3}; + long[] times = {50, 10, 40, 20, 30}; + Mockito.when(row.getInt(0)).thenAnswer(inv -> values[rowIdx.get()]); + Mockito.when(row.getTime()) + .thenAnswer( + inv -> { + long t = times[rowIdx.get()]; + rowIdx.incrementAndGet(); + return t; + }); + + PointCollector collector = Mockito.mock(PointCollector.class); + for (int i = 0; i < values.length; i++) { + udf.transform(row, collector); + } + + Mockito.doAnswer( + inv -> { + Assert.assertEquals(3, (int) inv.getArgument(1)); + Assert.assertEquals(30L, (long) inv.getArgument(0)); + return null; + }) + .when(collector) + .putInt(Mockito.anyLong(), Mockito.anyInt()); + + udf.terminate(collector); + Mockito.verify(collector, Mockito.times(1)).putInt(Mockito.anyLong(), Mockito.anyInt()); + } + + private void runExactDouble( + double[] values, long[] times, String rank, double expectedValue, long expectedTime) + throws Exception { + UDAFPercentile udf = new UDAFPercentile(); + Map attrs = new HashMap<>(); + attrs.put("error", "0"); + attrs.put("rank", rank); + UDFParameters params = + new UDFParameters( + Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); + UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); + udf.beforeStart(params, config); + + Row row = Mockito.mock(Row.class); + AtomicInteger rowIdx = new AtomicInteger(0); + Mockito.when(row.getDouble(0)).thenAnswer(inv -> values[rowIdx.get()]); + Mockito.when(row.getTime()) + .thenAnswer( + inv -> { + long t = times[rowIdx.get()]; + rowIdx.incrementAndGet(); + return t; + }); + + PointCollector collector = Mockito.mock(PointCollector.class); + for (int i = 0; i < values.length; i++) { + udf.transform(row, collector); + } + + Mockito.doAnswer( + inv -> { + Assert.assertEquals(expectedValue, (double) inv.getArgument(1), 0.0); + Assert.assertEquals(expectedTime, (long) inv.getArgument(0)); + return null; + }) + .when(collector) + .putDouble(Mockito.anyLong(), Mockito.anyDouble()); + + udf.terminate(collector); + Mockito.verify(collector, Mockito.times(1)).putDouble(Mockito.anyLong(), Mockito.anyDouble()); + } + + @Test + public void approximateDoubleManyPointsNoCrash() throws Exception { + UDAFPercentile udf = new UDAFPercentile(); + Map attrs = new HashMap<>(); + attrs.put("error", "0.01"); + attrs.put("rank", "0.5"); + UDFParameters params = + new UDFParameters( + Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); + UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); + udf.beforeStart(params, config); + + Row row = Mockito.mock(Row.class); + java.util.Random rnd = new java.util.Random(42); + Mockito.when(row.getDataType(0)).thenReturn(Type.DOUBLE); + Mockito.when(row.getDouble(0)).thenAnswer(inv -> rnd.nextDouble()); + + PointCollector collector = Mockito.mock(PointCollector.class); + final double[] captured = new double[1]; + Mockito.doAnswer( + inv -> { + captured[0] = inv.getArgument(1); + return null; + }) + .when(collector) + .putDouble(Mockito.anyLong(), Mockito.anyDouble()); + + for (int i = 0; i < 8000; i++) { + udf.transform(row, collector); + } + + udf.terminate(collector); + Assert.assertTrue(captured[0] >= 0.0 && captured[0] <= 1.0); + } + + /** + * Approximate path (GK): same five points as exact median; result must stay in sample range (not + * necessarily 3 — sketch is approximate). + */ + @Test + public void approximateOneToFiveRankHalfStaysInSampleRange() throws Exception { + UDAFPercentile udf = new UDAFPercentile(); + Map attrs = new HashMap<>(); + attrs.put("error", "0.01"); + attrs.put("rank", "0.5"); + UDFParameters params = + new UDFParameters( + Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); + UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); + udf.beforeStart(params, config); + + Row row = Mockito.mock(Row.class); + Mockito.when(row.getDataType(0)).thenReturn(Type.DOUBLE); + double[] values = {1, 2, 3, 4, 5}; + AtomicInteger k = new AtomicInteger(0); + Mockito.when(row.getDouble(0)).thenAnswer(inv -> values[k.getAndIncrement()]); + + PointCollector collector = Mockito.mock(PointCollector.class); + final double[] captured = new double[1]; + Mockito.doAnswer( + inv -> { + captured[0] = inv.getArgument(1); + return null; + }) + .when(collector) + .putDouble(Mockito.anyLong(), Mockito.anyDouble()); + + for (int i = 0; i < values.length; i++) { + udf.transform(row, collector); + } + udf.terminate(collector); + + Assert.assertTrue(captured[0] >= 1.0 && captured[0] <= 5.0); + } + + /** + * Approximate path with many points: values 1..N, rank 0.5 should be near (N+1)/2; allow slack + * for GK error parameter. + */ + @Test + public void approximateMedianOfOneToTwoThousandNearMiddle() throws Exception { + final int n = 2000; + UDAFPercentile udf = new UDAFPercentile(); + Map attrs = new HashMap<>(); + attrs.put("error", "0.01"); + attrs.put("rank", "0.5"); + UDFParameters params = + new UDFParameters( + Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); + UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); + udf.beforeStart(params, config); + + Row row = Mockito.mock(Row.class); + Mockito.when(row.getDataType(0)).thenReturn(Type.DOUBLE); + AtomicInteger seq = new AtomicInteger(0); + // Approximate path calls Util.getValueAsDouble once per row (no second getDouble / getTime). + Mockito.when(row.getDouble(0)).thenAnswer(inv -> (double) seq.incrementAndGet()); + + PointCollector collector = Mockito.mock(PointCollector.class); + final double[] captured = new double[1]; + Mockito.doAnswer( + inv -> { + captured[0] = inv.getArgument(1); + return null; + }) + .when(collector) + .putDouble(Mockito.anyLong(), Mockito.anyDouble()); + + for (int i = 0; i < n; i++) { + udf.transform(row, collector); + } + udf.terminate(collector); + + double expectedMid = (n + 1) / 2.0; + Assert.assertEquals(expectedMid, captured[0], 250.0); + } +} diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java new file mode 100644 index 0000000000000..ab005c86b90e9 --- /dev/null +++ b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.library.dprofile; + +import org.apache.iotdb.udf.api.access.Row; +import org.apache.iotdb.udf.api.collector.PointCollector; +import org.apache.iotdb.udf.api.customizer.config.UDTFConfigurations; +import org.apache.iotdb.udf.api.customizer.parameter.UDFParameters; +import org.apache.iotdb.udf.api.type.Type; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.time.ZoneId; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Regression: FLOAT input must not use dataToLong(Object) with boxed Double (ClassCastException). + */ +public class UDAFQuantileTest { + + @Test + public void floatSeriesTransformDoesNotThrow() throws Exception { + UDAFQuantile udf = new UDAFQuantile(); + Map attrs = new HashMap<>(); + attrs.put("K", "100"); + attrs.put("rank", "0.5"); + UDFParameters params = + new UDFParameters( + Collections.singletonList("s"), Collections.singletonList(Type.FLOAT), attrs); + UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); + udf.beforeStart(params, config); + + Row row = Mockito.mock(Row.class); + Mockito.when(row.getDataType(0)).thenReturn(Type.FLOAT); + Mockito.when(row.getFloat(0)).thenReturn(1.25f); + + udf.transform(row, Mockito.mock(PointCollector.class)); + Assert.assertTrue(true); + } +} diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java new file mode 100644 index 0000000000000..39a824be96827 --- /dev/null +++ b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.library.dprofile.util; + +import org.eclipse.collections.impl.list.mutable.primitive.DoubleArrayList; +import org.junit.Assert; +import org.junit.Test; + +/** + * Discrete nearest-rank percentile: index = ceil(n * phi) - 1 clamped to [0, n - 1]. See class + * Javadoc of {@link ExactOrderStatistics}. + */ +public class ExactOrderStatisticsPercentileTest { + + private static DoubleArrayList doubles(double... values) { + DoubleArrayList d = new DoubleArrayList(); + for (double v : values) { + d.add(v); + } + return d; + } + + @Test + public void oneToFiveRankHalfIsThree() { + DoubleArrayList d = doubles(1, 2, 3, 4, 5); + Assert.assertEquals(3.0, ExactOrderStatistics.getPercentile(d, 0.5), 0.0); + } + + @Test + public void oneToFiveUnsortedRankHalfIsThree() { + DoubleArrayList d = doubles(5, 1, 4, 2, 3); + Assert.assertEquals(3.0, ExactOrderStatistics.getPercentile(d, 0.5), 0.0); + } + + @Test + public void oneToFiveRankOneIsMax() { + DoubleArrayList d = doubles(1, 2, 3, 4, 5); + Assert.assertEquals(5.0, ExactOrderStatistics.getPercentile(d, 1.0), 0.0); + } + + @Test + public void oneToFiveRankSmallPhiTakesSmallest() { + DoubleArrayList d = doubles(1, 2, 3, 4, 5); + // ceil(5 * 0.01) - 1 = 0 -> 1 + Assert.assertEquals(1.0, ExactOrderStatistics.getPercentile(d, 0.01), 0.0); + } + + @Test + public void oneToFiveRankPointTwoIsFirstOrderStat() { + DoubleArrayList d = doubles(1, 2, 3, 4, 5); + // ceil(5 * 0.2) - 1 = ceil(1.0) - 1 = 0 -> 1 + Assert.assertEquals(1.0, ExactOrderStatistics.getPercentile(d, 0.2), 0.0); + } + + @Test + public void oneToFiveRankPointFourIsSecondOrderStat() { + DoubleArrayList d = doubles(1, 2, 3, 4, 5); + // ceil(5 * 0.4) - 1 = 1 -> 2 + Assert.assertEquals(2.0, ExactOrderStatistics.getPercentile(d, 0.4), 0.0); + } + + @Test + public void fourElementsRankHalfIsSecond() { + DoubleArrayList d = doubles(1, 2, 3, 4); + // ceil(4 * 0.5) - 1 = 1 -> 2 (discrete; not the arithmetic mean 2.5) + Assert.assertEquals(2.0, ExactOrderStatistics.getPercentile(d, 0.5), 0.0); + } +} diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java new file mode 100644 index 0000000000000..9feef38ddc529 --- /dev/null +++ b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.library.dprofile.util; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Random; + +/** Stress tests for {@link GKArray} merge (including exhausted-additionalEntries path). */ +public class GKArrayTest { + + @Test + public void manyCompressCyclesRandomUniform() { + GKArray gk = new GKArray(0.01); + Random rnd = new Random(0); + for (int i = 0; i < 50_000; i++) { + gk.insert(rnd.nextDouble()); + } + double q = gk.query(0.5); + Assert.assertTrue("median-ish should stay in [0,1]", q >= 0.0 && q <= 1.0); + } + + @Test + public void sequentialForcesMergeWithExistingSketch() { + GKArray gk = new GKArray(0.05); + for (int i = 0; i < 10_000; i++) { + gk.insert(i); + } + double q = gk.query(0.5); + Assert.assertTrue(q >= 0 && q < 10_000); + } + + @Test + public void queryEndpoints() { + GKArray gk = new GKArray(0.02); + for (int i = 1; i <= 100; i++) { + gk.insert(i); + } + double q0 = gk.query(0.01); + double q1 = gk.query(1.0); + Assert.assertTrue(q0 <= q1); + } +} From c079ad1de8248ea54b4356f4a0bace0a85a1b0bb Mon Sep 17 00:00:00 2001 From: suyx1999 Date: Fri, 27 Mar 2026 15:26:20 +0800 Subject: [PATCH 2/3] Add unit test dependency in pom.xml --- library-udf/pom.xml | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/library-udf/pom.xml b/library-udf/pom.xml index 7337fa7670ebf..e188f5c7fd242 100644 --- a/library-udf/pom.xml +++ b/library-udf/pom.xml @@ -24,7 +24,7 @@ org.apache.iotdb iotdb-parent - 1.3.0-SNAPSHOT + 2.0.7-SNAPSHOT library-udf IoTDB: UDF @@ -34,14 +34,14 @@ - org.apache.iotdb - common-api - 1.3.0-SNAPSHOT + org.apache.tsfile + common + ${tsfile.version} org.apache.iotdb udf-api - 1.3.0-SNAPSHOT + 2.0.7-SNAPSHOT org.slf4j @@ -58,23 +58,18 @@ org.apache.commons commons-math3 + ${commons-math3.version} org.apache.commons commons-lang3 + ${commons-lang3.version} com.github.wendykierp JTransforms - - - com.github.ggalmazor - lt_downsampling_java8 - - 0.0.6 - com.google.guava guava @@ -84,11 +79,6 @@ junit test - - org.mockito - mockito-core - test - From ee55d779cbb84d74080139cd6a6b51e16350c982 Mon Sep 17 00:00:00 2001 From: suyx1999 Date: Fri, 27 Mar 2026 15:38:09 +0800 Subject: [PATCH 3/3] Remove Additional Tests --- .../library/dprofile/UDAFPercentileTest.java | 260 ------------------ .../library/dprofile/UDAFQuantileTest.java | 61 ---- .../ExactOrderStatisticsPercentileTest.java | 85 ------ .../library/dprofile/util/GKArrayTest.java | 61 ---- 4 files changed, 467 deletions(-) delete mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java delete mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java delete mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java delete mode 100644 library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java deleted file mode 100644 index fb7721278aa47..0000000000000 --- a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFPercentileTest.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.iotdb.library.dprofile; - -import org.apache.iotdb.udf.api.access.Row; -import org.apache.iotdb.udf.api.collector.PointCollector; -import org.apache.iotdb.udf.api.customizer.config.UDTFConfigurations; -import org.apache.iotdb.udf.api.customizer.parameter.UDFParameters; -import org.apache.iotdb.udf.api.type.Type; - -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -import java.time.ZoneId; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -/** Tests {@link UDAFPercentile} exact (error=0) and approximate (GK sketch) paths. */ -public class UDAFPercentileTest { - - @Test - public void exactDoubleDiscreteNearestRankMedian() throws Exception { - // Sorted 1..5, phi=0.5 -> ceil(5*0.5)-1 = 2 -> value 3.0 - runExactDouble(new double[] {5, 1, 4, 2, 3}, new long[] {50, 10, 40, 20, 30}, "0.5", 3.0, 30L); - } - - /** User-style check: ordered inputs 1..5, rank=0.5 -> discrete rank 3. */ - @Test - public void exactSequentialOneToFiveRankHalfReturnsThree() throws Exception { - runExactDouble(new double[] {1, 2, 3, 4, 5}, new long[] {10, 20, 30, 40, 50}, "0.5", 3.0, 30L); - } - - @Test - public void exactDoubleRankOne() throws Exception { - // n=5, phi=1 -> ceil(5)-1 = 4 -> max value 5 - runExactDouble(new double[] {1, 2, 3, 4, 5}, new long[] {1, 2, 3, 4, 5}, "1", 5.0, 5L); - } - - @Test - public void exactIntDiscreteNearestRank() throws Exception { - UDAFPercentile udf = new UDAFPercentile(); - Map attrs = new HashMap<>(); - attrs.put("error", "0"); - attrs.put("rank", "0.5"); - UDFParameters params = - new UDFParameters( - Collections.singletonList("s"), Collections.singletonList(Type.INT32), attrs); - UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); - udf.beforeStart(params, config); - - Row row = Mockito.mock(Row.class); - AtomicInteger rowIdx = new AtomicInteger(0); - int[] values = {5, 1, 4, 2, 3}; - long[] times = {50, 10, 40, 20, 30}; - Mockito.when(row.getInt(0)).thenAnswer(inv -> values[rowIdx.get()]); - Mockito.when(row.getTime()) - .thenAnswer( - inv -> { - long t = times[rowIdx.get()]; - rowIdx.incrementAndGet(); - return t; - }); - - PointCollector collector = Mockito.mock(PointCollector.class); - for (int i = 0; i < values.length; i++) { - udf.transform(row, collector); - } - - Mockito.doAnswer( - inv -> { - Assert.assertEquals(3, (int) inv.getArgument(1)); - Assert.assertEquals(30L, (long) inv.getArgument(0)); - return null; - }) - .when(collector) - .putInt(Mockito.anyLong(), Mockito.anyInt()); - - udf.terminate(collector); - Mockito.verify(collector, Mockito.times(1)).putInt(Mockito.anyLong(), Mockito.anyInt()); - } - - private void runExactDouble( - double[] values, long[] times, String rank, double expectedValue, long expectedTime) - throws Exception { - UDAFPercentile udf = new UDAFPercentile(); - Map attrs = new HashMap<>(); - attrs.put("error", "0"); - attrs.put("rank", rank); - UDFParameters params = - new UDFParameters( - Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); - UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); - udf.beforeStart(params, config); - - Row row = Mockito.mock(Row.class); - AtomicInteger rowIdx = new AtomicInteger(0); - Mockito.when(row.getDouble(0)).thenAnswer(inv -> values[rowIdx.get()]); - Mockito.when(row.getTime()) - .thenAnswer( - inv -> { - long t = times[rowIdx.get()]; - rowIdx.incrementAndGet(); - return t; - }); - - PointCollector collector = Mockito.mock(PointCollector.class); - for (int i = 0; i < values.length; i++) { - udf.transform(row, collector); - } - - Mockito.doAnswer( - inv -> { - Assert.assertEquals(expectedValue, (double) inv.getArgument(1), 0.0); - Assert.assertEquals(expectedTime, (long) inv.getArgument(0)); - return null; - }) - .when(collector) - .putDouble(Mockito.anyLong(), Mockito.anyDouble()); - - udf.terminate(collector); - Mockito.verify(collector, Mockito.times(1)).putDouble(Mockito.anyLong(), Mockito.anyDouble()); - } - - @Test - public void approximateDoubleManyPointsNoCrash() throws Exception { - UDAFPercentile udf = new UDAFPercentile(); - Map attrs = new HashMap<>(); - attrs.put("error", "0.01"); - attrs.put("rank", "0.5"); - UDFParameters params = - new UDFParameters( - Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); - UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); - udf.beforeStart(params, config); - - Row row = Mockito.mock(Row.class); - java.util.Random rnd = new java.util.Random(42); - Mockito.when(row.getDataType(0)).thenReturn(Type.DOUBLE); - Mockito.when(row.getDouble(0)).thenAnswer(inv -> rnd.nextDouble()); - - PointCollector collector = Mockito.mock(PointCollector.class); - final double[] captured = new double[1]; - Mockito.doAnswer( - inv -> { - captured[0] = inv.getArgument(1); - return null; - }) - .when(collector) - .putDouble(Mockito.anyLong(), Mockito.anyDouble()); - - for (int i = 0; i < 8000; i++) { - udf.transform(row, collector); - } - - udf.terminate(collector); - Assert.assertTrue(captured[0] >= 0.0 && captured[0] <= 1.0); - } - - /** - * Approximate path (GK): same five points as exact median; result must stay in sample range (not - * necessarily 3 — sketch is approximate). - */ - @Test - public void approximateOneToFiveRankHalfStaysInSampleRange() throws Exception { - UDAFPercentile udf = new UDAFPercentile(); - Map attrs = new HashMap<>(); - attrs.put("error", "0.01"); - attrs.put("rank", "0.5"); - UDFParameters params = - new UDFParameters( - Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); - UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); - udf.beforeStart(params, config); - - Row row = Mockito.mock(Row.class); - Mockito.when(row.getDataType(0)).thenReturn(Type.DOUBLE); - double[] values = {1, 2, 3, 4, 5}; - AtomicInteger k = new AtomicInteger(0); - Mockito.when(row.getDouble(0)).thenAnswer(inv -> values[k.getAndIncrement()]); - - PointCollector collector = Mockito.mock(PointCollector.class); - final double[] captured = new double[1]; - Mockito.doAnswer( - inv -> { - captured[0] = inv.getArgument(1); - return null; - }) - .when(collector) - .putDouble(Mockito.anyLong(), Mockito.anyDouble()); - - for (int i = 0; i < values.length; i++) { - udf.transform(row, collector); - } - udf.terminate(collector); - - Assert.assertTrue(captured[0] >= 1.0 && captured[0] <= 5.0); - } - - /** - * Approximate path with many points: values 1..N, rank 0.5 should be near (N+1)/2; allow slack - * for GK error parameter. - */ - @Test - public void approximateMedianOfOneToTwoThousandNearMiddle() throws Exception { - final int n = 2000; - UDAFPercentile udf = new UDAFPercentile(); - Map attrs = new HashMap<>(); - attrs.put("error", "0.01"); - attrs.put("rank", "0.5"); - UDFParameters params = - new UDFParameters( - Collections.singletonList("s"), Collections.singletonList(Type.DOUBLE), attrs); - UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); - udf.beforeStart(params, config); - - Row row = Mockito.mock(Row.class); - Mockito.when(row.getDataType(0)).thenReturn(Type.DOUBLE); - AtomicInteger seq = new AtomicInteger(0); - // Approximate path calls Util.getValueAsDouble once per row (no second getDouble / getTime). - Mockito.when(row.getDouble(0)).thenAnswer(inv -> (double) seq.incrementAndGet()); - - PointCollector collector = Mockito.mock(PointCollector.class); - final double[] captured = new double[1]; - Mockito.doAnswer( - inv -> { - captured[0] = inv.getArgument(1); - return null; - }) - .when(collector) - .putDouble(Mockito.anyLong(), Mockito.anyDouble()); - - for (int i = 0; i < n; i++) { - udf.transform(row, collector); - } - udf.terminate(collector); - - double expectedMid = (n + 1) / 2.0; - Assert.assertEquals(expectedMid, captured[0], 250.0); - } -} diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java deleted file mode 100644 index ab005c86b90e9..0000000000000 --- a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/UDAFQuantileTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.iotdb.library.dprofile; - -import org.apache.iotdb.udf.api.access.Row; -import org.apache.iotdb.udf.api.collector.PointCollector; -import org.apache.iotdb.udf.api.customizer.config.UDTFConfigurations; -import org.apache.iotdb.udf.api.customizer.parameter.UDFParameters; -import org.apache.iotdb.udf.api.type.Type; - -import org.junit.Assert; -import org.junit.Test; -import org.mockito.Mockito; - -import java.time.ZoneId; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Regression: FLOAT input must not use dataToLong(Object) with boxed Double (ClassCastException). - */ -public class UDAFQuantileTest { - - @Test - public void floatSeriesTransformDoesNotThrow() throws Exception { - UDAFQuantile udf = new UDAFQuantile(); - Map attrs = new HashMap<>(); - attrs.put("K", "100"); - attrs.put("rank", "0.5"); - UDFParameters params = - new UDFParameters( - Collections.singletonList("s"), Collections.singletonList(Type.FLOAT), attrs); - UDTFConfigurations config = new UDTFConfigurations(ZoneId.systemDefault()); - udf.beforeStart(params, config); - - Row row = Mockito.mock(Row.class); - Mockito.when(row.getDataType(0)).thenReturn(Type.FLOAT); - Mockito.when(row.getFloat(0)).thenReturn(1.25f); - - udf.transform(row, Mockito.mock(PointCollector.class)); - Assert.assertTrue(true); - } -} diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java deleted file mode 100644 index 39a824be96827..0000000000000 --- a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/ExactOrderStatisticsPercentileTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.iotdb.library.dprofile.util; - -import org.eclipse.collections.impl.list.mutable.primitive.DoubleArrayList; -import org.junit.Assert; -import org.junit.Test; - -/** - * Discrete nearest-rank percentile: index = ceil(n * phi) - 1 clamped to [0, n - 1]. See class - * Javadoc of {@link ExactOrderStatistics}. - */ -public class ExactOrderStatisticsPercentileTest { - - private static DoubleArrayList doubles(double... values) { - DoubleArrayList d = new DoubleArrayList(); - for (double v : values) { - d.add(v); - } - return d; - } - - @Test - public void oneToFiveRankHalfIsThree() { - DoubleArrayList d = doubles(1, 2, 3, 4, 5); - Assert.assertEquals(3.0, ExactOrderStatistics.getPercentile(d, 0.5), 0.0); - } - - @Test - public void oneToFiveUnsortedRankHalfIsThree() { - DoubleArrayList d = doubles(5, 1, 4, 2, 3); - Assert.assertEquals(3.0, ExactOrderStatistics.getPercentile(d, 0.5), 0.0); - } - - @Test - public void oneToFiveRankOneIsMax() { - DoubleArrayList d = doubles(1, 2, 3, 4, 5); - Assert.assertEquals(5.0, ExactOrderStatistics.getPercentile(d, 1.0), 0.0); - } - - @Test - public void oneToFiveRankSmallPhiTakesSmallest() { - DoubleArrayList d = doubles(1, 2, 3, 4, 5); - // ceil(5 * 0.01) - 1 = 0 -> 1 - Assert.assertEquals(1.0, ExactOrderStatistics.getPercentile(d, 0.01), 0.0); - } - - @Test - public void oneToFiveRankPointTwoIsFirstOrderStat() { - DoubleArrayList d = doubles(1, 2, 3, 4, 5); - // ceil(5 * 0.2) - 1 = ceil(1.0) - 1 = 0 -> 1 - Assert.assertEquals(1.0, ExactOrderStatistics.getPercentile(d, 0.2), 0.0); - } - - @Test - public void oneToFiveRankPointFourIsSecondOrderStat() { - DoubleArrayList d = doubles(1, 2, 3, 4, 5); - // ceil(5 * 0.4) - 1 = 1 -> 2 - Assert.assertEquals(2.0, ExactOrderStatistics.getPercentile(d, 0.4), 0.0); - } - - @Test - public void fourElementsRankHalfIsSecond() { - DoubleArrayList d = doubles(1, 2, 3, 4); - // ceil(4 * 0.5) - 1 = 1 -> 2 (discrete; not the arithmetic mean 2.5) - Assert.assertEquals(2.0, ExactOrderStatistics.getPercentile(d, 0.5), 0.0); - } -} diff --git a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java b/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java deleted file mode 100644 index 9feef38ddc529..0000000000000 --- a/library-udf/src/test/java/org/apache/iotdb/library/dprofile/util/GKArrayTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.iotdb.library.dprofile.util; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.Random; - -/** Stress tests for {@link GKArray} merge (including exhausted-additionalEntries path). */ -public class GKArrayTest { - - @Test - public void manyCompressCyclesRandomUniform() { - GKArray gk = new GKArray(0.01); - Random rnd = new Random(0); - for (int i = 0; i < 50_000; i++) { - gk.insert(rnd.nextDouble()); - } - double q = gk.query(0.5); - Assert.assertTrue("median-ish should stay in [0,1]", q >= 0.0 && q <= 1.0); - } - - @Test - public void sequentialForcesMergeWithExistingSketch() { - GKArray gk = new GKArray(0.05); - for (int i = 0; i < 10_000; i++) { - gk.insert(i); - } - double q = gk.query(0.5); - Assert.assertTrue(q >= 0 && q < 10_000); - } - - @Test - public void queryEndpoints() { - GKArray gk = new GKArray(0.02); - for (int i = 1; i <= 100; i++) { - gk.insert(i); - } - double q0 = gk.query(0.01); - double q1 = gk.query(1.0); - Assert.assertTrue(q0 <= q1); - } -}