diff --git a/src/main/java/org/springframework/data/domain/Range.java b/src/main/java/org/springframework/data/domain/Range.java index 3b1562af4..f027f24a4 100644 --- a/src/main/java/org/springframework/data/domain/Range.java +++ b/src/main/java/org/springframework/data/domain/Range.java @@ -17,6 +17,7 @@ package org.springframework.data.domain; import java.util.Comparator; import java.util.Optional; +import java.util.function.Function; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -30,7 +31,7 @@ import org.springframework.util.ObjectUtils; */ public final class Range { - private final static Range UNBOUNDED = Range.of(Bound.unbounded(), Bound.UNBOUNDED); + private final static Range UNBOUNDED = Range.of(Bound.unbounded(), Bound.unbounded()); /** * The lower bound of the range. @@ -217,6 +218,22 @@ public final class Range { return greaterThanLowerBound && lessThanUpperBound; } + /** + * Apply a mapping {@link Function} to the lower and upper boundary values. + * + * @param mapper must not be {@literal null}. If the mapper returns {@code null}, then the corresponding boundary + * value represents an {@link Bound#unbounded()} boundary. + * @return a new {@link Range} after applying the value to the mapper. + * @param + * @since 3.0 + */ + public Range map(Function mapper) { + + Assert.notNull(mapper, "Mapping function must not be null"); + + return Range.of(lowerBound.map(mapper), upperBound.map(mapper)); + } + @Override public String toString() { return String.format("%s-%s", lowerBound.toPrefixString(), upperBound.toSuffixString()); @@ -265,8 +282,7 @@ public final class Range { */ public static final class Bound { - @SuppressWarnings({ "rawtypes", "unchecked" }) // - private static final Bound UNBOUNDED = new Bound(Optional.empty(), true); + private static final Bound UNBOUNDED = new Bound<>(Optional.empty(), true); private final Optional value; private final boolean inclusive; @@ -302,7 +318,7 @@ public final class Range { public static Bound inclusive(T value) { Assert.notNull(value, "Value must not be null"); - return new Bound<>(Optional.of(value), true); + return Bound.of(Optional.of(value), true); } /** @@ -354,7 +370,7 @@ public final class Range { public static Bound exclusive(T value) { Assert.notNull(value, "Value must not be null"); - return new Bound<>(Optional.of(value), false); + return Bound.of(Optional.of(value), false); } /** @@ -437,6 +453,10 @@ public final class Range { return false; } + if (!value.isPresent() && !bound.value.isPresent()) { + return true; + } + if (inclusive != bound.inclusive) return false; @@ -445,10 +465,41 @@ public final class Range { @Override public int hashCode() { + + if (!value.isPresent()) { + return ObjectUtils.nullSafeHashCode(value); + } + int result = ObjectUtils.nullSafeHashCode(value); result = 31 * result + (inclusive ? 1 : 0); return result; } + + /** + * Apply a mapping {@link Function} to the boundary value. + * + * @param mapper must not be {@literal null}. If the mapper returns {@code null}, then the boundary value + * corresponds with {@link Bound#unbounded()}. + * @return a new {@link Bound} after applying the value to the mapper. + * @param + * @since 3.0 + */ + public Bound map(Function mapper) { + + Assert.notNull(mapper, "Mapping function must not be null"); + + return Bound.of(value.map(mapper), inclusive); + } + + private static Bound of(Optional value, boolean inclusive) { + + if (value.isPresent()) { + return new Bound<>(value, inclusive); + } + + return unbounded(); + } + } /** diff --git a/src/test/java/org/springframework/data/domain/RangeUnitTests.java b/src/test/java/org/springframework/data/domain/RangeUnitTests.java index 3146b2616..0ccc0b7b0 100755 --- a/src/test/java/org/springframework/data/domain/RangeUnitTests.java +++ b/src/test/java/org/springframework/data/domain/RangeUnitTests.java @@ -253,6 +253,20 @@ class RangeUnitTests { assertThat(range.contains(10L, Long::compareTo)).isFalse(); } + @Test // GH-2692 + void mapsBoundaryValues() { + + var range = Range.leftOpen(5L, 10L).map(it -> it * 10); + + assertThat(range.getLowerBound()).isEqualTo(Bound.exclusive(50L)); + assertThat(range.getUpperBound()).isEqualTo(Bound.inclusive(100L)); + + range = Range.leftOpen(5L, 10L).map(it -> null); + + assertThat(range.getLowerBound()).isEqualTo(Bound.unbounded()); + assertThat(range.getUpperBound()).isEqualTo(Bound.unbounded()); + } + @Test // DATACMNS-1499 void createsLeftUnboundedRange() { assertThat(Range.leftUnbounded(Bound.inclusive(10L)).contains(-10000L)).isTrue();