Ranges

Syntax

Numeric ranges can be constructed by the .. (exclusive) or ..= (inclusive) operators.

Exclusive range

start .. end

An exclusive range does not include the last (i.e. “end”) value.

The Rust type of an exclusive range is std::ops::Range<INT>.

type_of() an exclusive range returns "range".

Inclusive range

start ..= end

An inclusive range includes the last (i.e. “end”) value.

The Rust type of an inclusive range is std::ops::RangeInclusive<INT>.

type_of() an inclusive range returns "range=".

Usage Scenarios

Ranges are commonly used in the following scenarios.

ScenarioExample
for statementsfor n in 0..100 { ... }
in expressionsif n in 0..100 { ... }
switch expressionsswitch n { 0..100 => ... }
Bit-fields accesslet x = n[2..6];
Bits iterationfor bit in n.bits(2..=9) { ... }
Array range-based API’sarray.extract(2..8)
BLOB range-based API’sblob.parse_le_int(4..8)
String range-based API’sstring.sub_string(4..=12)
Characters iterationfor ch in string.bits(4..=12) { ... }
Custom typesmy_obj.action(3..=15, "foo");

Use as Parameter Type

Native Rust functions that take parameters of type std::ops::Range<INT> or std::ops::RangeInclusive<INT>, when registered into an Engine, accept ranges as arguments.

However, beware that .. (exclusive range) and ..= (inclusive range) are different types to Rhai and they do not interoperate. Therefore, two different versions of the same API must be registered to handle both range styles.


#![allow(unused)]
fn main() {
use std::ops::{Range, RangeInclusive};

/// The actual work function
fn do_work(obj: &mut TestStruct, from: i64, to: i64, inclusive: bool) {
    ...
}

let mut engine = Engine::new();

engine
    /// Version of API that accepts an exclusive range
    .register_fn("do_work", |obj: &mut TestStruct, range: Range<i64>|
        do_work(obj, range.start, range.end, false)
    )
    /// Version of API that accepts an inclusive range
    .register_fn("do_work", |obj: &mut TestStruct, range: RangeInclusive<i64>|
        do_work(obj, range.start(), range.end(), true)
    );

engine.run(
"
    let obj = new_ts();

    obj.do_work(0..12);         // use exclusive range

    obj.do_work(0..=11);        // use inclusive range
")?;
}

Indexers Using Ranges

Indexers commonly use ranges as parameters.


#![allow(unused)]
fn main() {
use std::ops::{Range, RangeInclusive};

let mut engine = Engine::new();

engine
    /// Version of indexer that accepts an exclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
    )
    /// Version of indexer that accepts an inclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
    );

engine.run(
"
    let obj = new_ts();

    let x = obj[0..12];         // use exclusive range

    obj[0..=11] = !x;           // use inclusive range
")?;
}

Built-in Functions

The following methods (mostly defined in the BasicIteratorPackage but excluded if using a raw Engine) operate on ranges.

FunctionParameter(s)Description
start method and propertybeginning of the range
end method and propertyend of the range
contains, in operatornumber to checkdoes this range contain the specified number?
is_inclusive method and propertyis the range inclusive?
is_exclusive method and propertyis the range exclusive?

TL;DR

Q: What happened to the open-ended ranges?

Rust has open-ended ranges, such as start.., ..end and ..=end. They are not available in Rhai.

They are not needed because Rhai can overload functions.

Typically, an API accepting ranges as parameters would have equivalent versions that accept a starting position and a length (the standard start + len pair), as well as a versions that accept only the starting position (the length assuming to the end).

In fact, usually all versions redirect to a call to one single version.

For example, a naive implementation of the extract method for arrays (without any error handling) would look like:


#![allow(unused)]
fn main() {
use std::ops::{Range, RangeInclusive};

// Version with exclusive range
#[rhai_fn(name = "extract", pure)]
pub fn extract_range(array: &mut Array, range: Range<i64>) -> Array {
    array[range].to_vec()
}
// Version with inclusive range
#[rhai_fn(name = "extract", pure)]
pub fn extract_range2(array: &mut Array, range: RangeInclusive<i64>) -> Array {
    extract_range(array, range.start()..range.end() + 1)
}
// Version with start
#[rhai_fn(name = "extract", pure)]
pub fn extract_to_end(array: &mut Array, start: i64) -> Array {
    extract_range(array, start..start + array.len())
}
// Version with start+len
#[rhai_fn(name = "extract", pure)]
pub fn extract(array: &mut Array, start: i64, len: i64) -> Array {
    extract_range(array, start..start + len)
}
}

Therefore, there should always be a function that can do what open-ended ranges are intended for.

The left-open form (i.e. ..end and ..=end) is trivially replaced by using zero as the starting position with a length that corresponds to the end position (for ..end).

The right-open form (i.e. start..) is trivially replaced by the version taking a single starting position.


#![allow(unused)]
fn main() {
let x = [1, 2, 3, 4, 5];

x.extract(0..3);    // normal range argument
                    // copies 'x' from positions 0-2

x.extract(2);       // copies 'x' from position 2 onwards
                    // equivalent to '2..'

x.extract(0, 2);    // copies 'x' from beginning for 2 items
                    // equivalent to '..2'
}