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
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