Operators and Expressions

Range Operators - .. and ..=

Range operators (.. and ..=) construct lightweight range structs from the std::ops module. Ranges are used throughout Rust for two primary purposes: generating sequences of values in for loops and iterators, and slicing contiguous collections (&slice[start..end]).

The Six Range Forms in Rust

Depending on whether a start bound, an end bound, or both are provided, .. and ..= produce six distinct types in std::ops:

Syntax Type (std::ops::...) Bounds Meaning in Iteration (for) Meaning in Slicing (&s[...])
start..end Range<Idx> Half-open [start, end) Yields start up to end - 1 Elements from index start up to end - 1
start..=end RangeInclusive<Idx> Closed [start, end] Yields start through end (inclusive) Elements from index start through end
start.. RangeFrom<Idx> Lower-bounded [start, ∞) Infinite iterator starting at start (use .take(n)!) Elements from index start to the end of the slice
..end RangeTo<Idx> Upper-bounded [0, end) Not iterable (no starting point) Elements from index 0 up to end - 1
..=end RangeToInclusive<Idx> Upper-bounded [0, end] Not iterable (no starting point) Elements from index 0 through end
.. RangeFull Unbounded (-∞, +∞) Not iterable The entire slice (0..s.len())
fn main() {
    let half_open = 1..5;          // std::ops::Range<i32>
    let inclusive = 1..=5;         // std::ops::RangeInclusive<i32>
    let from_ten = 10..;           // std::ops::RangeFrom<i32>
    let up_to_four = ..4;          // std::ops::RangeTo<usize>
    let up_to_inc = ..=4;          // std::ops::RangeToInclusive<usize>
    let full = ..;                 // std::ops::RangeFull

    println!("half_open:  {half_open:?} (start={}, end={})", half_open.start, half_open.end);
    println!("inclusive:  {inclusive:?} (start={}, end={})", inclusive.start(), inclusive.end());
    println!("from_ten:   {from_ten:?} (first 3: {:?})", from_ten.take(3).collect::<Vec<_>>());
    println!("Slicing ranges: {up_to_four:?}, {up_to_inc:?}, {full:?}");
}
half_open:  1..5 (start=1, end=5)
inclusive:  1..=5 (start=1, end=5)
from_ten:   10.. (first 3: [10, 11, 12])
Slicing ranges: ..4, ..=4, ..

Slicing Arrays, Vectors, and Strings with Ranges

All six range forms implement SliceIndex, allowing you to extract borrowed sub-slices from arrays, vectors, and strings:

Array:   let data = ['a', 'b', 'c', 'd', 'e'];
Indices:              0    1    2    3    4

  &data[1..4]   --> ['b', 'c', 'd']          (indices 1, 2, 3)
  &data[1..=4]  --> ['b', 'c', 'd', 'e']     (indices 1, 2, 3, 4)
  &data[..3]    --> ['a', 'b', 'c']          (indices 0, 1, 2)
  &data[..=3]   --> ['a', 'b', 'c', 'd']     (indices 0, 1, 2, 3)
  &data[2..]    --> ['c', 'd', 'e']          (indices 2, 3, 4)
  &data[..]     --> ['a', 'b', 'c', 'd', 'e'](all elements)
fn main() {
    let data = ['a', 'b', 'c', 'd', 'e'];

    println!("&data[1..4]  = {:?}", &data[1..4]);
    println!("&data[1..=4] = {:?}", &data[1..=4]);
    println!("&data[..3]   = {:?}", &data[..3]);
    println!("&data[..=3]  = {:?}", &data[..=3]);
    println!("&data[2..]   = {:?}", &data[2..]);
    println!("&data[..]    = {:?}", &data[..]);
}
&data[1..4]  = ['b', 'c', 'd']
&data[1..=4] = ['b', 'c', 'd', 'e']
&data[..3]   = ['a', 'b', 'c']
&data[..=3]  = ['a', 'b', 'c', 'd']
&data[2..]   = ['c', 'd', 'e']
&data[..]    = ['a', 'b', 'c', 'd', 'e']

Checking Membership with .contains(&value)

Every range type provides a .contains(&item) method that checks whether a value falls inside the range bounds in $O(1)$ time (comparing against start and end without iterating through the range!):

fn main() {
    let working_hours = 9..17;          // 9..=16
    let percentage = 0.0..=100.0;       // Works on f64 too!
    let lowercase = 'a'..='z';          // Works on char!

    println!("Is 12 in 9..17?       {}", working_hours.contains(&12));
    println!("Is 17 in 9..17?       {}", working_hours.contains(&17));
    println!("Is 99.5 in 0..=100?   {}", percentage.contains(&99.5));
    println!("Is 'm' lowercase?     {}", lowercase.contains(&'m'));
    println!("Is (5..2).is_empty()? {}", (5..2).is_empty());
}
Is 12 in 9..17?       true
Is 17 in 9..17?       false
Is 99.5 in 0..=100?   true
Is 'm' lowercase?     true
Is (5..2).is_empty()? true

Range Patterns in match and if let

In pattern matching (match, if let, matches!), inclusive ranges (start..=end) and open-ended ranges (start.. or ..=end / ..end) match values directly:

fn classify_http_status(code: u16) -> &'static str {
    match code {
        100..=199 => "Informational",
        200..=299 => "Success",
        300..=399 => "Redirection",
        400..=499 => "Client Error",
        500..=599 => "Server Error",
        _ => "Non-standard Status",
    }
}

fn main() {
    println!("204: {}", classify_http_status(204));
    println!("404: {}", classify_http_status(404));
}
204: Success
404: Client Error

Note: Inside a match pattern, bounded ranges must use the inclusive ..= syntax (100..=199) rather than half-open 100..200, so the reader never has to guess whether the upper endpoint of a pattern is included.

Operator Precedence of .. and ..=

The range operators .. and ..= have very low precedence (lower than arithmetic +, -, *, /, bit shifts <<, comparisons, and method calls .):

  • 1 + 2..5 + 5 is parsed as (1 + 2)..(5 + 5) (3..10), which is convenient when computing bounds.
  • However, 0..5.len() or (0..5) with method calls requires parentheses (0..5).rev() because . binds much tighter than ..:
// ❌ does not compile
fn main() {
    for i in 0..5.rev() {
        println!("{i}");
    }
}
error[E0599]: no method named `rev` found for type `{integer}` in the current scope
 --> src/main.rs:2:19
  |
2 |     for i in 0..5.rev() {
  |                   ^^^ method not found in `{integer}`

error: aborting due to 1 previous error

Always wrap the range in parentheses (0..5).rev() before calling iterator methods on it.