Scala - Range with Examples
Scala provides multiple ways to construct ranges. The most common approach uses the `to` method for inclusive ranges and `until` for exclusive ranges.
Key Insights
- Scala’s Range represents an immutable sequence of evenly-spaced integers, offering memory-efficient iteration without storing all values in memory
- Ranges support both inclusive (
to) and exclusive (until) boundaries, with customizable step values for forward and backward traversal - Understanding Range lazy evaluation and collection conversion patterns prevents performance pitfalls in production code
Creating Basic Ranges
Scala provides multiple ways to construct ranges. The most common approach uses the to method for inclusive ranges and until for exclusive ranges.
// Inclusive range: includes both start and end
val inclusive = 1 to 5
println(inclusive.toList) // List(1, 2, 3, 4, 5)
// Exclusive range: excludes the end value
val exclusive = 1 until 5
println(exclusive.toList) // List(1, 2, 3, 4)
// Alternative syntax using Range object
val range1 = Range(1, 5) // exclusive by default
val range2 = Range.inclusive(1, 5) // explicit inclusive
The to and until methods are infix operators, making range syntax readable. Behind the scenes, these create Range.Inclusive and Range objects respectively.
// Type information
val r1: Range.Inclusive = 1 to 10
val r2: Range = 1 until 10
// Check boundaries
println(r1.start) // 1
println(r1.end) // 10
println(r1.isInclusive) // true
Working with Step Values
Ranges support custom step values using the by method. This enables forward and backward traversal with arbitrary intervals.
// Forward with step 2
val evens = 0 to 10 by 2
println(evens.toList) // List(0, 2, 4, 6, 8, 10)
// Backward range
val countdown = 10 to 1 by -1
println(countdown.toList) // List(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
// Larger steps
val decades = 1900 to 2000 by 10
println(decades.toList) // List(1900, 1910, 1920, ..., 2000)
Step values must have the same sign as the range direction. Attempting to create an invalid range results in an empty range:
val invalid = 1 to 10 by -1
println(invalid.isEmpty) // true
println(invalid.toList) // List()
// Correct backward range
val valid = 10 to 1 by -1
println(valid.isEmpty) // false
Range Operations and Transformations
Ranges support standard collection operations. However, understanding their lazy nature is crucial for performance optimization.
// Filtering
val range = 1 to 20
val filtered = range.filter(_ % 3 == 0)
println(filtered) // Vector(3, 6, 9, 12, 15, 18)
// Mapping
val squared = (1 to 5).map(x => x * x)
println(squared) // Vector(1, 4, 9, 16, 25)
// Taking elements
val first5 = (1 to 100).take(5)
println(first5) // Range 1 to 5
// Dropping elements
val without10 = (1 to 20).drop(10)
println(without10) // Range 11 to 20
Operations that maintain the range structure return new Range objects, while transformations like map and filter return collections:
val r = 1 to 10
println(r.take(5).getClass) // class scala.collection.immutable.Range
println(r.map(_ * 2).getClass) // class scala.collection.immutable.Vector
Membership Testing and Contains
Ranges provide efficient membership testing using the contains method, which performs mathematical checks rather than iterating through values.
val range = 1 to 100 by 5
println(range.contains(50)) // true
println(range.contains(51)) // false
println(range.contains(1)) // true
println(range.contains(100)) // true
// Performance benefit: O(1) instead of O(n)
val largeRange = 1 to 1000000
val startTime = System.nanoTime()
largeRange.contains(999999)
val elapsed = System.nanoTime() - startTime
println(s"Lookup time: $elapsed ns") // Very fast, constant time
For complex membership patterns, combine ranges with collection methods:
val validHours = 9 to 17
val validMinutes = 0 to 59 by 15
def isValidTime(hour: Int, minute: Int): Boolean = {
validHours.contains(hour) && validMinutes.contains(minute)
}
println(isValidTime(10, 15)) // true
println(isValidTime(10, 16)) // false
Practical Applications
Iteration Patterns
Ranges excel at replacing traditional for-loops with functional constructs:
// Traditional indexed iteration
val data = Array("a", "b", "c", "d", "e")
for (i <- 0 until data.length) {
println(s"Index $i: ${data(i)}")
}
// Batch processing
def processBatch(start: Int, end: Int): Unit = {
(start until end).foreach { id =>
println(s"Processing record $id")
}
}
processBatch(1000, 1010)
Generating Test Data
Ranges simplify test data generation:
case class User(id: Int, name: String)
// Generate test users
val testUsers = (1 to 100).map { id =>
User(id, s"user_$id")
}
// Create sample data with patterns
val temperatureReadings = (0 until 24).map { hour =>
val baseTemp = 20
val variation = Math.sin(hour * Math.PI / 12) * 5
(hour, baseTemp + variation)
}
println(temperatureReadings.take(5))
Pagination Logic
Ranges naturally model pagination scenarios:
def paginateResults[T](items: Seq[T], pageSize: Int): Seq[Seq[T]] = {
val pageCount = (items.length + pageSize - 1) / pageSize
(0 until pageCount).map { pageNum =>
val start = pageNum * pageSize
val end = Math.min(start + pageSize, items.length)
items.slice(start, end)
}
}
val allItems = (1 to 47).toList
val pages = paginateResults(allItems, 10)
pages.zipWithIndex.foreach { case (page, idx) =>
println(s"Page ${idx + 1}: ${page.size} items")
}
Performance Considerations
Ranges are lazy and don’t allocate memory for individual elements until accessed. This makes them highly efficient for large sequences:
// Memory efficient: doesn't allocate 1 billion integers
val huge = 1 to 1000000000
// Only materializes what's needed
val sample = huge.take(10).toList
println(sample) // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Avoid unnecessary conversion to collections
// Bad: materializes entire range
val bad = (1 to 1000000).toList.take(10)
// Good: takes before materializing
val good = (1 to 1000000).take(10).toList
When working with ranges in tight loops, prefer direct range iteration over converting to lists:
// Efficient
(1 to 1000000).foreach { i =>
// process i
}
// Inefficient: allocates unnecessary collection
(1 to 1000000).toList.foreach { i =>
// process i
}
Edge Cases and Gotchas
Understanding range boundaries prevents subtle bugs:
// Empty ranges
val empty1 = 5 to 1 // empty (wrong direction)
val empty2 = 1 to 5 by -1 // empty (step mismatch)
println(empty1.isEmpty) // true
// Floating point ranges don't exist in standard library
// Use NumericRange for Double/Float
val doubleRange = BigDecimal(0.0) to 1.0 by 0.1
println(doubleRange.toList)
// Inclusive vs exclusive with negative steps
val inc = 10 to 1 by -1 // includes 1
val exc = 10 until 1 by -1 // excludes 1
println(inc.last) // 1
println(exc.last) // 2
Ranges work exclusively with integral types. For other numeric types, use NumericRange:
import scala.collection.immutable.NumericRange
val longRange: NumericRange[Long] = NumericRange(1L, 1000000000L, 1L)
val charRange: NumericRange[Char] = NumericRange('a', 'z', 1)
println(charRange.take(5).toList) // List(a, b, c, d, e)