-
Notifications
You must be signed in to change notification settings - Fork 11k
RangesExplained
TODO: lots more examples
List<Double> scores;
Iterable<Double> belowMedianScores = Iterables.filter(scores, Range.lessThan(median));
...
Range<Integer> validGrades = Range.closed(1, 12);
for(int grade : ContiguousSet.create(validGrades, DiscreteDomain.integers())) {
...
}
A range, sometimes known as an interval, is a convex (informally, "contiguous" or "unbroken") portion of a particular domain. Formally, convexity means that for any a <= b <= c
, range.contains(a) && range.contains(c)
implies that range.contains(b)
.
Ranges may "extend to infinity" -- for example, the range "x > 3" contains arbitrarily large values -- or may be finitely constrained, for example "2 <= x < 5". We will use the more compact notation, familiar to programmers with a math background:
- (a..b) = {x | a < x < b}
- [a..b] = {x | a <= x <= b}
- [a..b) = {x | a <= x < b}
- (a..b] = {x | a < x <= b}
- (a..+∞) = {x | x > a}
- [a..+∞) = {x | x >= a}
- (-∞..b) = {x | x < b}
- (-∞..b] = {x | x <= b}
- (-∞..+∞) = all values
The values a and b used above are called endpoints. To improve consistency, Guava's notion of Range
requires that the upper endpoint may not be less than the lower endpoint. The endpoints may be equal only if at least one of the bounds is closed:
- [a..a] : singleton range
- [a..a); (a..a] : empty, but valid
- (a..a) : invalid
A range in Guava has the type Range<C>. All ranges are immutable.
Ranges can be obtained from the static methods on Range:
(a..b) | open(C, C) |
---|---|
[ a..b]
|
closed(C, C) |
[ a..b) |
closedOpen(C, C) |
(a..b]
|
openClosed(C, C) |
(a..+∞) | greaterThan(C) |
[ a..+∞) |
atLeast(C) |
(-∞..b) | lessThan(C) |
(-∞..b]
|
atMost(C) |
(-∞..+∞) | all() |
Range.closed("left", "right"); // all strings lexographically between "left" and "right" inclusive
Range.lessThan(4.0); // double values strictly less than 4
Additionally, Range instances can be constructed by passing the bound types explicitly:
Bounded on both ends | range(C, BoundType, C, BoundType) |
---|---|
Unbounded on top ((a..+∞) or [ a..+∞)) |
downTo(C, BoundType) |
Unbounded on bottom ((-∞..b) or (-∞..b]) | upTo(C, BoundType) |
Here, BoundType is an enum containing the values CLOSED
and OPEN
.
Range.downTo(4, boundType); // allows you to decide whether or not you want to include 4
Range.range(1, CLOSED, 4, OPEN); // another way of writing Range.closedOpen(1, 4)
The fundamental operation of a Range
is its contains(C) methods, which behaves exactly as you might expect. Additionally, a Range
may be used as a Predicate
, and used in functional idioms. Any Range
also supports containsAll(Iterable<? extends C>)
.
Range.closed(1, 3).contains(2); // returns true
Range.closed(1, 3).contains(4); // returns false
Range.lessThan(5).contains(5); // returns false
Range.closed(1, 4).containsAll(Ints.asList(1, 2, 3)); // returns true
To look at the endpoints of a range, Range
exposes the following methods:
- hasLowerBound() and hasUpperBound(), which check if the range has the specified endpoints, or goes on "through infinity."
-
lowerBoundType() and upperBoundType() return the
BoundType
for the corresponding endpoint, which can be eitherCLOSED
orOPEN
. If this range does not have the specified endpoint, the method throws anIllegalStateException
. -
lowerEndpoint() and upperEndpoint() return the endpoints on the specified end, or throw an
IllegalStateException
if the range does not have the specified endpoint. -
isEmpty() tests if the range is empty, that is, it has the form
[
a,a) or (a,a]
.
Range.closedOpen(4, 4).isEmpty(); // returns true
Range.openClosed(4, 4).isEmpty(); // returns true
Range.closed(4, 4).isEmpty(); // returns false
Range.open(4, 4).isEmpty(); // Range.open throws IllegalArgumentException
Range.closed(3, 10).lowerEndpoint(); // returns 3
Range.open(3, 10).lowerEndpoint(); // returns 3
Range.closed(3, 10).lowerBoundType(); // returns CLOSED
Range.open(3, 10).upperBoundType(); // returns OPEN
The most basic relation on ranges is encloses(Range)
, which is true if the bounds of the inner range do not extend outside the bounds of the outer range. This is solely dependent on comparisons between the endpoints!
- [3..6] encloses [4..5]
- (3..6) encloses (3..6)
- [3..6] encloses [4..4) (even though the latter is empty)
- (3..6] does not enclose [3..6]
- [4..5] does not enclose (3..6) even though it contains every value contained by the latter range, although use of discrete domains can address this (see below)
- [3..6] does not enclose (1..1] even though it contains every value contained by the latter range
encloses
is a partial ordering.
Given this, Range
provides the following operations:
Range.isConnected(Range)
, which tests if these ranges are connected. Specifically, isConnected
tests if there is some range enclosed by both of these ranges, but this is equivalent to the mathematical definition that the union of the ranges must form a connected set (except in the special case of empty ranges).
isConnected
is a reflexive, symmetric relation.
Range.closed(3, 5).isConnected(Range.open(5, 10)); // returns true
Range.closed(0, 9).isConnected(Range.closed(3, 4)); // returns true
Range.closed(0, 5).isConnected(Range.closed(3, 9)); // returns true
Range.open(3, 5).isConnected(Range.open(5, 10)); // returns false
Range.closed(1, 5).isConnected(Range.closed(6, 10)); // returns false
Range.intersection(Range)
returns the maximal range enclosed by both this range and other (which exists iff these ranges are connected), or if no such range exists, throws an IllegalArgumentException
.
intersection
is a commutative, associative operation.
Range.closed(3, 5).intersection(Range.open(5, 10)); // returns (5, 5]
Range.closed(0, 9).intersection(Range.closed(3, 4)); // returns [3, 4]
Range.closed(0, 5).intersection(Range.closed(3, 9)); // returns [3, 5]
Range.open(3, 5).intersection(Range.open(5, 10)); // throws IAE
Range.closed(1, 5).intersection(Range.closed(6, 10)); // throws IAE
Range.span(Range)
returns the minimal range that encloses both this range and other. If the ranges are both connected, this is their union.
span
is a commutative, associative, and closed operation.
Range.closed(3, 5).span(Range.open(5, 10)); // returns [3, 10)
Range.closed(0, 9).span(Range.closed(3, 4)); // returns [0, 9]
Range.closed(0, 5).span(Range.closed(3, 9)); // returns [0, 9]
Range.open(3, 5).span(Range.open(5, 10)); // returns (3, 10)
Range.closed(1, 5).span(Range.closed(6, 10)); // returns [1, 10]
Some types, but not all Comparable types, are discrete, meaning that ranges bounded on both sides can be enumerated.
In Guava, a DiscreteDomain<C> implements discrete operations for type C
. A discrete domain always represents the entire set of values of its type; it cannot represent partial domains such as "prime integers", "strings of length 5," or "timestamps at midnight."
The DiscreteDomain class provides DiscreteDomain
instances:
Type | DiscreteDomain |
---|---|
Integer |
integers() |
Long |
longs() |
Once you have a DiscreteDomain
, you can use the following Range
operations:
-
ContiguousSet.create(range, domain)
: view aRange<C>
as anImmutableSortedSet<C>
, with a few extra operations thrown in. (Does not work for unbounded ranges, unless the type itself is bounded.) -
canonical(domain)
: put ranges in a "canonical form." IfContiguousSet.create(a, domain).equals(ContiguousSet.create(b, domain))
and!a.isEmpty()
, thena.canonical(domain).equals(b.canonical(domain))
. (This does not, however, implya.equals(b)
.)
ImmutableSortedSet<Integer> set = ContiguousSet.create(Range.open(1, 5), DiscreteDomain.integers());
// set contains [2, 3, 4]
ContiguousSet.create(Range.greaterThan(0), DiscreteDomain.integers());
// set contains [1, 2, ..., Integer.MAX_VALUE]
Note that ContiguousSet.create
does not actually construct the entire range, but instead returns a view of the range as a set.
You can make your own DiscreteDomain
objects, but there are several important aspects of the DiscreteDomain
contract that you must remember.
- A discrete domain always represents the entire set of values of its type; it cannot represent partial domains such as "prime integers" or "strings of length 5." So you cannot, for example, construct a
DiscreteDomain
to view a set of days in a range, with a JODADateTime
that includes times up to the second: because this would not contain all elements of the type. - A
DiscreteDomain
may be infinite -- aBigInteger
DiscreteDomain
, for example. In this case, you should use the default implementation ofminValue()
andmaxValue()
, which throw aNoSuchElementException
. This forbids you from using theContiguousSet.create
method on an infinite range, however!
We wanted to strike a very specific balance in Range
between power and API complexity, and part of that involved not providing a Comparator
-based interface: we don't need to worry about how ranges based on different comparators interact; the API signatures are all significantly simplified; things are just nicer.
On the other hand, if you think you want an arbitrary Comparator
, you can do one of the following:
- Use a general
Predicate
and notRange
. (SinceRange
implements thePredicate
interface, you can usePredicates.compose(range, function)
to get aPredicate
.) - Use a wrapper class around your objects that defines the desired ordering.
- Introduction
- Basic Utilities
- Collections
- Graphs
- Caches
- Functional Idioms
- Concurrency
- Strings
- Networking
- Primitives
- Ranges
- I/O
- Hashing
- EventBus
- Math
- Reflection
- Releases
- Tips
- Glossary
- Mailing List
- Stack Overflow
- Android Overview
- Footprint of JDK/Guava data structures