Range Class

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");
}
}
Advertisements

About Markus Karg

Java Guru with +30 years of experience in professional software development. I travelled the whole world of IT, starting from Sinclair's great ZX Spectrum 48K, Commodore's 4040, over S/370, PCs since legendary XT, CP/M, VM/ESA, DOS, Windows (remember 3.1?), OS/2 WARP, Linux to Android and iOS... and still coding is my passion, and Java is my favourite drug!
This entry was posted in Java, Programming and tagged , , , . Bookmark the permalink.