Learn Groovy ranges with 10 practical examples. Create numeric, character, and date ranges. Inclusive, exclusive, and reverse. Tested on Groovy 5.x.
“A range is the simplest way to express a sequence. If you can count it, Groovy can range it.”
Dierk König, Groovy in Action
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 20 minutes
A Groovy range lets you express a sequence of values – numbers, characters, dates, or anything that implements Comparable and next()/previous() – using just two dots. Instead of writing for (int i = 0; i < 10; i++), you write 0..9 and use it in loops, switch statements, list slicing, and collection filtering.
Ranges are one of those features that make Groovy feel genuinely elegant. You will use them in loops, switch statements, list slicing, collection filtering, and even real-world tasks like pagination and batch processing. Once you start using ranges, you will wonder how you ever coded without them.
In this tutorial, we will cover every type of Groovy range with 10+ tested examples, walk through the key methods and properties, and show you how ranges interact with Groovy lists and collections. You will be comfortable using ranges everywhere in your Groovy code.
Table of Contents
What Is a Groovy Range?
A Groovy range is an object that represents a sequence of values between a start (lower bound) and an end (upper bound). Under the hood, ranges implement java.util.List, which means you can treat them like any other list — iterate over them, check their size, use subscript operators, and pass them to any method that accepts a List.
According to the official Groovy documentation on range operators, ranges are created using the .. (inclusive) or ..< (exclusive) operators. The key classes are groovy.lang.IntRange for integers and groovy.lang.ObjectRange for other types.
Key Points:
- Ranges are created with
..(inclusive) or..<(exclusive of the upper bound) - Ranges implement
java.util.List, so all list methods work on them - They work with any type that implements
Comparableand hasnext()/previous()methods - Ranges are lazy — they do not store every element in memory
- Reverse ranges (e.g.,
10..1) are supported natively - Common use cases: loops, switch cases, list subscripts, filtering, pagination
Syntax and Types of Ranges
| Type | Syntax | Description | Example Values |
|---|---|---|---|
| Inclusive | 1..5 | Includes both endpoints | 1, 2, 3, 4, 5 |
| Exclusive (right) | 1..<5 | Excludes the upper bound | 1, 2, 3, 4 |
| Reverse | 5..1 | Counts backwards | 5, 4, 3, 2, 1 |
| Character | 'a'..'f' | Character sequence | a, b, c, d, e, f |
| Single element | 5..5 | Range with one value | 5 |
Range Types Overview
// Inclusive range
def inclusive = 1..5
println "Inclusive: ${inclusive}"
println "Type: ${inclusive.getClass().name}"
// Exclusive range (excludes upper bound)
def exclusive = 1..<5
println "Exclusive: ${exclusive}"
// Reverse range
def reverse = 5..1
println "Reverse: ${reverse}"
// Character range
def chars = 'a'..'f'
println "Chars: ${chars}"
// Single element range
def single = 5..5
println "Single: ${single}"
Output
Inclusive: 1..5 Type: groovy.lang.IntRange Exclusive: 1..<5 Reverse: 5..1 Chars: a..f Single: 5..5
Notice that Groovy uses IntRange for integer ranges and ObjectRange for characters and other types. The exclusive operator ..< only excludes the right-hand (upper) bound. There is no built-in operator to exclude the left-hand bound.
Range Properties and Methods
| Method/Property | Description | Example | Result |
|---|---|---|---|
from | Start of the range | (1..10).from | 1 |
to | End of the range | (1..10).to | 10 |
size() | Number of elements | (1..10).size() | 10 |
contains(val) | Check membership | (1..10).contains(5) | true |
step(n) | Step through range | (1..10).step(3) | [1, 4, 7, 10] |
isReverse() | Check if reversed | (10..1).isReverse() | true |
toList() | Convert to list | (1..5).toList() | [1, 2, 3, 4, 5] |
each{} | Iterate elements | (1..3).each{ print it } | 123 |
Range Properties and Methods
def range = 1..10
println "from: ${range.from}"
println "to: ${range.to}"
println "size: ${range.size()}"
println "contains 5: ${range.contains(5)}"
println "contains 11: ${range.contains(11)}"
println "isReverse: ${range.isReverse()}"
println "step(3): ${range.step(3)}"
println "toList: ${range.toList()}"
println "min: ${range.min()}"
println "max: ${range.max()}"
println "sum: ${range.sum()}"
Output
from: 1 to: 10 size: 10 contains 5: true contains 11: false isReverse: false step(3): [1, 4, 7, 10] toList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] min: 1 max: 10 sum: 55
Since ranges implement List, you get access to all of Groovy’s collection methods — min(), max(), sum(), collect(), findAll(), and more. This is where ranges really shine compared to traditional Java loops.
10 Practical Range Examples
Example 1: Inclusive Range (1..10)
What we’re doing: Creating a basic inclusive range and exploring what it contains.
Example 1: Inclusive Range
// Inclusive range - both endpoints included
def numbers = 1..10
println "Range: ${numbers}"
println "Size: ${numbers.size()}"
println "First: ${numbers.first()}"
println "Last: ${numbers.last()}"
println "As List: ${numbers.toList()}"
// You can also iterate directly
print "Elements: "
numbers.each { print "${it} " }
println()
// Check membership with 'in' keyword
println "5 in range: ${5 in numbers}"
println "15 in range: ${15 in numbers}"
Output
Range: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Size: 10 First: 1 Last: 10 As List: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Elements: 1 2 3 4 5 6 7 8 9 10 5 in range: true 15 in range: false
What happened here: The 1..10 range includes both 1 and 10. It acts like a list, so first(), last(), and size() all work. The in keyword is a concise way to check if a value falls within the range — much cleaner than writing x >= 1 && x <= 10.
Example 2: Exclusive Range (1..<10)
What we’re doing: Creating a range that excludes the upper bound — perfect for zero-based indexing.
Example 2: Exclusive Range
// Exclusive range - excludes upper bound
def exclusive = 1..<10
println "Range: ${exclusive}"
println "Size: ${exclusive.size()}"
println "Contains 9: ${exclusive.contains(9)}"
println "Contains 10: ${exclusive.contains(10)}"
println "As List: ${exclusive.toList()}"
// Common use: iterating indices of a list
def fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
def indices = 0..<fruits.size()
println "\nIndices: ${indices}"
indices.each { i ->
println " fruits[${i}] = ${fruits[i]}"
}
Output
Range: 1..<10 Size: 9 Contains 9: true Contains 10: false As List: [1, 2, 3, 4, 5, 6, 7, 8, 9] Indices: 0..<5 fruits[0] = apple fruits[1] = banana fruits[2] = cherry fruits[3] = date fruits[4] = elderberry
What happened here: The ..< operator creates a range that includes the start but excludes the end. This is incredibly useful when working with zero-based indices — 0..<list.size() gives you exactly the valid index range. No more off-by-one errors.
Example 3: Character Ranges (‘a’..’z’)
What we’re doing: Creating ranges of characters — useful for alphabet operations and validation.
Example 3: Character Ranges
// Lowercase alphabet
def lowercase = 'a'..'z'
println "Lowercase letters: ${lowercase.size()}"
println "First 5: ${lowercase.toList().take(5)}"
// Uppercase range
def uppercase = 'A'..'Z'
println "Uppercase letters: ${uppercase.size()}"
// Partial ranges
def vowels = ('a'..'z').findAll { it in ['a', 'e', 'i', 'o', 'u'] }
println "Vowels: ${vowels}"
// Digit characters
def digits = '0'..'9'
println "Digit chars: ${digits.toList()}"
// Check if a character is a letter
def ch = 'G'
println "'${ch}' is uppercase letter: ${ch in ('A'..'Z')}"
println "'${ch}' is lowercase letter: ${ch in ('a'..'z')}"
Output
Lowercase letters: 26 First 5: [a, b, c, d, e] Uppercase letters: 26 Vowels: [a, e, i, o, u] Digit chars: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 'G' is uppercase letter: true 'G' is lowercase letter: false
What happened here: Character ranges work because Character implements Comparable and Groovy adds next()/previous() to it. You can combine character ranges with collection methods like findAll() to filter characters. This is great for validation — checking if a character falls within a specific alphabet range.
Example 4: Reverse Ranges (10..1)
What we’re doing: Counting backwards with reverse ranges and the reverse() method.
Example 4: Reverse Ranges
// Reverse range - countdown
def countdown = 10..1
println "Countdown: ${countdown.toList()}"
println "isReverse: ${countdown.isReverse()}"
// Reverse a forward range
def forward = 1..5
def reversed = forward.reverse()
println "Forward: ${forward.toList()}"
println "Reversed: ${reversed}"
// Practical: countdown timer
print "Launch in: "
(5..1).each { print "${it}... " }
println "Go!"
// Reverse character range
def reverseChars = 'z'..'a'
println "Last 5 letters reversed: ${reverseChars.toList().take(5)}"
// Reverse with step
println "Even countdown: ${(10..2).step(2)}"
Output
Countdown: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] isReverse: true Forward: [1, 2, 3, 4, 5] Reversed: [5, 4, 3, 2, 1] Launch in: 5... 4... 3... 2... 1... Go! Last 5 letters reversed: [z, y, x, w, v] Even countdown: [10, 8, 6, 4, 2]
What happened here: When the start value is greater than the end, Groovy automatically creates a reverse range. You can check this with isReverse(). Combined with step(), you get powerful countdown patterns without writing manual decrement loops.
Example 5: Ranges in For Loops
What we’re doing: Using ranges as loop iterators — the most common use case for ranges.
Example 5: Ranges in Loops
// Classic for-in loop with range
println "=== Multiplication Table (5) ==="
for (i in 1..10) {
println "5 x ${i} = ${5 * i}"
}
// Using each (more Groovy-idiomatic)
println "\n=== Squares ==="
(1..5).each { n ->
println "${n}^2 = ${n * n}"
}
// Nested ranges for grid
println "\n=== 3x3 Grid ==="
for (row in 1..3) {
for (col in 1..3) {
print "(${row},${col}) "
}
println()
}
// Times method vs range (comparison)
println "\n=== 3.times vs 0..2 ==="
3.times { print "times:${it} " }
println()
(0..2).each { print "range:${it} " }
println()
Output
=== Multiplication Table (5) === 5 x 1 = 5 5 x 2 = 10 5 x 3 = 15 5 x 4 = 20 5 x 5 = 25 5 x 6 = 30 5 x 7 = 35 5 x 8 = 40 5 x 9 = 45 5 x 10 = 50 === Squares === 1^2 = 1 2^2 = 4 3^2 = 9 4^2 = 16 5^2 = 25 === 3x3 Grid === (1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3) === 3.times vs 0..2 === times:0 times:1 times:2 range:0 range:1 range:2
What happened here: Ranges replace the verbose Java for (int i = 1; i <= 10; i++) with the clean for (i in 1..10). The .each{} method is even more idiomatic. Note that 3.times{} and (0..2).each{} produce the same result — use whichever reads better in context.
Example 6: Ranges in Switch Statements
What we’re doing: Using ranges as switch cases — one of the cleanest patterns in Groovy.
Example 6: Ranges in Switch
// Grade calculator using ranges in switch
def getGrade(int score) {
switch (score) {
case 90..100: return 'A'
case 80..<90: return 'B'
case 70..<80: return 'C'
case 60..<70: return 'D'
case 0..<60: return 'F'
default: return 'Invalid'
}
}
[95, 87, 73, 65, 42, 100, -5].each { score ->
println "Score ${score}: Grade ${getGrade(score)}"
}
// Age group classifier
println "\n=== Age Groups ==="
def classifyAge(int age) {
switch (age) {
case 0..12: return 'Child'
case 13..19: return 'Teenager'
case 20..64: return 'Adult'
case 65..120: return 'Senior'
default: return 'Unknown'
}
}
[5, 15, 35, 70].each { age ->
println "Age ${age}: ${classifyAge(age)}"
}
Output
Score 95: Grade A Score 87: Grade B Score 73: Grade C Score 65: Grade D Score 42: Grade F Score 100: Grade A Score -5: Grade Invalid === Age Groups === Age 5: Child Age 15: Teenager Age 35: Adult Age 70: Senior
What happened here: This is one of the most powerful uses of Groovy ranges. Groovy’s switch statement uses isCase() under the hood, which for ranges calls contains(). This lets you match against an entire range of values instead of writing long chains of if/else if comparisons. Compare this to how you would do it in Java — the difference is dramatic.
Example 7: Ranges with collect, each, and findAll
What we’re doing: Combining ranges with Groovy’s built-in collection methods for data transformation and filtering.
Example 7: Ranges with Collection Methods
// collect - transform each element
def squares = (1..10).collect { it * it }
println "Squares: ${squares}"
// findAll - filter elements
def evens = (1..20).findAll { it % 2 == 0 }
println "Evens: ${evens}"
// find - first match
def firstOver50 = (1..100).find { it * it > 50 }
println "First n where n^2 > 50: ${firstOver50}"
// every / any - boolean checks
println "All positive: ${(1..10).every { it > 0 }}"
println "Any > 8: ${(1..10).any { it > 8 }}"
// inject (reduce) - accumulate
def factorial = (1..6).inject(1) { acc, val -> acc * val }
println "6! = ${factorial}"
// groupBy
def grouped = (1..15).groupBy { it % 3 == 0 ? 'divisible by 3' : 'other' }
println "Grouped: ${grouped}"
// collect with index
def indexed = (1..5).withIndex().collect { val, idx ->
"index ${idx} -> value ${val}"
}
println "Indexed: ${indexed}"
Output
Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] Evens: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] First n where n^2 > 50: 8 All positive: true Any > 8: true 6! = 720 Grouped: [other:[1, 2, 4, 5, 7, 8, 10, 11, 13, 14], divisible by 3:[3, 6, 9, 12, 15]] Indexed: [index 0 -> value 1, index 1 -> value 2, index 2 -> value 3, index 3 -> value 4, index 4 -> value 5]
What happened here: Because ranges implement List, every GDK collection method works on them. This is where ranges become more than just loop shortcuts — they become a foundation for functional-style programming. You can generate sequences, filter them, transform them, and reduce them all in one clean pipeline. For more on these methods, see our Groovy List tutorial.
Example 8: Ranges as List Subscripts
What we’re doing: Using ranges to slice lists and strings — extracting sub-sequences with subscript notation.
Example 8: Range Subscripts
def languages = ['Java', 'Groovy', 'Kotlin', 'Scala', 'Clojure', 'Python', 'Ruby']
// Slice with inclusive range
println "First 3: ${languages[0..2]}"
// Slice with exclusive range
println "First 3 (exclusive): ${languages[0..<3]}"
// Negative indices
println "Last 2: ${languages[-2..-1]}"
// Middle slice
println "Middle: ${languages[2..4]}"
// Reverse slice
println "Reversed first 3: ${languages[2..0]}"
// String slicing with ranges
def text = "Groovy Ranges Rock"
println "\nString[0..5]: '${text[0..5]}'"
println "String[7..12]: '${text[7..12]}'"
println "String[-4..-1]: '${text[-4..-1]}'"
// Replace a slice
def mutable = ['a', 'b', 'c', 'd', 'e']
mutable[1..3] = ['X', 'Y']
println "\nAfter replacing [1..3]: ${mutable}"
Output
First 3: [Java, Groovy, Kotlin] First 3 (exclusive): [Java, Groovy, Kotlin] Last 2: [Python, Ruby] Middle: [Kotlin, Scala, Clojure] Reversed first 3: [Kotlin, Groovy, Java] String[0..5]: 'Groovy' String[7..12]: 'Ranges' String[-4..-1]: 'Rock' After replacing [1..3]: [a, X, Y, e]
What happened here: Range subscripts let you slice lists and strings just like Python. You can use negative indices to count from the end, reverse ranges to get reversed slices, and even assign to a range subscript to replace a chunk of a list. This is one of the most practical day-to-day uses of Groovy ranges — it makes data extraction elegant and readable.
Example 9: Date Ranges
What we’re doing: Creating ranges of dates — useful for generating calendars, schedules, and date-based reports.
Example 9: Date Ranges
import java.time.LocalDate
import java.time.Month
// Date range using LocalDate
def startDate = LocalDate.of(2026, Month.MARCH, 1)
def endDate = LocalDate.of(2026, Month.MARCH, 7)
def week = startDate..endDate
println "Week range: ${week.size()} days"
week.each { date ->
println " ${date} (${date.dayOfWeek})"
}
// Find weekdays only
def weekdays = week.findAll { date ->
date.dayOfWeek.value <= 5 // Mon=1, Fri=5
}
println "\nWeekdays: ${weekdays.size()}"
weekdays.each { println " ${it} (${it.dayOfWeek})" }
// Exclusive date range
def march = LocalDate.of(2026, 3, 1)..<LocalDate.of(2026, 4, 1)
println "\nDays in March 2026: ${march.size()}"
println "First day: ${march.from}"
println "Last day: ${march.toList().last()}"
Output
Week range: 7 days 2026-03-01 (SUNDAY) 2026-03-02 (MONDAY) 2026-03-03 (TUESDAY) 2026-03-04 (WEDNESDAY) 2026-03-05 (THURSDAY) 2026-03-06 (FRIDAY) 2026-03-07 (SATURDAY) Weekdays: 5 2026-03-02 (MONDAY) 2026-03-03 (TUESDAY) 2026-03-04 (WEDNESDAY) 2026-03-05 (THURSDAY) 2026-03-06 (FRIDAY) Days in March 2026: 31 First day: 2026-03-01 Last day: 2026-03-31
What happened here: LocalDate works with Groovy ranges because Groovy adds next() and previous() methods to it. This lets you iterate day-by-day across any date range. Combined with findAll(), you can easily filter for weekdays, weekends, or specific days of the week. This is a real-world pattern you will use for report generation and scheduling tasks.
Example 10: Real-World – Pagination and Batch Processing
What we’re doing: Using ranges for pagination logic and batch processing — patterns you will use in production code.
Example 10: Pagination and Batch Processing
// === Pagination ===
def allItems = (1..97).collect { "Item-${it}" }
def pageSize = 10
def totalPages = Math.ceil(allItems.size() / pageSize).intValue()
println "Total items: ${allItems.size()}"
println "Page size: ${pageSize}"
println "Total pages: ${totalPages}"
// Get a specific page
def getPage(List items, int page, int size) {
def start = (page - 1) * size
def end = Math.min(start + size, items.size()) - 1
if (start > end) return []
return items[start..end]
}
(1..3).each { page ->
def pageItems = getPage(allItems, page, pageSize)
println "Page ${page}: ${pageItems.first()} ... ${pageItems.last()} (${pageItems.size()} items)"
}
def lastPage = getPage(allItems, totalPages, pageSize)
println "Page ${totalPages}: ${lastPage.first()} ... ${lastPage.last()} (${lastPage.size()} items)"
// === Batch Processing ===
println "\n=== Batch Processing ==="
def records = (1..25).toList()
def batchSize = 7
(0..<records.size()).step(batchSize).eachWithIndex { start, batchNum ->
def end = Math.min(start + batchSize - 1, records.size() - 1)
def batch = records[start..end]
println "Batch ${batchNum + 1}: ${batch}"
}
// === Generate IDs ===
println "\n=== Generated IDs ==="
def ids = (1..5).collect { "USER-${it.toString().padLeft(4, '0')}" }
println ids
Output
Total items: 97 Page size: 10 Total pages: 10 Page 1: Item-1 ... Item-10 (10 items) Page 2: Item-11 ... Item-20 (10 items) Page 3: Item-21 ... Item-30 (10 items) Page 10: Item-91 ... Item-97 (7 items) === Batch Processing === Batch 1: [1, 2, 3, 4, 5, 6, 7] Batch 2: [8, 9, 10, 11, 12, 13, 14] Batch 3: [15, 16, 17, 18, 19, 20, 21] Batch 4: [22, 23, 24, 25] === Generated IDs === [USER-0001, USER-0002, USER-0003, USER-0004, USER-0005]
What happened here: Ranges make pagination logic clean and readable. The getPage() function uses a range subscript to extract exactly one page of items. For batch processing, step() lets you jump through a range in fixed increments — perfect for chunking large datasets. And for ID generation, ranges combined with collect() and padLeft() produce formatted sequences in one line.
Bonus Example 11: Custom Ranges with Comparable
What we’re doing: Creating ranges with your own classes by implementing the required interface.
Bonus: Custom Ranges
// Enum ranges
enum Priority {
LOW, MEDIUM, HIGH, CRITICAL
}
def urgentRange = Priority.MEDIUM..Priority.CRITICAL
println "Urgent priorities: ${urgentRange.toList()}"
println "HIGH is urgent: ${Priority.HIGH in urgentRange}"
println "LOW is urgent: ${Priority.LOW in urgentRange}"
// Ranges with step for custom intervals
println "\n=== Step Patterns ==="
println "Every 5th: ${(0..50).step(5)}"
println "Odd numbers: ${(1..20).step(2)}"
println "Every 3rd letter: ${('a'..'z').step(3)}"
// Using ranges for random number generation
def random = new Random()
def rolls = (1..10).collect {
(1..6)[random.nextInt(6)]
}
println "\n10 dice rolls: ${rolls}"
println "Average: ${rolls.sum() / rolls.size()}"
Output
Urgent priorities: [MEDIUM, HIGH, CRITICAL] HIGH is urgent: true LOW is urgent: false === Step Patterns === Every 5th: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50] Odd numbers: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] Every 3rd letter: [a, d, g, j, m, p, s, v, y] 10 dice rolls: [3, 1, 4, 6, 2, 5, 3, 4, 1, 6] Average: 3.5
What happened here: Enums work with ranges automatically because they implement Comparable and Groovy provides next()/previous() for them. The step() method lets you skip elements at regular intervals — useful for generating patterns like odd numbers, multiples, or sampling every Nth item from a sequence.
Ranges vs Lists – When to Use Which
Since ranges implement List, you might wonder when to use a range versus a plain Groovy list. Here is the breakdown:
| Feature | Range | List |
|---|---|---|
| Memory | Stores only start + end | Stores every element |
| Mutability | Immutable | Mutable (ArrayList) |
| Creation | 1..1000000 | Would store 1M elements |
| Element types | Must be sequential/Comparable | Any mix of types |
| contains() speed | O(1) for IntRange | O(n) for ArrayList |
| Use case | Sequential numeric/char data | Arbitrary collections |
Range vs List Comparison
// Range: memory efficient, immutable
def range = 1..1000000
println "Range size: ${range.size()}"
println "Range contains 500000: ${range.contains(500000)}" // O(1) check
// Converting to list: uses memory for every element
def list = range.toList()
println "List size: ${list.size()}"
// Range is immutable
try {
range.add(42)
} catch (UnsupportedOperationException e) {
println "Cannot modify range: ${e.message}"
}
// List is mutable
list = [1, 2, 3]
list.add(4)
println "Mutable list: ${list}"
Output
Range size: 1000000 Range contains 500000: true List size: 1000000 Cannot modify range: null Mutable list: [1, 2, 3, 4]
The rule of thumb: if your data is a simple sequence (consecutive numbers, characters, dates), use a range. If your data is arbitrary or needs to be modified, use a list. For a deeper look at lists, check out our Groovy List tutorial, and for the bigger picture, see the Groovy Collections overview.
Edge Cases and Best Practices
Best Practices Summary
DO:
- Use
..<(exclusive) for zero-based index iteration to avoid off-by-one errors - Use ranges in switch statements for clean numeric/character classification
- Use
step()when you need intervals larger than 1 - Prefer ranges over manual counter variables for loops
- Use the
inkeyword for readable range membership checks
DON’T:
- Call
toList()on very large ranges unless you actually need every element in memory - Try to modify a range — they are immutable. Convert to a list first if you need mutation
- Assume a character range crosses character sets —
'Z'..'a'includes non-letter characters between them in Unicode - Forget that
..<only excludes the right-hand side — there is no>..operator for excluding the left side
Performance Considerations
Ranges are inherently memory-efficient because they only store two values (the start and the end) regardless of how many elements they represent. Here are some things to keep in mind:
Performance Tips
// Memory efficient: range stores only start and end
def bigRange = 1..10_000_000
println "Range created. Size: ${bigRange.size()}"
println "Contains 5000000: ${bigRange.contains(5_000_000)}" // O(1) - instant
// Avoid this: converts to List, stores 10M elements in memory
// def bigList = bigRange.toList() // Bad idea!
// Good: iterate without converting to list
def sum = 0
bigRange.each { sum += it }
// Or even better:
def sum2 = bigRange.sum()
// step() is also memory-efficient
def stepped = (1..1_000_000).step(1000)
println "Stepped size: ${stepped.size()}"
Output
Range created. Size: 10000000 Contains 5000000: true Stepped size: 1000
The biggest performance trap is calling toList() on a massive range. A range with 10 million elements uses almost no memory — but converting it to a list allocates storage for all 10 million integers. Use each{} or collect{} directly on the range to process elements without storing them all at once.
Common Pitfalls
Pitfall 1: Off-by-One with Inclusive vs Exclusive
Off-by-One Pitfall
def items = ['a', 'b', 'c', 'd', 'e']
// Wrong: inclusive range goes one past the last index
try {
println items[0..5] // IndexOutOfBoundsException!
} catch (IndexOutOfBoundsException e) {
println "Error: ${e.message}"
}
// Correct: use exclusive range matching list size
println "Correct (exclusive): ${items[0..<items.size()]}"
// Also correct: use -1 with inclusive range
println "Correct (inclusive): ${items[0..items.size()-1]}"
// Best: just use toList() or the list itself
println "Simplest: ${items}"
Output
Error: Index: 5, Size: 5 Correct (exclusive): [a, b, c, d, e] Correct (inclusive): [a, b, c, d, e] Simplest: [a, b, c, d, e]
Pitfall 2: Character Range Crossing Character Sets
Character Set Pitfall
// This range includes non-letter characters!
def crossRange = 'Z'..'a'
println "Z to a range size: ${crossRange.size()}"
println "Elements: ${crossRange.toList()}"
// Includes: Z, [, \, ], ^, _, `, a (Unicode order)
// Safe: keep within one character set
def upperOnly = 'A'..'Z'
def lowerOnly = 'a'..'z'
println "\nUpper: ${upperOnly.size()} chars"
println "Lower: ${lowerOnly.size()} chars"
Output
Z to a range size: 8 Elements: [Z, [, \, ], ^, _, `, a] Upper: 26 chars Lower: 26 chars
Between uppercase 'Z' (Unicode 90) and lowercase 'a' (Unicode 97), there are six non-letter characters: [, \, ], ^, _, `. Always stay within the same character set when using character ranges.
Conclusion
We covered a lot of ground in this Groovy range tutorial — from basic inclusive and exclusive ranges to character ranges, date ranges, switch statements, collection method chaining, list slicing, and real-world pagination patterns. Ranges are one of those Groovy features that seem simple on the surface but unlock a whole new level of expressiveness once you start using them throughout your code.
The key things to remember: use .. for inclusive, ..< for exclusive, and in for membership checks. Ranges implement List, so all collection methods work on them. And ranges are memory-efficient — they only store two values no matter how large the sequence.
For related topics, check out the Groovy List tutorial to learn how ranges interact with lists, and the Groovy Collections overview for the big picture on Groovy’s collection types.
Summary
- Groovy ranges use
..(inclusive) and..<(exclusive) operators to create sequences - Ranges work with integers, characters, dates, enums, and any
Comparabletype - Ranges implement
List, so all GDK collection methods (collect, findAll, inject, etc.) work on them - Ranges in switch statements replace verbose if/else chains with clean case blocks
- Ranges are memory-efficient and immutable — they store only the start and end values
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Collections Overview – Lists, Maps, Sets, and More
Frequently Asked Questions
What is the difference between .. and ..< in Groovy ranges?
The .. operator creates an inclusive range that includes both endpoints. For example, 1..5 contains [1, 2, 3, 4, 5]. The ..< operator creates an exclusive range that excludes the upper bound. So 1..<5 contains [1, 2, 3, 4]. Use exclusive ranges when working with zero-based indices to avoid off-by-one errors.
Can Groovy ranges work with dates?
Yes. Groovy adds next() and previous() methods to java.time.LocalDate, so you can create date ranges like LocalDate.of(2026,1,1)..LocalDate.of(2026,1,31). You can iterate over them, filter weekdays with findAll(), and check if a date falls within a range using the in keyword.
Are Groovy ranges stored in memory like lists?
No. Ranges are memory-efficient because they only store the start and end values. A range like 1..1000000 uses almost no memory, while (1..1000000).toList() would allocate storage for one million integers. Avoid calling toList() on large ranges unless you truly need every element in a list.
How do I create a range with a step size in Groovy?
Use the step() method on any range. For example, (0..20).step(5) produces [0, 5, 10, 15, 20] and (1..20).step(2) gives you odd numbers [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]. The step method returns a List of the stepped values.
Can I use ranges in Groovy switch statements?
Yes, and it is one of the most useful features of Groovy’s switch. You can write case 90..100: return 'A' instead of if (score >= 90 && score <= 100). Groovy’s switch uses isCase() internally, which for ranges calls contains(). This makes classification logic dramatically cleaner than Java’s if/else chains.
Related Posts
Previous in Series: Groovy Set – Unique Collections Made Easy
Next in Series: Groovy Collections Overview – Lists, Maps, Sets, and More
Related Topics You Might Like:
- Groovy List Tutorial – Complete Guide with Examples
- Groovy Collections Overview – Lists, Maps, Sets, and More
- Groovy Set – Unique Collections Made Easy
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment