.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.
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.
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.
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".
uk.org.bobulous.java.intervals currently contains the interface
Interval, the concrete implementation
GenericInterval, and the support class
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
The interface defines the
public static enum EndpointMode type which must be used to specify whether an interval endpoint is
CLOSED. It also declares the methods:
getLowerEndpoint()which must return the interval's lower endpoint value of type
nullif the interval is not left-bounded.
getLowerEndpointMode()which must return the mode of the lower endpoint, either
getUpperEndpoint()which must return the interval's upper endpoint value of type
nullif the interval is not right-bounded.
getUpperEndpointMode()which must return the mode of the upper endpoint, either
includes(T value)which must return
trueif the specified value (which must be of the same base type,
T, as this interval) is included by this interval;
falseif the value is not included by this interval.
includes(Interval<T> interval)which must return
trueif the specified interval (which must have the same base type,
T, as this interval) is wholly included by this interval;
falseotherwise. In other words, must return
trueonly if every value included by the specified interval is also included by this interval.
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:
GenericIntervalwhich creates a closed interval using the supplied values of type
(T lowerEndpoint, T upperEndpoint)
GenericIntervalwhich can be used to create an open, left-open, right-open or closed interval having the supplied endpoint values of type
(EndpointMode lowerEndpointMode, T lowerEndpoint, T upperEndpoint, EndpointMode upperEndpointMode)
After implementing the methods required by the
GenericInterval also overrides the
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
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
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<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.
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.
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.
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.
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
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
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.
.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.