Generic implementation of the “Range” pattern. A Range has an upper and lower limit and can tell whether one element is in between. There also is a unit test available. An introduction to the “Range” pattern can be found at Martin FOWLER’s web site. To use this class as a LGPL’ed Maven dependency, use the following coordinates:
<dependency> <groupId>eu.headcrashing.treasure-chest</groupId> <artifactId>RangeClass</artifactId> <version>[1.2.2, 2)</version> </dependency>
/* * * This file is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * For a copy of the GNU Lesser General Public License * see <http://www.gnu.org/licenses/>. */ package eu.headcrashing.java.util; import static java.lang.Integer.MAX_VALUE; import static java.lang.Integer.MIN_VALUE; /** * A range is defined by a begin and an end. It allows checking whether a value * is within the range or outside. A range can be open at one or both ends, i. * e. the range is assumed to be endless in that direction. * * @author Markus KARG (markus@headcrashing.eu) * @version 1.2.1 */ public final class Range> { private final T lowerBound; private final T upperBound; /** * Creates a range with the specified bounds. The bounds will be included, * i. e. are part of the range. Bounds can be declared as being open, i. e. * the range is assumed to be endless in that direction. * * @param lowerBound * The lowest possible value of the range, or {@code null} if * there is no lower bound. * @param upperBound * The greatest possible value of the range, or {@code null} if * there is no upper bound. * @throws IllegalArgumentException * if lower bound is greater than upper bound */ public Range(final T lowerBound, final T upperBound) throws IllegalArgumentException { if (lowerBound != null && upperBound != null && lowerBound.compareTo(upperBound) > 0) throw new IllegalArgumentException("lowerBound is greater than upperBound"); this.lowerBound = lowerBound; this.upperBound = upperBound; } /** * Checks whether the specified object is within the range, including * bounds. * * @param object * The object to be checked. Must not be {@code null}. * @return {@code false} if {@code object} is lower than the lower bound or * greater than the upper bound; otherwise {@code true}. * @throws IllegalArgumentException * if {@code object} is {@code null}. */ public final boolean contains(final T object) throws IllegalArgumentException { if (object == null) throw new IllegalArgumentException("object is null"); if (this.lowerBound != null && object.compareTo(this.lowerBound) < 0) return false; if (this.upperBound != null && object.compareTo(this.upperBound) > 0) return false; return true; } /** * Checks whether the specified range is entirely contained in the range, * including bounds. * * @param range * The range to be checked. Must not be {@code null}. * @return {@code false} if {@code range} has a lower bound lower than the * lower bound of this or an upper bound greater than the upper * bound of this (i. e. {@code other} overlaps or is completely * outside); otherwise {@code true}. * @throws IllegalArgumentException * if {@code other} is {@code null}. * @since 1.1.0 */ public final boolean contains(final Range range) throws IllegalArgumentException { if (range == null) throw new IllegalArgumentException("range is null"); if (this.lowerBound != null && (range.lowerBound == null || range.lowerBound.compareTo(this.lowerBound) < 0)) return false; if (this.upperBound != null && (range.upperBound == null || range.upperBound.compareTo(this.upperBound) > 0)) return false; return true; } /** * Checks whether the specified range overlaps this range (i. e. whether the * ranges intersect). * * @param range * The {@code range} to be checked. Must not be {@code null}. * @return {@code false} if {@code range} has an upper bound lower than the * lower bound of this or a lower bound greater than the upper bound * of this; otherwise {@code true}. * @throws IllegalArgumentException * if {@code range} is {@code null}. * @since 1.2.0 */ public final boolean overlaps(final Range range) throws IllegalArgumentException { if (range == null) throw new IllegalArgumentException("range is null"); if (this.upperBound != null && range.lowerBound != null && this.upperBound.compareTo(range.lowerBound) < 0) return false; if (this.lowerBound != null && range.upperBound != null && this.lowerBound.compareTo(range.upperBound) > 0) return false; return true; } @Override public final int hashCode() { // MAX_value is used for open LOWER bound as it will be most far away // from any existing LOWER bound. final int lowerBoundHashCode = this.lowerBound == null ? MAX_VALUE : this.lowerBound.hashCode(); // MIN_value is used for open UPPER bound as it will be most far away // from any existing UPPER bound. final int upperBoundHashCode = this.upperBound == null ? MIN_VALUE : this.upperBound.hashCode(); return lowerBoundHashCode << 16 ^ upperBoundHashCode; } @Override public final boolean equals(final Object other) { if (!(other instanceof Range)) return false; final Range<?> that = (Range<?>) other; return equals(this.lowerBound, that.lowerBound) && equals(this.upperBound, that.upperBound); } private static final boolean equals(final Object a, final Object b) { if (a == null && b == null) return true; if (a != null && b != null) return a.equals(b); return false; } @Override public final String toString() { return String.format("Range[%s, %s]", this.lowerBound, this.upperBound); } }
/* * Copyright 2010 Markus KARG * * This file is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * For a copy of the GNU Lesser General Public License * see <http://www.gnu.org/licenses/>. */ package eu.headcrashing.java.util; import static java.lang.Integer.MAX_VALUE; import static java.lang.Integer.MIN_VALUE; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import org.junit.Test; /** * Unit test for {@code Range} class. * * @author Markus KARG (markus@headcrashing.eu) */ public final class RangeTest { @Test(expected = IllegalArgumentException.class) public final void constructorWithInterchangedBounds() { new Range(2, 1); } @Test public final void _equals() { assertThat("(null, null).equals(123)", new Range(null, null).equals(123), is(false)); assertThat("(null, null).equals(null, null)", new Range(null, null).equals(new Range(null, null)), is(true)); assertThat("(1, 2).equals(1, 2)", new Range(1, 2).equals(new Range(1, 2)), is(true)); assertThat("(null, 2).equals(null, 2)", new Range(null, 2).equals(new Range(null, 2)), is(true)); assertThat("(1, null).equals(1, null)", new Range(1, null).equals(new Range(1, null)), is(true)); assertThat("(1, 2).equals(1, 3)", new Range(1, 2).equals(new Range(1, 3)), is(false)); assertThat("(1, 2).equals(0, 2)", new Range(1, 2).equals(new Range(0, 2)), is(false)); assertThat("(1, 2).equals(null, 2)", new Range(1, 2).equals(new Range(null, 2)), is(false)); assertThat("(1, 2).equals(1, null)", new Range(1, 2).equals(new Range(1, null)), is(false)); assertThat("(1, 2).equals(null, null)", new Range(1, 2).equals(new Range(null, null)), is(false)); assertThat("(null, 2).equals(1, 2)", new Range(null, 2).equals(new Range(1, 2)), is(false)); assertThat("(1, null).equals(1, 2)", new Range(1, null).equals(new Range(1, 2)), is(false)); assertThat("(null, null).equals(1, 2)", new Range(null, null).equals(new Range(1, 2)), is(false)); } @Test public final void _toString() { assertThat("(A, B).toString()", new Range("A", "B").toString(), is("Range[A, B]")); } @Test public final void contains() { assertThat("(1, 3).contains(0)", new Range(1, 3).contains(0), is(false)); assertThat("(1, 3).contains(1)", new Range(1, 3).contains(1), is(true)); assertThat("(1, 3).contains(2)", new Range(1, 3).contains(2), is(true)); assertThat("(1, 3).contains(3)", new Range(1, 3).contains(3), is(true)); assertThat("(1, 3).contains(4)", new Range(1, 3).contains(4), is(false)); assertThat("(null, 3).contains(2)", new Range(null, 3).contains(2), is(true)); assertThat("(1, null).contains(2)", new Range(1, null).contains(2), is(true)); assertThat("(null, null).contains(2)", new Range(null, null).contains(2), is(true)); } @Test(expected = IllegalArgumentException.class) public final void containsNull() { new Range(1, 3).contains((Integer) null); } @Test public final void containsRange() { final Range boundedRange = new Range(0, 10); assertThat("Bounded range contains smaller bounded range", boundedRange.contains(new Range(2, 3)), is(true)); assertThat("Bounded range contains equal bounded range", boundedRange.contains(new Range(0, 10)), is(true)); assertThat("Bounded range contains smaller bounded range aligned left", boundedRange.contains(new Range(0, 8)), is(true)); assertThat("Bounded range contains smaller bounded range aligned right", boundedRange.contains(new Range(2, 10)), is(true)); assertThat("Bounded range contains larger bounded range", boundedRange.contains(new Range(5, 11)), is(false)); assertThat("Bounded range contains smaller bounded range overlapping left", boundedRange.contains(new Range(-1, 4)), is(false)); assertThat("Bounded range contains left open range located within", boundedRange.contains(new Range(null, 4)), is(false)); assertThat("Bounded range contains left open range located outside", boundedRange.contains(new Range(null, 11)), is(false)); assertThat("Bounded range contains right open range located within", boundedRange.contains(new Range(2, null)), is(false)); assertThat("Bounded range contains right open range located outside", boundedRange.contains(new Range(-1, null)), is(false)); assertThat("Bounded range contains unbounded range", boundedRange.contains(new Range(null, null)), is(false)); final Range leftOpenRange = new Range(null, 10); assertThat("Left open range contains smaller bounded range", leftOpenRange.contains(new Range(2, 3)), is(true)); assertThat("Left open range contains left open range located within", leftOpenRange.contains(new Range(null, 3)), is(true)); assertThat("Left open range contains left open range located outside", leftOpenRange.contains(new Range(null, 11)), is(false)); assertThat("Left open range contains right overlapping range", leftOpenRange.contains(new Range(2, 11)), is(false)); assertThat("Left open range contains right open range located inside", leftOpenRange.contains(new Range(2, null)), is(false)); assertThat("Left open range contains unbounded range", leftOpenRange.contains(new Range(null, null)), is(false)); final Range rightOpenRange = new Range(0, null); assertThat("Right open range contains smaller bounded ranged", rightOpenRange.contains(new Range(2, 3)), is(true)); assertThat("Right open range contains right open range located within", rightOpenRange.contains(new Range(2, null)), is(true)); assertThat("Right open range contains right open range located outside", rightOpenRange.contains(new Range(-1, null)), is(false)); assertThat("Right open range contains smaller bounded range overlapping left", rightOpenRange.contains(new Range(-1, 8)), is(false)); assertThat("Right open range contains left open range located inside", rightOpenRange.contains(new Range(null, 8)), is(false)); assertThat("Right open range contains unbounded range", rightOpenRange.contains(new Range(null, null)), is(false)); final Range unbounded = new Range(null, null); assertThat("Unbounded range contains bounded range", unbounded.contains(new Range(2, 3)), is(true)); assertThat("Unbounded range contains right open range", unbounded.contains(new Range(2, null)), is(true)); assertThat("Unbounded range contains left open range", unbounded.contains(new Range(null, 3)), is(true)); assertThat("Unbounded range contains unbounded range", unbounded.contains(new Range(null, null)), is(true)); } @Test public final void overlaps() { final Range boundedRange = new Range(0, 10); assertThat("Bounded range overlaps smaller bounded range", boundedRange.overlaps(new Range(2, 3)), is(true)); assertThat("Bounded range overlaps smaller bounded range overlapping right", boundedRange.overlaps(new Range(9, 11)), is(true)); assertThat("Bounded range overlaps smaller bounded range overlapping left", boundedRange.overlaps(new Range(-1, 1)), is(true)); assertThat("Bounded range overlaps left open range located within", boundedRange.overlaps(new Range(null, 5)), is(true)); assertThat("Bounded range overlaps left open range located outside", boundedRange.overlaps(new Range(null, -4)), is(false)); assertThat("Bounded range overlaps right open range located within", boundedRange.overlaps(new Range(5, null)), is(true)); assertThat("Bounded range overlaps right open range located outside", boundedRange.overlaps(new Range(12, null)), is(false)); assertThat("Bounded range overlaps unbounded range", boundedRange.overlaps(new Range(null, null)), is(true)); assertThat("Bounded range overlaps bounded range located outside left", boundedRange.overlaps(new Range(-5, -1)), is(false)); assertThat("Bounded range overlaps bounded range located outside right", boundedRange.overlaps(new Range(11, 14)), is(false)); final Range leftOpenRange = new Range(null, 10); assertThat("Left open range overlaps bounded range located within", leftOpenRange.overlaps(new Range(2, 3)), is(true)); assertThat("Left open range overlaps bounded range located outside", leftOpenRange.overlaps(new Range(11, 15)), is(false)); assertThat("Left open range overlaps bounded range overlapping right", leftOpenRange.overlaps(new Range(9, 11)), is(true)); assertThat("Left open range overlaps left open range located outside right", leftOpenRange.overlaps(new Range(null, 11)), is(true)); assertThat("Left open range overlaps left open range located within", leftOpenRange.overlaps(new Range(null, 3)), is(true)); assertThat("Left open range overlaps right open range located outside right", leftOpenRange.overlaps(new Range(11, null)), is(false)); assertThat("Left open range overlaps unbound range", leftOpenRange.overlaps(new Range(null, null)), is(true)); final Range rightOpenRange = new Range(0, null); assertThat("Right open range overlaps bounded range located within", rightOpenRange.overlaps(new Range(2, 3)), is(true)); assertThat("Right open range overlaps bounded range located outside", rightOpenRange.overlaps(new Range(-4, -1)), is(false)); assertThat("Right open range overlaps bounded range overlapping left", rightOpenRange.overlaps(new Range(-1, 8)), is(true)); assertThat("Right open range overlaps left open range located outside left", rightOpenRange.overlaps(new Range(null, -1)), is(false)); assertThat("Right open range overlaps right open range within", rightOpenRange.overlaps(new Range(2, null)), is(true)); assertThat("Right open range overlaps right open range located outside left", rightOpenRange.overlaps(new Range(-1, null)), is(true)); assertThat("Right open range overlaps unbound range", rightOpenRange.overlaps(new Range(null, null)), is(true)); final Range unboundedRange = new Range(null, null); assertThat("Unbounded range overlaps bounded range", unboundedRange.overlaps(new Range(2, 3)), is(true)); assertThat("Unbounded range overlaps left open range", unboundedRange.overlaps(new Range(null, 3)), is(true)); assertThat("Unbounded range overlaps right open range", unboundedRange.overlaps(new Range(2, null)), is(true)); assertThat("Unbounded range overlaps unbounded range", unboundedRange.overlaps(new Range(null, null)), is(true)); } @Test public final void _hashCode() { final Integer hashCodeOfMaxValue = Integer.valueOf(MAX_VALUE).hashCode(); final Integer hashCodeOfMinValue = Integer.valueOf(MIN_VALUE).hashCode(); final Integer hashCodeOf7 = Integer.valueOf(7).hashCode(); final Integer hashCodeOf11 = Integer.valueOf(11).hashCode(); assertThat("(null, null).hashCode()", new Range(null, null).hashCode(), is(hashCodeOfMaxValue << 16 ^ hashCodeOfMinValue)); assertThat("(7, null).hashCode()", new Range(7, null).hashCode(), is(hashCodeOf7 << 16 ^ hashCodeOfMinValue)); assertThat("(null, 11).hashCode()", new Range(null, 11).hashCode(), is(hashCodeOfMaxValue << 16 ^ hashCodeOf11)); assertThat("(7, 11).hashCode()", new Range(7, 11).hashCode(), is(hashCodeOf7 << 16 ^ hashCodeOf11)); } /** * Regression test for {@code Range.compare(String)} false * positives. * * Problem: {@code Range("c", "f")} pretends to contain both, * {@code "a"} and {@code "g"}. * * Cause: Incorrect assumption of {@code Comparable.compareTo(T)} result * values being exactly {@code -1} or {@code 1}, but actually can be * {@code -N} and {@code N}. */ @Test public final void compareStringRegression() { assertThat("(\"c\", \"e\").contains(\"a\")", new Range("c", "e").contains("a"), is(false)); assertThat("(\"c\", \"e\").contains(\"g\")", new Range("c", "e").contains("g"), is(false)); } /** * Regression test for {@code Range(String, String)} false negatives * in interchanged limits test. * * Problem: {@code Range("c", "a")} pretends to be a valid range. * * Cause: Incorrect assumption of {@code Comparable.compareTo(T)} result * values being exactly {@code -1} or {@code 1}, but actually can be * {@code -N} and {@code N}. */ @Test(expected = IllegalArgumentException.class) public final void interchangedLimitsStringRegression() { new Range("c", "a"); } }