Range pattern
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:
| Pattern | Matches | Suits |
|---|---|---|
200..=299 | 200 up to and including 299 | tables whose rows name first and last |
0..10 | 0 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.
// 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
With
const Low = 100;, the arm Low..=199 => stops with error: pattern 'Low' cannot bind a new variable because 'Low' already names a constant. Write the number itself.0..=10 => "low", 10..=20 => "mid" compiles, but both arms claim 10 and the first one wins, so 10 is "low". Use .. for ranges that meet, or make sure each inclusive range starts one past the previous end.10..1 => is accepted, but no number is at least 10 and below 1, so the arm never matches and every value goes on to the next arm. The smaller end comes first.'a'..='z' => stops with error: range pattern cannot match value of type 'char32'. Bind the character and compare it in a guard instead — Character pattern shows how.Try it yourself
- Add a
1000..10000 => "thousands"arm toSizeand printSize(4096). - Rewrite
Statuswith exclusive ranges,100..200and so on. Does the output change? - Give
Farea guarded arm that lets anyone of 60 to 64 travel for 6 when they hold a pass, adding apass: boolparameter.
Learn more
matchand ranges in the Rux Reference- Range — the same
..and..=in aforloop - Tuple pattern — ranges as parts of a bigger pattern