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)

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.