Interval and GenericInterval

An introduction to the uk.org.bobulous.java.intervals package.

What is uk.org.bobulous.java.intervals?

A few times whilst writing Java code I found it frustrating that Java offers no core class to represent the concept of a mathematical interval. So I created the uk.org.bobulous.java.intervals package with the aim of providing interfaces and classes which offer support for intervals. The source code itself is full of detailed Javadoc comments, so take a look at that for the full specification. This page will act as an introduction to the package with a few code examples.

What is an interval in mathematics?

In mathematics an interval is a subset of an ordered set, defined by a lower endpoint and an upper endpoint. The interval includes every value of the ordered set which is considered greater than the lower endpoint value and less than the upper endpoint value. If the endpoint value itself is included in the interval than the endpoint is said to be closed; if the endpoint value itself is not included then the endpoint is said to be open.

For example, the interval [1, 5] in the set of integers has a lower endpoint of one and an upper endpoint of five and both endpoints are closed (indicated by using a square bracket). So the interval includes the integers 1, 2, 3, 4 and 5 and nothing else.

Another example, the interval (0.0, 10.0] in the set of real numbers has a lower endpoint of 0.0 and an upper endpoint of 10.0 and the lower endpoint is open (indicated using a round bracket) but the upper endpoint is closed. So this interval includes all real numbers which are greater than zero and also less-than-or-equal-to ten. Zero is not included in the interval, even though the lower endpoint is zero, because the lower endpoint mode is open.

Empty and degenerate intervals

It is possible for an interval to include no values whatsoever, so that it represents the empty set. For example, the intervals (0.0, 0.0) and (3.0, 3.0] and [8.0, 8.0) all represent the empty set because in each case there is no value which is permitted by both endpoints. It is also possible for a closed interval to contain only a single value, for example the integer interval [1, 1], and this is known as a degenerate interval.

Bounded and unbounded intervals

An interval can be bounded (where both endpoints are finite, as in the examples so far), unbounded (where both endpoints are infinite and permit all values from the ordered set), left-bounded (where only the lower endpoint is finite), or right-bounded (where only the upper endpoint is finite). If an endpoint is infinite then its mode (open/closed) is irrelevant as it permits any value.

For example, the integer interval (-∞, +∞) has a lower endpoint of negative infinity and an upper endpoint of positive infinity, and so includes every integer. And the real number interval (0.0, +∞) includes every real number which is greater than zero. Note that infinity is not actually a number (nor a member of any ordered set) but a statement which says "keep going forever and never stop" and in terms of endpoints it means "include everything in this direction without limit".

Representing an interval in Java using Interval

The package uk.org.bobulous.java.intervals currently contains the interface Interval, the concrete implementation GenericInterval, and the support class IntervalComparator.

Interval

The interface Interval<T extends Comparable<T>> defines a type which represents an interval through the type T. Note that the base type T must be a type which implements Comparable<T>. In other words, the base type T must be a type whose objects can be compared with other objects of the same type, giving the type a natural ordering. For example, a few types which fit this requirement include Integer, Double, String, and BigDecimal.

The interface defines the public static enum EndpointMode type which must be used to specify whether an interval endpoint is OPEN or CLOSED. It also declares the methods:

  1. getLowerEndpoint() which must return the interval's lower endpoint value of type T, or null if the interval is not left-bounded.
  2. getLowerEndpointMode() which must return the mode of the lower endpoint, either Interval.EndpointMode.OPEN or Interval.EndpointMode.CLOSED.
  3. getUpperEndpoint() which must return the interval's upper endpoint value of type T, or null if the interval is not right-bounded.
  4. getUpperEndpointMode() which must return the mode of the upper endpoint, either Interval.EndpointMode.OPEN or Interval.EndpointMode.CLOSED.
  5. includes(T value) which must return true if the specified value (which must be of the same base type, T, as this interval) is included by this interval; false if the value is not included by this interval.
  6. includes(Interval<T> interval) which must return true if the specified interval (which must have the same base type, T, as this interval) is wholly included by this interval; false otherwise. In other words, must return true only if every value included by the specified interval is also included by this interval.

GenericInterval

The class Interval<T extends Comparable<T>> is a concrete implementation of the interface Interval<T extends Comparable<T>>. It also permits the use of any base type which implements Comparable<T>, so can be used with any type which has a natural ordering.

The class provides two constructors:

  1. GenericInterval(T lowerEndpoint, T upperEndpoint) which creates a closed interval using the supplied values of type T.
  2. GenericInterval(EndpointMode lowerEndpointMode, T lowerEndpoint, T upperEndpoint, EndpointMode upperEndpointMode) which can be used to create an open, left-open, right-open or closed interval having the supplied endpoint values of type T.

After implementing the methods required by the Interval interface, GenericInterval also overrides the equals, hashCode and toString methods, and provides a method called inMathematicalNotation which returns a String which represents the interval using the mathematical square/round bracket notation.

Note that the endpoint values and modes of a GenericInterval cannot be modified once an instance is created. This means that an instance of GenericInterval is immutable if its base type is a truly immutable type such as Integer, Double, String and so on. A GenericInterval of an immutable type can be shared safely. But if the base type is a mutable type, such as Date or Calendar then the instance of GenericInterval cannot be considered immutable because even though the endpoints are permanently fixed to pointing to the original objects, the value of those mutable objects might change, which will alter the interval represented by the GenericInterval instance. If you create a GenericInterval instance on a mutable base type then you must carefully guard that instance, and the objects used as its endpoints, otherwise the interval could be modified when you don't expect it to be.

IntervalComparator

The class IntervalComparator<T extends Comparable<T>> defines a Comparator<Interval<T>> which offers one method for comparing intervals of the naturally ordered base type T. This does not have any basis in mathematics so far as I'm aware, but it does provide a way of ordering intervals and is used by several methods of the GenericInterval class. See the JavaDoc within the IntervalComparator class for full details of the logic it uses to compare intervals.

Creating intervals in Java

Now you've had an overview of the classes which are found within the uk.org.bobulous.java.intervals package, let's take a look at a few examples.

A closed interval of integers

Interval<Integer> oneToFive = new GenericInterval<>(1, 5);
boolean included;
included = oneToFive.includes(0); // evaluates to false
included = oneToFive.includes(1); // evaluates to true
included = oneToFive.includes(3); // evaluates to true
included = oneToFive.includes(5); // evaluates to true
included = oneToFive.includes(6); // evaluates to false

This example creates a GenericInterval<Integer> to represent the closed integer interval [1, 5] described in an earlier section of this page. Then the includes method is called to determine whether different integer values are included by the interval. Note that, because the interval is closed, the endpoint values of one and five are both included by the interval.

A left-open interval of double values

Interval<Double> zeroToTen = new GenericInterval<>(
        EndpointMode.OPEN, 0.0, 10.0, EndpointMode.CLOSED);
boolean included;
included = zeroToTen.includes(0.0); // evaluates to false
included = zeroToTen.includes(0.1); // evaluates to true
included = zeroToTen.includes(6.3); // evaluates to true
included = zeroToTen.includes(10.0); // evaluates to true
included = zeroToTen.includes(10.1); // evaluates to false

This example creates a GenericInterval<Double> to represent the left-open real number interval (0, 10] described in an earlier section of this page. This is done by explicitly stating the endpoint mode of the lower and upper endpoints, setting the lower endpoint mode to OPEN and the upper endpoint mode to CLOSED. Then the includes method is called to determine whether different double values are included by the interval. Note that, because the interval's lower endpoint mode is open, the lower endpoint value of zero is not included by the interval.

A right-open interval of String values

Interval<String> alphaToBravo = new GenericInterval<>(
        EndpointMode.CLOSED, "a", "b", EndpointMode.OPEN);
boolean included;
included = alphaToBravo.includes("A"); // evaluates to false
included = alphaToBravo.includes("a"); // evaluates to true
included = alphaToBravo.includes("aardvark"); // evaluates to true
included = alphaToBravo.includes("ale"); // evaluates to true
included = alphaToBravo.includes("axe"); // evaluates to true
included = alphaToBravo.includes("b"); // evaluates to false
included = alphaToBravo.includes("ball"); // evaluates to false

In this example we consider the right-open interval ["a", "b") of String values. Any Java String which, according to the natural order of String (as defined by its compareTo(String) method), is equal-to-or-greater-than "a" and also less-than "b" is included in this interval. So the String "a" is included in this interval, as are "aardvark", "ale", "axe", etc, but "b" is not included, nor are "ball", "car", "dinosaur", "8-ball", " " (space), "" (empty String) etc. It's also important to note that neither "A" nor "Academy" are included by this interval, because uppercase "A" is not equal to lowercase "a" according to the compareTo(String) method of String.

Checking whether one interval includes another

Interval<Double> allDoubles = new GenericInterval<>(null, null);
Interval<Double> nonNegativeDoubles = new GenericInterval<>(0.0, null);
Interval<Double> positiveDoubles = new GenericInterval<>(
        EndpointMode.OPEN, 0.0, null, EndpointMode.OPEN);
boolean included;
included = allDoubles.includes(nonNegativeDoubles); // evaluates to true
included = allDoubles.includes(positiveDoubles); // evaluates to true
included = nonNegativeDoubles.includes(allDoubles); // evaluates to false
included = positiveDoubles.includes(allDoubles); // evaluates to false
included = nonNegativeDoubles.includes(positiveDoubles); // evaluates to true
included = positiveDoubles.includes(nonNegativeDoubles); // evaluates to false

In this example the includes method is being called to determine whether one interval wholly includes another. That is, whether the first interval includes every value permitted by the second interval. The allDoubles interval is completely unbounded: its null lower endpoint and null upper endpoint are equivalent to negative and positive infinity respectively, which means that this interval includes every possible Double value. So this interval also includes every possible Interval<Double>, which means that the includes method always returns true when called on allDoubles.

Note that the an Interval never actually includes null, even if one or both of its endpoints are null. This is because null represents the lack of any value, which makes sense for an endpoint because the lack of a value is interpreted as that endpoint being unbounded, infinite. But it does not make sense to check whether an interval contains a non-value, so this is not permitted. In fact, if you call either includes method of GenericInterval with a null argument it will throw a NullPointerException.

The only difference between the nonNegativeDoubles and positiveDoubles intervals is that the interval nonNegativeDoubles includes the value 0.0. This means that the interval nonNegativeDoubles wholly includes the interval positiveDoubles, but the interval positiveDoubles does not include the interval nonNegativeDoubles. And neither of these intervals includes the all-encompassing interval allDoubles.

Download uk.org.bobulous.java.intervals

You can fetch the source code from the JavaIntervals repository on Codeberg. The source code is made available under the Mozilla Public Licence 2.0.

I have harnessed a few dozen unit tests to these classes to hunt for any bugs or errors, but please be sure to write your own unit tests to confirm that the package behaves as you expect before you put it to serious use. (For this reason, the unit tests are not included as part of the package.) If you find any issues, please raise them on the Codeberg issues page.