Interval
and GenericInterval
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.
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".
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:
getLowerEndpoint()
which must return the interval's lower endpoint value of type T
, or null
if the interval is not left-bounded.getLowerEndpointMode()
which must return the mode of the lower endpoint, either Interval .EndpointMode .OPEN
or Interval .EndpointMode .CLOSED
.getUpperEndpoint()
which must return the interval's upper endpoint value of type T
, or null
if the interval is not right-bounded.getUpperEndpointMode()
which must return the mode of the upper endpoint, either Interval .EndpointMode .OPEN
or Interval .EndpointMode .CLOSED
.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.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.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:
GenericInterval (T lowerEndpoint, T upperEndpoint)
which creates a closed interval using the supplied values of type T
.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.
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.
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 String
.
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.
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.