From 146551b07cdf6ad8791b6aa6e2bad400b35cef14 Mon Sep 17 00:00:00 2001 From: Zbynek Vyskovsky Date: Sun, 25 Aug 2024 18:01:49 -0700 Subject: [PATCH 1/3] binarySearch on sorted arrays and sorted lists --- .../org/apache/commons/lang3/ArrayUtils.java | 109 +++++++++++++++++ .../apache/commons/lang3/SortedListUtils.java | 110 ++++++++++++++++++ .../lang3/ArrayUtilsBinarySearchTest.java | 109 +++++++++++++++++ .../commons/lang3/SortedListUtilsTest.java | 84 +++++++++++++ 4 files changed, 412 insertions(+) create mode 100755 src/main/java/org/apache/commons/lang3/SortedListUtils.java create mode 100644 src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java create mode 100644 src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java diff --git a/src/main/java/org/apache/commons/lang3/ArrayUtils.java b/src/main/java/org/apache/commons/lang3/ArrayUtils.java index b299707c322..56b5df638c2 100644 --- a/src/main/java/org/apache/commons/lang3/ArrayUtils.java +++ b/src/main/java/org/apache/commons/lang3/ArrayUtils.java @@ -1432,6 +1432,115 @@ public static T arraycopy(final T source, final int sourcePos, final T dest, return dest; } + /** + * Searches element in array sorted by key. + * + * @param array + * array sorted by key field + * @param key + * key to search for + * @param keyExtractor + * function to extract key from element + * @param comparator + * comparator for keys + * + * @return + * index of the search key, if it is contained in the array; otherwise, (-first_greater - 1). + * The first_greater is the index of lowest greater element in the list - if all elements are lower, the + * first_greater is defined as array.length. + * + * @param + * type of array element + * @param + * type of key + */ + public static int binarySearch( + T[] array, + K key, + Function keyExtractor, Comparator comparator + ) { + return binarySearch0(array, 0, array.length, key, keyExtractor, comparator); + } + + /** + * Searches element in array sorted by key, within range fromIndex (inclusive) - toIndex (exclusive). + * + * @param array + * array sorted by key field + * @param fromIndex + * start index (inclusive) + * @param toIndex + * end index (exclusive) + * @param key + * key to search for + * @param keyExtractor + * function to extract key from element + * @param comparator + * comparator for keys + * + * @return + * index of the search key, if it is contained in the array within specified range; otherwise, + * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements + * are lower, the first_greater is defined as toIndex. + * + * @throws ArrayIndexOutOfBoundsException + * when fromIndex or toIndex is out of array range + * @throws IllegalArgumentException + * when fromIndex is greater than toIndex + * + * @param + * type of array element + * @param + * type of key + */ + public static int binarySearch( + T[] array, + int fromIndex, int toIndex, + K key, + Function keyExtractor, Comparator comparator + ) { + if (fromIndex > toIndex) { + throw new IllegalArgumentException( + "fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + } + if (fromIndex < 0) { + throw new ArrayIndexOutOfBoundsException(fromIndex); + } + if (toIndex > array.length) { + throw new ArrayIndexOutOfBoundsException(toIndex); + } + + return binarySearch0(array, fromIndex, toIndex, key, keyExtractor, comparator); + } + + // common implementation for binarySearch methods, with same semantics: + private static int binarySearch0( + T[] array, + int fromIndex, int toIndex, + K key, + Function keyExtractor, Comparator comparator + ) { + int l = fromIndex; + int h = toIndex - 1; + + while (l <= h) { + final int m = (l + h) >>> 1; // unsigned shift to avoid overflow + final K value = keyExtractor.apply(array[m]); + final int c = comparator.compare(value, key); + if (c < 0) { + l = m + 1; + } else if (c > 0) { + h = m - 1; + } else { + // 0, found + return m; + } + } + + // not found, the l points to the lowest higher match: + return -l - 1; + } + /** * Clones an array or returns {@code null}. *

diff --git a/src/main/java/org/apache/commons/lang3/SortedListUtils.java b/src/main/java/org/apache/commons/lang3/SortedListUtils.java new file mode 100755 index 00000000000..e64778d8de3 --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/SortedListUtils.java @@ -0,0 +1,110 @@ +/* + * 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.commons.lang3; + +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + + +/** + * Operations on sorted {@link List}. + */ +public class SortedListUtils { + /** + * Finds element in sorted list. + * + * @param list + * list sorted by key field + * @param key + * key to search for + * @param keyExtractor + * function to extract key from element + * @param comparator + * comparator for keys + * + * @return + * index of the search key, if it is contained in the list within specified range; otherwise, + * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements + * are lower, the first_greater is defined as toIndex. + * + * @param + * type of list element + * @param + * type of key + */ + public static int binarySearch( + List list, + K key, + Function keyExtractor, Comparator comparator + ) { + return binarySearch(list, 0, list.size(), key, keyExtractor, comparator); + } + + /** + * Finds element in sorted list, within range fromIndex - toIndex (inclusive - exclusive). + * + * @param list + * list sorted by key field + * @param fromIndex + * start index + * @param toIndex + * end index (exclusive) + * @param key + * key to search for + * @param keyExtractor + * function to extract key from element + * @param comparator + * comparator for keys + * + * @return + * index of the search key, if it is contained in the list within specified range; otherwise, + * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements + * are lower, the first_greater is defined as toIndex. + * + * @param + * type of array element + * @param + * type of key + */ + public static int binarySearch( + List list, + int fromIndex, int toIndex, + K key, + Function keyExtractor, Comparator comparator + ) { + int l = fromIndex; + int h = toIndex - 1; + + while (l <= h) { + int m = (l + h) >>> 1; // unsigned shift to avoid overflow + K value = keyExtractor.apply(list.get(m)); + int c = comparator.compare(value, key); + if (c < 0) { + l = m + 1; + } else if (c > 0) { + h = m - 1; + } else { + // 0, found + return m; + } + } + + // not found, the l points to the lowest higher match: + return -l - 1; + } +} diff --git a/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java b/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java new file mode 100644 index 00000000000..f04dba91721 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java @@ -0,0 +1,109 @@ +/* + * 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.commons.lang3; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** + * Unit tests {@link ArrayUtils} binarySearch functions. + */ +public class ArrayUtilsBinarySearchTest extends AbstractLangTest { + + @Test + public void binarySearch_whenLowHigherThanEnd_throw() { + final Data[] list = createList(0, 1); + assertThrowsExactly(IllegalArgumentException.class, () -> ArrayUtils.binarySearch(list, 1, 0, 0, Data::getValue, Integer::compare)); + } + + @Test + public void binarySearch_whenLowNegative_throw() { + final Data[] list = createList(0, 1); + assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> ArrayUtils.binarySearch(list, -1, 0, 0, Data::getValue, Integer::compare)); + } + + @Test + public void binarySearch_whenEndBeyondLength_throw() { + final Data[] list = createList(0, 1); + assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> ArrayUtils.binarySearch(list, 0, 3, 0, Data::getValue, Integer::compare)); + } + + @Test + public void binarySearch_whenEmpty_returnM1() { + final Data[] list = createList(); + final int found = ArrayUtils.binarySearch(list, 0, Data::getValue, Integer::compare); + assertEquals(-1, found); + } + + @Test + public void binarySearch_whenExists_returnIndex() { + final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + final int found = ArrayUtils.binarySearch(list, 9, Data::getValue, Integer::compare); + assertEquals(5, found); + } + + @Test + public void binarySearch_whenNotExistsMiddle_returnMinusInsertion() { + final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + final int found = ArrayUtils.binarySearch(list, 8, Data::getValue, Integer::compare); + assertEquals(-6, found); + } + + @Test + public void binarySearch_whenNotExistsBeginning_returnMinus1() { + final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + final int found = ArrayUtils.binarySearch(list, -3, Data::getValue, Integer::compare); + assertEquals(-1, found); + } + + @Test + public void binarySearch_whenNotExistsEnd_returnMinusLength() { + final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + final int found = ArrayUtils.binarySearch(list, 29, Data::getValue, Integer::compare); + assertEquals(-(list.length + 1), found); + } + + @Test + @Timeout(10) + public void binarySearch_whenUnsorted_dontInfiniteLoop() { + final Data[] list = createList(7, 1, 4, 9, 11, 8); + final int found = ArrayUtils.binarySearch(list, 10, Data::getValue, Integer::compare); + } + + private Data[] createList(int... values) { + return IntStream.of(values).mapToObj(Data::new) + .toArray(Data[]::new); + } + + static class Data { + + private final int value; + + Data(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } +} diff --git a/src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java b/src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java new file mode 100644 index 00000000000..c458215bdaf --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java @@ -0,0 +1,84 @@ +/* + * 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.commons.lang3; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests {@link SortedListUtils}. + */ +public class SortedListUtilsTest extends AbstractLangTest { + + @Test + public void binarySearch_whenEmpty_returnM1() { + List list = createList(); + int found = SortedListUtils.binarySearch(list, 0, Data::getValue, Integer::compare); + assertEquals(-1, found); + } + + @Test + public void binarySearch_whenExists_returnIndex() { + List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + int found = SortedListUtils.binarySearch(list, 9, Data::getValue, Integer::compare); + assertEquals(5, found); + } + + @Test + public void binarySearch_whenNotExists_returnMinusInsertion() { + List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + int found = SortedListUtils.binarySearch(list, 8, Data::getValue, Integer::compare); + assertEquals(-6, found); + } + + @Test + public void binarySearch_whenNotExistsBeginning_returnMinus1() { + List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + int found = SortedListUtils.binarySearch(list, -3, Data::getValue, Integer::compare); + assertEquals(-1, found); + } + + @Test + public void binarySearch_whenNotExistsEnd_returnMinusLength() { + List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); + int found = SortedListUtils.binarySearch(list, 29, Data::getValue, Integer::compare); + assertEquals(-(list.size() + 1), found); + } + + private List createList(int... values) { + return IntStream.of(values).mapToObj(Data::new) + .collect(Collectors.toList()); + } + + public class Data + { + private final int value; + + public Data(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } +} From 76e2a2c63cb8cadf25c1488d0aeecc1a2de4c364 Mon Sep 17 00:00:00 2001 From: Zbynek Vyskovsky Date: Tue, 3 Sep 2024 18:44:44 -0700 Subject: [PATCH 2/3] Remove SortedListUtils binarySearch --- .../apache/commons/lang3/SortedListUtils.java | 110 ------------------ .../commons/lang3/SortedListUtilsTest.java | 84 ------------- 2 files changed, 194 deletions(-) delete mode 100755 src/main/java/org/apache/commons/lang3/SortedListUtils.java delete mode 100644 src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java diff --git a/src/main/java/org/apache/commons/lang3/SortedListUtils.java b/src/main/java/org/apache/commons/lang3/SortedListUtils.java deleted file mode 100755 index e64778d8de3..00000000000 --- a/src/main/java/org/apache/commons/lang3/SortedListUtils.java +++ /dev/null @@ -1,110 +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.commons.lang3; - -import java.util.Comparator; -import java.util.List; -import java.util.function.Function; - - -/** - * Operations on sorted {@link List}. - */ -public class SortedListUtils { - /** - * Finds element in sorted list. - * - * @param list - * list sorted by key field - * @param key - * key to search for - * @param keyExtractor - * function to extract key from element - * @param comparator - * comparator for keys - * - * @return - * index of the search key, if it is contained in the list within specified range; otherwise, - * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements - * are lower, the first_greater is defined as toIndex. - * - * @param - * type of list element - * @param - * type of key - */ - public static int binarySearch( - List list, - K key, - Function keyExtractor, Comparator comparator - ) { - return binarySearch(list, 0, list.size(), key, keyExtractor, comparator); - } - - /** - * Finds element in sorted list, within range fromIndex - toIndex (inclusive - exclusive). - * - * @param list - * list sorted by key field - * @param fromIndex - * start index - * @param toIndex - * end index (exclusive) - * @param key - * key to search for - * @param keyExtractor - * function to extract key from element - * @param comparator - * comparator for keys - * - * @return - * index of the search key, if it is contained in the list within specified range; otherwise, - * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements - * are lower, the first_greater is defined as toIndex. - * - * @param - * type of array element - * @param - * type of key - */ - public static int binarySearch( - List list, - int fromIndex, int toIndex, - K key, - Function keyExtractor, Comparator comparator - ) { - int l = fromIndex; - int h = toIndex - 1; - - while (l <= h) { - int m = (l + h) >>> 1; // unsigned shift to avoid overflow - K value = keyExtractor.apply(list.get(m)); - int c = comparator.compare(value, key); - if (c < 0) { - l = m + 1; - } else if (c > 0) { - h = m - 1; - } else { - // 0, found - return m; - } - } - - // not found, the l points to the lowest higher match: - return -l - 1; - } -} diff --git a/src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java b/src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java deleted file mode 100644 index c458215bdaf..00000000000 --- a/src/test/java/org/apache/commons/lang3/SortedListUtilsTest.java +++ /dev/null @@ -1,84 +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.commons.lang3; - -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests {@link SortedListUtils}. - */ -public class SortedListUtilsTest extends AbstractLangTest { - - @Test - public void binarySearch_whenEmpty_returnM1() { - List list = createList(); - int found = SortedListUtils.binarySearch(list, 0, Data::getValue, Integer::compare); - assertEquals(-1, found); - } - - @Test - public void binarySearch_whenExists_returnIndex() { - List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - int found = SortedListUtils.binarySearch(list, 9, Data::getValue, Integer::compare); - assertEquals(5, found); - } - - @Test - public void binarySearch_whenNotExists_returnMinusInsertion() { - List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - int found = SortedListUtils.binarySearch(list, 8, Data::getValue, Integer::compare); - assertEquals(-6, found); - } - - @Test - public void binarySearch_whenNotExistsBeginning_returnMinus1() { - List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - int found = SortedListUtils.binarySearch(list, -3, Data::getValue, Integer::compare); - assertEquals(-1, found); - } - - @Test - public void binarySearch_whenNotExistsEnd_returnMinusLength() { - List list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - int found = SortedListUtils.binarySearch(list, 29, Data::getValue, Integer::compare); - assertEquals(-(list.size() + 1), found); - } - - private List createList(int... values) { - return IntStream.of(values).mapToObj(Data::new) - .collect(Collectors.toList()); - } - - public class Data - { - private final int value; - - public Data(int value) { - this.value = value; - } - - public int getValue() { - return value; - } - } -} From f56bf0a2d5e1e79fb71985130ded97926f1ac651 Mon Sep 17 00:00:00 2001 From: Zbynek Vyskovsky Date: Thu, 12 Sep 2024 19:29:49 -0700 Subject: [PATCH 3/3] Add binarySearchFirst and binarySearchLast for stable search with duplicit elements --- .../org/apache/commons/lang3/ArrayUtils.java | 166 +++++++++++++++--- .../lang3/ArrayUtilsBinarySearchTest.java | 72 ++++++-- 2 files changed, 197 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/ArrayUtils.java b/src/main/java/org/apache/commons/lang3/ArrayUtils.java index 56b5df638c2..6f2a34d01f2 100644 --- a/src/main/java/org/apache/commons/lang3/ArrayUtils.java +++ b/src/main/java/org/apache/commons/lang3/ArrayUtils.java @@ -1433,7 +1433,8 @@ public static T arraycopy(final T source, final int sourcePos, final T dest, } /** - * Searches element in array sorted by key. + * Searches element in array sorted by key. If there are multiple elements matching, it returns first occurrence. + * If the array is not sorted, the result is undefined. * * @param array * array sorted by key field @@ -1445,25 +1446,26 @@ public static T arraycopy(final T source, final int sourcePos, final T dest, * comparator for keys * * @return - * index of the search key, if it is contained in the array; otherwise, (-first_greater - 1). - * The first_greater is the index of lowest greater element in the list - if all elements are lower, the - * first_greater is defined as array.length. + * index of the first occurrence of search key, if it is contained in the array; otherwise, + * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements + * are lower, the first_greater is defined as array.length. * * @param * type of array element * @param * type of key */ - public static int binarySearch( + public static int binarySearchFirst( T[] array, K key, Function keyExtractor, Comparator comparator ) { - return binarySearch0(array, 0, array.length, key, keyExtractor, comparator); + return binarySearchFirst0(array, 0, array.length, key, keyExtractor, comparator); } /** - * Searches element in array sorted by key, within range fromIndex (inclusive) - toIndex (exclusive). + * Searches element in array sorted by key, within range fromIndex (inclusive) - toIndex (exclusive). If there are + * multiple elements matching, it returns first occurrence. If the array is not sorted, the result is undefined. * * @param array * array sorted by key field @@ -1479,9 +1481,9 @@ public static int binarySearch( * comparator for keys * * @return - * index of the search key, if it is contained in the array within specified range; otherwise, - * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements - * are lower, the first_greater is defined as toIndex. + * index of the first occurrence of search key, if it is contained in the array within specified range; + * otherwise, (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if + * all elements are lower, the first_greater is defined as toIndex. * * @throws ArrayIndexOutOfBoundsException * when fromIndex or toIndex is out of array range @@ -1493,28 +1495,124 @@ public static int binarySearch( * @param * type of key */ - public static int binarySearch( + public static int binarySearchFirst( T[] array, int fromIndex, int toIndex, K key, Function keyExtractor, Comparator comparator ) { - if (fromIndex > toIndex) { - throw new IllegalArgumentException( - "fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); - } - if (fromIndex < 0) { - throw new ArrayIndexOutOfBoundsException(fromIndex); - } - if (toIndex > array.length) { - throw new ArrayIndexOutOfBoundsException(toIndex); + checkRange(array.length, fromIndex, toIndex); + + return binarySearchFirst0(array, fromIndex, toIndex, key, keyExtractor, comparator); + } + + // common implementation for binarySearch methods, with same semantics: + private static int binarySearchFirst0( + T[] array, + int fromIndex, int toIndex, + K key, + Function keyExtractor, Comparator comparator + ) { + int l = fromIndex; + int h = toIndex - 1; + + while (l <= h) { + final int m = (l + h) >>> 1; // unsigned shift to avoid overflow + final K value = keyExtractor.apply(array[m]); + final int c = comparator.compare(value, key); + if (c < 0) { + l = m + 1; + } else if (c > 0) { + h = m - 1; + } else if (l < h) { + // possibly multiple matching items remaining: + h = m; + } else { + // single matching item remaining: + return m; + } } - return binarySearch0(array, fromIndex, toIndex, key, keyExtractor, comparator); + // not found, the l points to the lowest higher match: + return -l - 1; + } + + /** + * Searches element in array sorted by key. If there are multiple elements matching, it returns last occurrence. + * If the array is not sorted, the result is undefined. + * + * @param array + * array sorted by key field + * @param key + * key to search for + * @param keyExtractor + * function to extract key from element + * @param comparator + * comparator for keys + * + * @return + * index of the last occurrence of search key, if it is contained in the array; otherwise, + * (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements + * are lower, the first_greater is defined as array.length. + * + * @param + * type of array element + * @param + * type of key + */ + public static int binarySearchLast( + T[] array, + K key, + Function keyExtractor, Comparator comparator + ) { + return binarySearchLast0(array, 0, array.length, key, keyExtractor, comparator); + } + + /** + * Searches element in array sorted by key, within range fromIndex (inclusive) - toIndex (exclusive). If there are + * multiple elements matching, it returns last occurrence. If the array is not sorted, the result is undefined. + * + * @param array + * array sorted by key field + * @param fromIndex + * start index (inclusive) + * @param toIndex + * end index (exclusive) + * @param key + * key to search for + * @param keyExtractor + * function to extract key from element + * @param comparator + * comparator for keys + * + * @return + * index of the last occurrence of search key, if it is contained in the array within specified range; + * otherwise, (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if + * all elements are lower, the first_greater is defined as toIndex. + * + * @throws ArrayIndexOutOfBoundsException + * when fromIndex or toIndex is out of array range + * @throws IllegalArgumentException + * when fromIndex is greater than toIndex + * + * @param + * type of array element + * @param + * type of key + */ + public static int binarySearchLast( + T[] array, + int fromIndex, int toIndex, + K key, + Function keyExtractor, Comparator comparator + ) { + checkRange(array.length, fromIndex, toIndex); + + return binarySearchLast0(array, fromIndex, toIndex, key, keyExtractor, comparator); } // common implementation for binarySearch methods, with same semantics: - private static int binarySearch0( + private static int binarySearchLast0( T[] array, int fromIndex, int toIndex, K key, @@ -1531,8 +1629,16 @@ private static int binarySearch0( l = m + 1; } else if (c > 0) { h = m - 1; + } else if (m + 1 < h) { + // matching, more than two items remaining: + l = m; + } else if (m + 1 == h) { + // two items remaining, next loops would result in unchanged l and h, we have to choose m or h: + final K valueH = keyExtractor.apply(array[h]); + final int cH = comparator.compare(valueH, key); + return cH == 0 ? h : m; } else { - // 0, found + // one item remaining, single match: return m; } } @@ -9573,4 +9679,18 @@ public static String[] toStringArray(final Object[] array, final String valueFor public ArrayUtils() { // empty } + + static void checkRange(int length, int fromIndex, int toIndex) { + if (fromIndex > toIndex) { + throw new IllegalArgumentException( + "fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); + } + if (fromIndex < 0) { + throw new ArrayIndexOutOfBoundsException(fromIndex); + } + if (toIndex > length) { + throw new ArrayIndexOutOfBoundsException(toIndex); + } + + } } diff --git a/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java b/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java index f04dba91721..62219ae829c 100644 --- a/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java +++ b/src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java @@ -30,63 +30,99 @@ public class ArrayUtilsBinarySearchTest extends AbstractLangTest { @Test - public void binarySearch_whenLowHigherThanEnd_throw() { + public void binarySearchFirst_whenLowHigherThanEnd_throw() { final Data[] list = createList(0, 1); - assertThrowsExactly(IllegalArgumentException.class, () -> ArrayUtils.binarySearch(list, 1, 0, 0, Data::getValue, Integer::compare)); + assertThrowsExactly(IllegalArgumentException.class, () -> + ArrayUtils.binarySearchFirst(list, 1, 0, 0, Data::getValue, Integer::compare)); } @Test - public void binarySearch_whenLowNegative_throw() { + public void binarySearchFirst_whenLowNegative_throw() { final Data[] list = createList(0, 1); - assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> ArrayUtils.binarySearch(list, -1, 0, 0, Data::getValue, Integer::compare)); + assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> + ArrayUtils.binarySearchFirst(list, -1, 0, 0, Data::getValue, Integer::compare)); } @Test - public void binarySearch_whenEndBeyondLength_throw() { + public void binarySearchFirst_whenEndBeyondLength_throw() { final Data[] list = createList(0, 1); - assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> ArrayUtils.binarySearch(list, 0, 3, 0, Data::getValue, Integer::compare)); + assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> + ArrayUtils.binarySearchFirst(list, 0, 3, 0, Data::getValue, Integer::compare)); } @Test - public void binarySearch_whenEmpty_returnM1() { + public void binarySearchLast_whenLowHigherThanEnd_throw() { + final Data[] list = createList(0, 1); + assertThrowsExactly(IllegalArgumentException.class, () -> + ArrayUtils.binarySearchLast(list, 1, 0, 0, Data::getValue, Integer::compare)); + } + + @Test + public void binarySearchFirst_whenEmpty_returnM1() { final Data[] list = createList(); - final int found = ArrayUtils.binarySearch(list, 0, Data::getValue, Integer::compare); + final int found = ArrayUtils.binarySearchFirst(list, 0, Data::getValue, Integer::compare); assertEquals(-1, found); } @Test - public void binarySearch_whenExists_returnIndex() { + public void binarySearchFirst_whenExists_returnIndex() { final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - final int found = ArrayUtils.binarySearch(list, 9, Data::getValue, Integer::compare); + final int found = ArrayUtils.binarySearchFirst(list, 9, Data::getValue, Integer::compare); assertEquals(5, found); } @Test - public void binarySearch_whenNotExistsMiddle_returnMinusInsertion() { + @Timeout(10) + public void binarySearchFirst_whenMultiple_returnFirst() { + final Data[] list = createList(3, 4, 6, 6, 6, 7, 7, 8, 8, 9, 9, 9); + for (int i = 0; i < list.length; ++i) { + if (i > 0 && list[i].value == list[i - 1].value) { + continue; + } + final int found = ArrayUtils.binarySearchFirst(list, list[i].value, Data::getValue, Integer::compare); + assertEquals(i, found); + } + } + + @Test + @Timeout(10) + public void binarySearchLast_whenMultiple_returnFirst() { + final Data[] list = createList(3, 4, 6, 6, 6, 7, 7, 8, 8, 9, 9, 9); + for (int i = 0; i < list.length; ++i) { + if (i < list.length - 1 && list[i].value == list[i + 1].value) { + continue; + } + final int found = ArrayUtils.binarySearchLast(list, list[i].value, Data::getValue, Integer::compare); + assertEquals(i, found); + } + } + + @Test + public void binarySearchFirst_whenNotExistsMiddle_returnMinusInsertion() { final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - final int found = ArrayUtils.binarySearch(list, 8, Data::getValue, Integer::compare); + final int found = ArrayUtils.binarySearchFirst(list, 8, Data::getValue, Integer::compare); assertEquals(-6, found); } @Test - public void binarySearch_whenNotExistsBeginning_returnMinus1() { + public void binarySearchFirst_whenNotExistsBeginning_returnMinus1() { final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - final int found = ArrayUtils.binarySearch(list, -3, Data::getValue, Integer::compare); + final int found = ArrayUtils.binarySearchFirst(list, -3, Data::getValue, Integer::compare); assertEquals(-1, found); } @Test - public void binarySearch_whenNotExistsEnd_returnMinusLength() { + public void binarySearchFirst_whenNotExistsEnd_returnMinusLength() { final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); - final int found = ArrayUtils.binarySearch(list, 29, Data::getValue, Integer::compare); + final int found = ArrayUtils.binarySearchFirst(list, 29, Data::getValue, Integer::compare); assertEquals(-(list.length + 1), found); } @Test @Timeout(10) - public void binarySearch_whenUnsorted_dontInfiniteLoop() { + public void binarySearchFirst_whenUnsorted_dontInfiniteLoop() { final Data[] list = createList(7, 1, 4, 9, 11, 8); - final int found = ArrayUtils.binarySearch(list, 10, Data::getValue, Integer::compare); + final int found = ArrayUtils.binarySearchFirst(list, 10, Data::getValue, Integer::compare); } private Data[] createList(int... values) {