Ranges
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, released 2026-09-22) and was run with JDK 26 on 2026-10-14. A
range is a lightweight, immutable sequence between two bounds, written with the .. operator. groovy.lang.Range extends
java.util.List, so every list method applies (size, each, collect, contains, subscript, toList), but an
IntRange stores only its two bounds, so even 1..1_000_000 costs no memory until you materialise it. Ranges are used to
iterate (see Control Structures), to slice lists and strings
(Lists & Arrays) and in switch and grep.
Range literals
The operators are:
| Literal | Bounds | Notes |
|---|---|---|
|
inclusive |
both ends included |
|
right-exclusive |
includes |
|
left-exclusive |
excludes |
|
fully exclusive |
excludes both ends (4.0) |
The left-exclusive and fully exclusive forms were added in Groovy 4.0. Surround a range literal with parentheses when you call a method on it, so that the method does not bind to the
right operand, and use println(...) with an explicit parenthesis rather than a space: println (1..5).size() is parsed as
println1..5.size().
def r = 1..5
println r
println r.class.name
println r.from + ' ' + r.to
println r.size()
println r.contains(5)
println(1..<5)
println((1..<5).contains(5))
println(1<..5)
println(1<..<5)
println((1<..<5).toList())
println((1..<5).class.name)
assert (0..5) instanceof List
assert (0..5).collect() == [0, 1, 2, 3, 4, 5]
assert (0<..<5).collect() == [1, 2, 3, 4]
Output:
1..5
groovy.lang.IntRange
1 5
5
true
1..<5
false
1<..5
1<..<5
[2, 3, 4]
groovy.lang.IntRange
Beginless and endless range literals (..5, 5..) did not compile in the Groovy 6.0.0 run used for this page, so give a range an explicit upper bound.
Reverse ranges
If the first bound is greater than the second, the range runs backwards. Its size counts the elements and isReverse() is
true. reverse() returns a new list in the opposite order, and reverseEach iterates backwards without building one.
def down = 5..1
println down
println down.toList()
println down.size()
println down.isReverse()
println((5..1).reverse())
println((1..0).size()) // two elements, 1 and 0: not empty
println((3..<3).size()) // empty: bounds equal and exclusive
println((3..<3).isEmpty())
(1..3).reverseEach { print it }
println()
Output:
5..1
[5, 4, 3, 2, 1]
5
true
[1, 2, 3, 4, 5]
2
0
true
321
An empty range needs equal bounds with an exclusive end; 1..0 is a two-element reverse range, which is a common source
of off-by-one bugs when the upper bound can be zero. Prefer 0..<n for loops over n items.
Stepping
step(n) returns a list (not a lazy range) of every n-th value. step on a reverse range walks down. Number.step(to, step, closure), upto and downto offer closure-based alternatives with the same
effect.
println((1..10).step(3))
println((1..10).step(3).class.name)
println((10..0).step(5))
println((1..20).step(5).toList())
println((1..<10).step(4).toList())
1.step(10, 4) { print it + ' ' }
println()
def countdown = []
10.downto(7) { countdown << it }
println countdown
def up = []
3.upto(6) { up << it }
println up
Output:
[1, 4, 7, 10]
java.util.ArrayList
[10, 5, 0]
[1, 6, 11, 16]
[1, 5, 9]
1 5 9
[10, 9, 8, 7]
[3, 4, 5, 6]
Iterating and transforming
Because a range is a list, the usual GDK methods apply. for (i in range) and each are the two common loops; the
exclusive upper bound 0..<n is the idiom for index loops.
println((1..4).collect { it * it })
println((1..4).sum())
println((1..4).inject(1) { a, b -> a * b })
println((1..10).findAll { it % 3 == 0 })
println((1..10).toList().class.simpleName)
for (i in 0..<3) print i
println()
(1..3).each { print it }
println()
def (a, b) = 1..2 // a range can be destructured
println "$a $b"
println(((1..3) as List) == [1, 2, 3])
println((1..3) == [1, 2, 3])
Output:
[1, 4, 9, 16]
10
24
[3, 6, 9]
ArrayList
012
123
1 2
true
true
Ranges of characters, strings and decimals
Any Comparable with next() and previous() methods can be a bound. Characters and strings step with String.next():
a string range such as 'aa'..'ad' increments the last character with carry, and the bounds must be able to reach each other
(otherwise an IllegalArgumentException is thrown). Ranges with BigDecimal bounds step by 1.
println('a'..'e')
println(('a'..'e').toList().reverse())
println('aa'..'ad')
println('a'..<'e')
println((1.5..4))
println((1.0G..3.0G).toList())
try {
println(('MON'..'WED').toList())
} catch (IllegalArgumentException e) {
println e.message
}
Output:
a..e
[e, d, c, b, a]
aa..ad
a..d
1.5..4
[1.0, 2.0, 3.0]
Incompatible Strings for Range: String#next() will not reach the expected value
Dates, enums and custom types
Dates, java.time.LocalDate (Groovy’s date/time extension methods supply next() and previous()), and enums are ranges out of
the box. For your own type, implement Comparable and add next() and previous(); equality should be consistent with
compareTo, and hashCode with equals.
import java.time.LocalDate
class Level implements Comparable<Level> {
int n
Level(int n) { this.n = n }
Level next() { new Level(n + 1) }
Level previous() { new Level(n - 1) }
int compareTo(Level o) { n <=> o.n }
String toString() { "L$n" }
boolean equals(o) { o instanceof Level && o.n == n }
int hashCode() { n }
}
def lv = new Level(1)..new Level(4)
println lv
println lv.class.name
println lv.size()
println lv.collect { it.n }
println(new Level(2) in lv)
println lv.step(2)
def d = LocalDate.of(2026, 3, 30)..LocalDate.of(2026, 4, 2)
println d
println d.size()
def dd = new Date(0)..new Date(0) + 2
println dd.collect { it.format('yyyy-MM-dd', TimeZone.getTimeZone('UTC')) }
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
println(Season.SPRING..Season.AUTUMN)
println((Season.SUMMER..Season.WINTER).size())
println(Season.AUTUMN..Season.SUMMER)
Output:
L1..L4
groovy.lang.ObjectRange
4
[1, 2, 3, 4]
true
[L1, L3]
2026-03-30..2026-04-02
4
[1970-01-01, 1970-01-02, 1970-01-03]
SPRING..AUTUMN
3
AUTUMN..SUMMER
Ranges in switch, grep and in
A range is a case label in switch (the class Range implements isCase as membership), and grep, in and
isCase use the same test. The range must be bounded; for open conditions use a closure case. Remember that
contains on an IntRange tests whether the value is one of the elements, whereas containsWithinBounds tests the
interval.
def classify(int t) {
switch (t) {
case { it < 10 }: return 'cold'
case 10..<25: return 'ok'
case 25..40: return 'hot'
default: return 'off-scale'
}
}
println([5, 10, 24, 25, 40, 41].collect { classify(it) })
println((1..20).grep(5..8))
println(['a', 'f', 'z'].grep('a'..'m'))
println(5 in 1..10)
println(11 in 1..10)
println((1..10).isCase(5))
println((1.0..2.0).contains(1.5))
println((1.0..2.0).containsWithinBounds(1.5))
Output:
[cold, ok, ok, hot, hot, off-scale]
[5, 6, 7, 8]
[a, f]
true
false
true
false
true
Slicing with ranges
A range as a subscript on a list, array or string calls getAt(Range) and returns a new object. Negative
indices count from the end, and a reverse range reverses the result; an exclusive range stops one before the bound. A range
on the left of = replaces a slice (putAt).
def l = (10..20).toList()
println l[2..4]
println l[2..<4]
println l[-3..-1]
println l[4..2]
println l[0..<-1] // every element except the last
println l[1..-2] // without first and last
def s = 'greenhouse'
println s[0..4]
println s[-5..-1]
println s[5..<8]
println s[4..0] // reversed
println "[${s[2..<2]}]" // empty
def window = [1, 2, 3, 4, 5]
window[1..2] = [9]
println window
Output:
[12, 13, 14]
[12, 13]
[18, 19, 20]
[14, 13, 12]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[11, 12, 13, 14, 15, 16, 17, 18, 19]
green
house
hou
neerg
[]
[1, 9, 4, 5]