Patterns · Lesson 7.2

Range pattern

Source
Match a whole run of integers in one arm with 1..=9 => and 0..10 =>, and narrow a range with a guard.

A literal pattern matches one value. A range pattern matches a whole run of them. It turns the question "is the value between these two numbers?" into a single arm instead of a pair of comparisons, and a ladder of ranges reads like the table it came from.

Two kinds of range

A range pattern is written with the same two operators as a range in a for loop:

PatternMatchesSuits
200..=299200 up to and including 299tables whose rows name first and last
0..100 up to, but not including, 10 (so 0 to 9)boundaries where one range starts another

Inclusive ranges: a table

HTTP status codes come in blocks of a hundred, and each block is described by its first and last code. ..= writes those rows exactly as a reference table would:

func Status(code: int32) -> char8[..] {
    return match code {
        100..=199 => "informational",
        200..=299 => "success",
        300..=399 => "redirection",
        400..=499 => "client error",
        500..=599 => "server error",
        else => "not a status code"
    };
}

An int32 has billions of values the arms do not reach, so the match ends with else, which decides what all of them mean.

Exclusive ranges: boundaries

.. stops just before its end, so when each range ends where the next one starts, no value falls between two arms and none is claimed twice:

func Size(count: int32) -> char8[..] {
    return match count {
        0 => "none",
        1..10 => "a few",
        10..100 => "dozens",
        100..1000 => "hundreds",
        -1000..0 => "a debt",
        else => "a lot"
    };
}

Two more things show here. Ranges mix freely with literal arms — 0 has an arm of its own — and a negative end is written with its minus sign, as in -1000..0.

flowchart LR
    n["`count`"] --> z["`0`<br/>none"]
    n --> a["`1..10`<br/>a few"]
    n --> b["`10..100`<br/>dozens"]
    n --> c["`100..1000`<br/>hundreds"]
    n --> d["`-1000..0`<br/>a debt"]
    n --> e["`else`<br/>a lot"]

Ten is the end of 1..10 but not part of it, so Size(10) is "dozens", as the output shows.

Ranges take guards

A range is a pattern like any other, so it can carry a guard. The arm is chosen only when the value is in the range and the condition holds; otherwise the match moves on:

func Fare(age: int32, student: bool) -> int32 {
    return match age {
        0..=5 => 0,
        6..=17 => 5,
        18..=25 if student => 5,
        65..=120 => 6,
        else => 10
    };
}

A student of 20 takes the third arm and pays 5. A non-student of 20 fails the guard, matches no other range, and reaches else: full fare.

Only literal numbers

Both ends must be literal integers. A named constant is not accepted as an end, because a bare name in a pattern means "bind a new variable" — that is how value if … worked in the previous lesson. Ranges also work on integers only; characters are matched one at a time, as Character pattern shows.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A literal pattern matches one value. A range pattern matches a whole run of them, written with
// the same two operators as a range in a `for` loop:
//
//     200..=299   from 200 up to and including 299
//     0..10       from 0 up to, but not including, 10
//
// That turns "is the value between these two numbers?" into a single arm instead of a pair of
// comparisons, and a ladder of ranges reads like the table it came from.
//
// Both ends must be literal numbers. A named constant is not accepted as an end, because a bare
// name in a pattern means "bind a new variable". With `const Low = 100;`, the arm `Low..=199 =>`
// stops with
//     error: pattern 'Low' cannot bind a new variable because 'Low' already names a constant
// Ranges work on integers only; characters are matched one at a time (see the CharacterPattern
// lesson).
import Io::PrintLine;

// Inclusive ranges, `..=`, suit a table whose rows name their first and last value.
func Status(code: int32) -> char8[..] {
    return match code {
        100..=199 => "informational",
        200..=299 => "success",
        300..=399 => "redirection",
        400..=499 => "client error",
        500..=599 => "server error",
        else => "not a status code"
    };
}

// Exclusive ranges, `..`, suit boundaries: each range stops where the next one starts, so no value
// falls between two arms and none is claimed twice. Ranges mix freely with literal arms, and a
// negative end is written with its minus sign.
func Size(count: int32) -> char8[..] {
    return match count {
        0 => "none",
        1..10 => "a few",
        10..100 => "dozens",
        100..1000 => "hundreds",
        -1000..0 => "a debt",
        else => "a lot"
    };
}

// A range takes a guard like any other pattern. The arm is chosen only when the age is in the range
// *and* the condition holds; otherwise the match moves on, so a non-student of 20 pays full fare.
func Fare(age: int32, student: bool) -> int32 {
    return match age {
        0..=5 => 0,
        6..=17 => 5,
        18..=25 if student => 5,
        65..=120 => 6,
        else => 10
    };
}

func Main() -> int {
    let codes: int32[4] = [204, 302, 404, 99];
    for index in 0..4 {
        PrintLine("{} {}", codes[index], Status(codes[index]));
    }

    let counts: int32[5] = [0, 9, 10, 250, -3];
    for index in 0..5 {
        PrintLine("{} is {}", counts[index], Size(counts[index]));
    }

    PrintLine("fares: age 4 pays {}, 12 pays {}, 70 pays {}", Fare(4, false), Fare(12, false),
        Fare(70, false));
    PrintLine("age 20 pays {} as a student, {} otherwise", Fare(20, true), Fare(20, false));
    return 0;
}

Run it

cd Examples/Patterns/RangePattern
rux run
204 success
302 redirection
404 client error
99 not a status code
0 is none
9 is a few
10 is dozens
250 is hundreds
-3 is a debt
fares: age 4 pays 0, 12 pays 5, 70 pays 6
age 20 pays 5 as a student, 10 otherwise

Common mistakes

Try it yourself

  1. Add a 1000..10000 => "thousands" arm to Size and print Size(4096).
  2. Rewrite Status with exclusive ranges, 100..200 and so on. Does the output change?
  3. Give Fare a guarded arm that lets anyone of 60 to 64 travel for 6 when they hold a pass, adding a pass: bool parameter.

Learn more