From 0d43333430ab8b543f1d7c563bbedc1ed3b507ab Mon Sep 17 00:00:00 2001 From: Edgar Asatryan Date: Sun, 2 Oct 2022 17:31:32 +0400 Subject: [PATCH] Empty Range now actually empty not infinity. Closes: gh-492 --- .../hibernate/type/range/Range.java | 87 ++++++++++++++++--- .../hibernate/type/range/RangeTest.java | 58 +++++++++++++ 2 files changed, 134 insertions(+), 11 deletions(-) diff --git a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java index 976b8a3e7..07958cc3b 100644 --- a/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java +++ b/hibernate-types-55/src/main/java/com/vladmihalcea/hibernate/type/range/Range.java @@ -1,5 +1,7 @@ package com.vladmihalcea.hibernate.type.range; +import static java.util.stream.Collectors.toList; + import java.io.Serializable; import java.math.BigDecimal; import java.time.*; @@ -7,8 +9,10 @@ import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; +import java.util.List; import java.util.Objects; import java.util.function.Function; +import java.util.stream.IntStream; /** * Represents the range/interval with two bounds. Abstraction follows the semantics of the mathematical interval. The @@ -68,7 +72,7 @@ private Range(T lower, T upper, int mask, Class clazz) { this.mask = mask; this.clazz = clazz; - if (isBounded() && lower.compareTo(upper) > 0) { + if (isBounded() && lower != null && upper != null && lower.compareTo(upper) > 0) { throw new IllegalArgumentException("The lower bound is greater then upper!"); } } @@ -250,6 +254,28 @@ public static > Range infinite(Class cls) return new Range<>(null, null, LOWER_INFINITE | UPPER_INFINITE, cls); } + /** + * Creates the empty range. In other words the range that contains no points. + *

+ * The mathematical equivalent will be: + *

{@code
+     *     (a, a) = ∅
+     * }
+ * + * @param cls The range class, never null. + * @param The type of bounds. + * + * @return The empty range. + */ + public static > Range emptyRange(Class cls) { + return new Range<>( + null, + null, + LOWER_EXCLUSIVE | UPPER_EXCLUSIVE, + cls + ); + } + public static > Range ofString(String str, Function converter, Class clazz) { if(str.equals(EMPTY)) { return emptyRange(clazz); @@ -498,6 +524,7 @@ public String toString() { return "Range{" + "lower=" + lower + ", upper=" + upper + ", mask=" + mask + + ", maskNames=" + masks() + ", clazz=" + clazz + '}'; } @@ -561,6 +588,10 @@ public T upper() { * @return Whether {@code point} in this range or not. */ public boolean contains(T point) { + if (isEmpty()) { + return false; + } + boolean l = hasLowerBound(); boolean u = hasUpperBound(); @@ -596,7 +627,30 @@ public boolean contains(T point) { * @return Whether {@code range} in this range or not. */ public boolean contains(Range range) { - return (!range.hasLowerBound() || contains(range.lower)) && (!range.hasUpperBound() || contains(range.upper)); + return !isEmpty() && (!range.hasLowerBound() || contains(range.lower)) && (!range.hasUpperBound() || contains(range.upper)); + } + + /** + * Determines whether this range is empty or not. + *

+ * For example: + *

{@code
+     *     assertFalse(integerRange("empty").contains(1))
+     * }
+ * + * @return Whether {@code range} in this range or not. + */ + public boolean isEmpty() { + boolean boundedExclusive = isBounded() + && hasMask(LOWER_EXCLUSIVE) + && hasMask(UPPER_EXCLUSIVE); + + return boundedExclusive && hasEqualBounds(); + } + + private boolean hasEqualBounds() { + return lower == null && upper == null + || lower != null && upper != null && lower.compareTo(upper) == 0; } public String asString() { @@ -621,16 +675,27 @@ private Function boundToString() { }; } - Class getClazz() { - return clazz; + private List masks() { + return IntStream.of(LOWER_INCLUSIVE, LOWER_EXCLUSIVE, UPPER_INCLUSIVE, UPPER_EXCLUSIVE, LOWER_INFINITE, + UPPER_INFINITE) + .filter(this::hasMask) + .mapToObj(this::maskName) + .collect(toList()); } - public static > Range emptyRange(Class clazz) { - return new Range<>( - null, - null, - LOWER_INFINITE|UPPER_INFINITE, - clazz - ); + private String maskName(int mask) { + switch (mask) { + case LOWER_INCLUSIVE: return "LOWER_INCLUSIVE"; + case LOWER_EXCLUSIVE: return "LOWER_EXCLUSIVE"; + case UPPER_INCLUSIVE: return "UPPER_INCLUSIVE"; + case UPPER_EXCLUSIVE: return "UPPER_EXCLUSIVE"; + case LOWER_INFINITE: return "LOWER_INFINITE"; + case UPPER_INFINITE: return "UPPER_INFINITE"; + default: return "UNKNOWN"; + } + } + + Class getClazz() { + return clazz; } } diff --git a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java index a9441b687..8029de669 100644 --- a/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java +++ b/hibernate-types-55/src/test/java/com/vladmihalcea/hibernate/type/range/RangeTest.java @@ -5,8 +5,15 @@ import static com.vladmihalcea.hibernate.type.range.Range.integerRange; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; /** * @author Edgar Asatryan @@ -36,6 +43,9 @@ public void ofStringTest() { assertThat(integerRange("(-5,5]").isUpperBoundClosed(), is(true)); assertThat(integerRange("(-5,5]").isLowerBoundClosed(), is(false)); + assertThat(integerRange("(,)").contains(integerRange("empty")), is(true)); + + assertThat(integerRange("empty").contains(integerRange("(,)")), is(false)); } @Test @@ -74,4 +84,52 @@ public void zonedDateTimeTest() { assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.123456+05:30,)")); assertNotNull(Range.zonedDateTimeRange("[2019-03-27 16:33:10.123456-06,infinity)")); } + + + @Test + public void emptyInfinityEquality() { + assertEquals(integerRange("empty"), integerRange("empty")); + assertEquals(integerRange("(infinity,infinity)"), integerRange("(infinity,infinity)")); + assertEquals(integerRange("(,)"), integerRange("(infinity,infinity)")); + assertEquals(integerRange("(infinity,infinity)"), integerRange("(,)")); + + assertNotEquals(integerRange("empty"), integerRange("(infinity,infinity)")); + assertNotEquals(integerRange("empty"), integerRange("(,)")); + assertNotEquals(integerRange("empty"), integerRange("(5,5)")); + } + + @Test + public void emptyRangeWithEmptyKeyword() { + Range empty = Range.localDateRange("empty"); + + assertTrue(empty.isEmpty()); + + assertFalse(empty.contains(LocalDate.MIN)); + assertFalse(empty.contains((LocalDate) null)); + assertFalse(empty.contains(LocalDate.now())); + assertFalse(empty.contains(LocalDate.MAX)); + + assertNull(empty.upper()); + assertNull(empty.lower()); + } + + @Test + public void emptyRangeWithValues() { + Range empty = Range.localDateRange("(2019-03-27,2019-03-27)"); + + assertTrue(empty.isEmpty()); + assertFalse(empty.contains(LocalDate.MIN)); + assertFalse(empty.contains(LocalDate.now())); + assertFalse(empty.contains(LocalDate.MAX)); + + assertTrue(integerRange("(5,5)").isEmpty()); + } + + @Test + public void notEmptyWithValues() { + assertFalse(integerRange("(5,)").isEmpty()); + assertFalse(integerRange("(5,5]").isEmpty()); + assertFalse(integerRange("(,5)").isEmpty()); + assertFalse(integerRange("(,)").isEmpty()); + } } \ No newline at end of file