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

a..b

inclusive

both ends included

a..<b

right-exclusive

includes a, excludes b

a<..b

left-exclusive

excludes a, includes b (4.0)

a<..<b

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]

Where the book is outdated

Older Groovy books predate the left-exclusive <.. and fully exclusive <..< forms (4.0) and show only .. and ..<. Range semantics (a lazily bounded List of Comparable elements) are unchanged and remain the same in Groovy 6.0.