Allow using Range with Comparator.

Remove Comparable requirement for general Range usage. Comparable is only required for the contains(…) check. Alternatively, accept a Comparator.

Closes #2571
This commit is contained in:
Mark Paluch
2022-03-07 09:51:42 +01:00
parent 0e5e869cbf
commit 21e4fd5ffb
2 changed files with 110 additions and 33 deletions

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.domain;
import java.util.Comparator;
import java.util.Optional;
import org.springframework.util.Assert;
@@ -27,7 +28,7 @@ import org.springframework.util.ObjectUtils;
* @author Mark Paluch
* @since 1.10
*/
public final class Range<T extends Comparable<T>> {
public final class Range<T> {
private final static Range<?> UNBOUNDED = Range.of(Bound.unbounded(), Bound.UNBOUNDED);
@@ -57,7 +58,7 @@ public final class Range<T extends Comparable<T>> {
* @since 2.0
*/
@SuppressWarnings("unchecked")
public static <T extends Comparable<T>> Range<T> unbounded() {
public static <T> Range<T> unbounded() {
return (Range<T>) UNBOUNDED;
}
@@ -70,7 +71,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.2
*/
public static <T extends Comparable<T>> Range<T> closed(T from, T to) {
public static <T> Range<T> closed(T from, T to) {
return new Range<>(Bound.inclusive(from), Bound.inclusive(to));
}
@@ -83,7 +84,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.2
*/
public static <T extends Comparable<T>> Range<T> open(T from, T to) {
public static <T> Range<T> open(T from, T to) {
return new Range<>(Bound.exclusive(from), Bound.exclusive(to));
}
@@ -96,7 +97,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.2
*/
public static <T extends Comparable<T>> Range<T> leftOpen(T from, T to) {
public static <T> Range<T> leftOpen(T from, T to) {
return new Range<>(Bound.exclusive(from), Bound.inclusive(to));
}
@@ -109,7 +110,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.2
*/
public static <T extends Comparable<T>> Range<T> rightOpen(T from, T to) {
public static <T> Range<T> rightOpen(T from, T to) {
return new Range<>(Bound.inclusive(from), Bound.exclusive(to));
}
@@ -122,7 +123,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.2
*/
public static <T extends Comparable<T>> Range<T> leftUnbounded(Bound<T> to) {
public static <T> Range<T> leftUnbounded(Bound<T> to) {
return new Range<>(Bound.unbounded(), to);
}
@@ -135,7 +136,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.2
*/
public static <T extends Comparable<T>> Range<T> rightUnbounded(Bound<T> from) {
public static <T> Range<T> rightUnbounded(Bound<T> from) {
return new Range<>(from, Bound.unbounded());
}
@@ -146,7 +147,7 @@ public final class Range<T extends Comparable<T>> {
* @return
* @since 2.0
*/
public static <T extends Comparable<T>> RangeBuilder<T> from(Bound<T> lower) {
public static <T> RangeBuilder<T> from(Bound<T> lower) {
Assert.notNull(lower, "Lower bound must not be null!");
return new RangeBuilder<>(lower);
@@ -161,7 +162,7 @@ public final class Range<T extends Comparable<T>> {
* @since 2.0
* @see #from(Bound)
*/
public static <T extends Comparable<T>> Range<T> of(Bound<T> lowerBound, Bound<T> upperBound) {
public static <T> Range<T> of(Bound<T> lowerBound, Bound<T> upperBound) {
return new Range<>(lowerBound, upperBound);
}
@@ -171,9 +172,9 @@ public final class Range<T extends Comparable<T>> {
* @param <T>
* @param value must not be {@literal null}.
* @return
* @see Range#closed(Comparable, Comparable)
* @see Range#closed(Object, Object)
*/
public static <T extends Comparable<T>> Range<T> just(T value) {
public static <T> Range<T> just(T value) {
return Range.closed(value, value);
}
@@ -183,16 +184,34 @@ public final class Range<T extends Comparable<T>> {
* @param value must not be {@literal null}.
* @return
*/
public boolean contains(T value) {
@SuppressWarnings({ "unchecked" })
public boolean contains(Comparable<T> value) {
return contains((T) value, (o1, o2) -> {
Assert.isInstanceOf(Comparable.class, o1,
"Range value must be an instance of Comparable to use contains(Comparable<T>)");
return ((Comparable<T>) o1).compareTo(o2);
});
}
/**
* Returns whether the {@link Range} contains the given value.
*
* @param value must not be {@literal null}.
* @return
* @since 3.0
*/
public boolean contains(T value, Comparator<T> comparator) {
Assert.notNull(value, "Reference value must not be null!");
boolean greaterThanLowerBound = lowerBound.getValue() //
.map(it -> lowerBound.isInclusive() ? it.compareTo(value) <= 0 : it.compareTo(value) < 0) //
.map(it -> lowerBound.isInclusive() ? comparator.compare(it, value) <= 0 : comparator.compare(it, value) < 0) //
.orElse(true);
boolean lessThanUpperBound = upperBound.getValue() //
.map(it -> upperBound.isInclusive() ? it.compareTo(value) >= 0 : it.compareTo(value) > 0) //
.map(it -> upperBound.isInclusive() ? comparator.compare(it, value) >= 0 : comparator.compare(it, value) > 0) //
.orElse(true);
return greaterThanLowerBound && lessThanUpperBound;
@@ -238,13 +257,13 @@ public final class Range<T extends Comparable<T>> {
/**
* Value object representing a boundary. A boundary can either be {@link #unbounded() unbounded},
* {@link #inclusive(Comparable) including its value} or {@link #exclusive(Comparable) its value}.
* {@link #inclusive(Object) including its value} or {@link #exclusive(Object) its value}.
*
* @author Mark Paluch
* @since 2.0
* @soundtrack Mohamed Ragab - Excelsior Sessions (March 2017)
*/
public static final class Bound<T extends Comparable<T>> {
public static final class Bound<T> {
@SuppressWarnings({ "rawtypes", "unchecked" }) //
private static final Bound<?> UNBOUNDED = new Bound(Optional.empty(), true);
@@ -261,7 +280,7 @@ public final class Range<T extends Comparable<T>> {
* Creates an unbounded {@link Bound}.
*/
@SuppressWarnings("unchecked")
public static <T extends Comparable<T>> Bound<T> unbounded() {
public static <T> Bound<T> unbounded() {
return (Bound<T>) UNBOUNDED;
}
@@ -280,7 +299,7 @@ public final class Range<T extends Comparable<T>> {
* @param value must not be {@literal null}.
* @return
*/
public static <T extends Comparable<T>> Bound<T> inclusive(T value) {
public static <T> Bound<T> inclusive(T value) {
Assert.notNull(value, "Value must not be null!");
return new Bound<>(Optional.of(value), true);
@@ -332,7 +351,7 @@ public final class Range<T extends Comparable<T>> {
* @param value must not be {@literal null}.
* @return
*/
public static <T extends Comparable<T>> Bound<T> exclusive(T value) {
public static <T> Bound<T> exclusive(T value) {
Assert.notNull(value, "Value must not be null!");
return new Bound<>(Optional.of(value), false);
@@ -439,7 +458,7 @@ public final class Range<T extends Comparable<T>> {
* @since 2.0
* @soundtrack Aly and Fila - Future Sound Of Egypt 493
*/
public static class RangeBuilder<T extends Comparable<T>> {
public static class RangeBuilder<T> {
private final Bound<T> lower;

View File

@@ -36,7 +36,7 @@ class RangeUnitTests {
.isThrownBy(() -> Range.from(Bound.inclusive(10L)).to(Bound.inclusive(20L)).contains(null));
}
@Test // DATACMNS-651
@Test // DATACMNS-651, GH-2571
void excludesLowerBoundIfConfigured() {
var range = Range.from(Bound.exclusive(10L)).to(Bound.inclusive(20L));
@@ -46,9 +46,15 @@ class RangeUnitTests {
assertThat(range.contains(15L)).isTrue();
assertThat(range.contains(5L)).isFalse();
assertThat(range.contains(25L)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isFalse();
assertThat(range.contains(20L, Long::compareTo)).isTrue();
assertThat(range.contains(15L, Long::compareTo)).isTrue();
assertThat(range.contains(5L, Long::compareTo)).isFalse();
assertThat(range.contains(25L, Long::compareTo)).isFalse();
}
@Test // DATACMNS-651
@Test // DATACMNS-651, GH-2571
void excludesUpperBoundIfConfigured() {
var range = Range.of(Bound.inclusive(10L), Bound.exclusive(20L));
@@ -58,9 +64,15 @@ class RangeUnitTests {
assertThat(range.contains(15L)).isTrue();
assertThat(range.contains(5L)).isFalse();
assertThat(range.contains(25L)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
assertThat(range.contains(20L, Long::compareTo)).isFalse();
assertThat(range.contains(15L, Long::compareTo)).isTrue();
assertThat(range.contains(5L, Long::compareTo)).isFalse();
assertThat(range.contains(25L, Long::compareTo)).isFalse();
}
@Test // DATACMNS-651, DATACMNS-1050
@Test // DATACMNS-651, DATACMNS-1050, GH-2571
void handlesOpenUpperBoundCorrectly() {
var range = Range.of(Bound.inclusive(10L), Bound.unbounded());
@@ -70,12 +82,19 @@ class RangeUnitTests {
assertThat(range.contains(15L)).isTrue();
assertThat(range.contains(5L)).isFalse();
assertThat(range.contains(25L)).isTrue();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
assertThat(range.contains(20L, Long::compareTo)).isTrue();
assertThat(range.contains(15L, Long::compareTo)).isTrue();
assertThat(range.contains(5L, Long::compareTo)).isFalse();
assertThat(range.contains(25L, Long::compareTo)).isTrue();
assertThat(range.getLowerBound().isBounded()).isTrue();
assertThat(range.getUpperBound().isBounded()).isFalse();
assertThat(range.toString()).isEqualTo("[10-unbounded");
}
@Test // DATACMNS-651, DATACMNS-1050
@Test // DATACMNS-651, DATACMNS-1050, GH-2571
void handlesOpenLowerBoundCorrectly() {
var range = Range.of(Bound.unbounded(), Bound.inclusive(20L));
@@ -85,10 +104,17 @@ class RangeUnitTests {
assertThat(range.contains(15L)).isTrue();
assertThat(range.contains(5L)).isTrue();
assertThat(range.contains(25L)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
assertThat(range.contains(20L, Long::compareTo)).isTrue();
assertThat(range.contains(15L, Long::compareTo)).isTrue();
assertThat(range.contains(5L, Long::compareTo)).isTrue();
assertThat(range.contains(25L, Long::compareTo)).isFalse();
assertThat(range.getLowerBound().isBounded()).isFalse();
assertThat(range.getUpperBound().isBounded()).isTrue();
}
@Test // DATACMNS-1050
void createsInclusiveBoundaryCorrectly() {
@@ -107,7 +133,7 @@ class RangeUnitTests {
assertThat(bound.getValue()).contains(10d);
}
@Test // DATACMNS-1050
@Test // DATACMNS-1050, GH-2571
void createsRangeFromBoundariesCorrectly() {
var lower = Bound.inclusive(10L);
@@ -119,9 +145,14 @@ class RangeUnitTests {
assertThat(range.contains(10L)).isTrue();
assertThat(range.contains(20L)).isTrue();
assertThat(range.contains(21L)).isFalse();
assertThat(range.contains(9L, Long::compareTo)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
assertThat(range.contains(20L, Long::compareTo)).isTrue();
assertThat(range.contains(21L, Long::compareTo)).isFalse();
}
@Test // DATACMNS-1050
@Test // DATACMNS-1050, GH-2571
void shouldExclusiveBuildRangeLowerFirst() {
var range = Range.from(Bound.exclusive(10L)).to(Bound.exclusive(20L));
@@ -132,10 +163,17 @@ class RangeUnitTests {
assertThat(range.contains(19L)).isTrue();
assertThat(range.contains(20L)).isFalse();
assertThat(range.contains(21L)).isFalse();
assertThat(range.contains(9L, Long::compareTo)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isFalse();
assertThat(range.contains(11L, Long::compareTo)).isTrue();
assertThat(range.contains(19L, Long::compareTo)).isTrue();
assertThat(range.contains(20L, Long::compareTo)).isFalse();
assertThat(range.contains(21L, Long::compareTo)).isFalse();
assertThat(range.toString()).isEqualTo("(10-20)");
}
@Test // DATACMNS-1050
@Test // DATACMNS-1050, GH-2571
void shouldBuildRange() {
var range = Range.from(Bound.inclusive(10L)).to(Bound.inclusive(20L));
@@ -146,53 +184,73 @@ class RangeUnitTests {
assertThat(range.contains(19L)).isTrue();
assertThat(range.contains(20L)).isTrue();
assertThat(range.contains(21L)).isFalse();
assertThat(range.contains(9L, Long::compareTo)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
assertThat(range.contains(11L, Long::compareTo)).isTrue();
assertThat(range.contains(19L, Long::compareTo)).isTrue();
assertThat(range.contains(20L, Long::compareTo)).isTrue();
assertThat(range.contains(21L, Long::compareTo)).isFalse();
assertThat(range.toString()).isEqualTo("[10-20]");
}
@Test // DATACMNS-1050
@Test // DATACMNS-1050, GH-2571
void createsUnboundedRange() {
Range<Long> range = Range.unbounded();
assertThat(range.contains(10L)).isTrue();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
assertThat(range.getLowerBound().getValue()).isEmpty();
assertThat(range.getUpperBound().getValue()).isEmpty();
}
@Test // DATACMNS-1499
@Test // DATACMNS-1499, GH-2571
void createsOpenRange() {
var range = Range.open(5L, 10L);
assertThat(range.contains(5L)).isFalse();
assertThat(range.contains(10L)).isFalse();
assertThat(range.contains(5L, Long::compareTo)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isFalse();
}
@Test // DATACMNS-1499
@Test // DATACMNS-1499, GH-2571
void createsClosedRange() {
var range = Range.closed(5L, 10L);
assertThat(range.contains(5L)).isTrue();
assertThat(range.contains(10L)).isTrue();
assertThat(range.contains(5L, Long::compareTo)).isTrue();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
}
@Test // DATACMNS-1499
@Test // DATACMNS-1499, GH-2571
void createsLeftOpenRange() {
var range = Range.leftOpen(5L, 10L);
assertThat(range.contains(5L)).isFalse();
assertThat(range.contains(10L)).isTrue();
assertThat(range.contains(5L, Long::compareTo)).isFalse();
assertThat(range.contains(10L, Long::compareTo)).isTrue();
}
@Test // DATACMNS-1499
@Test // DATACMNS-1499, GH-2571
void createsRightOpenRange() {
var range = Range.rightOpen(5L, 10L);
assertThat(range.contains(5L)).isTrue();
assertThat(range.contains(10L)).isFalse();
assertThat(range.contains(5L, Long::compareTo)).isTrue();
assertThat(range.contains(10L, Long::compareTo)).isFalse();
}
@Test // DATACMNS-1499