This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_engine/rhaibook/language/ranges.md
2025-04-03 09:18:05 +02:00

6.7 KiB

Ranges

{{#include ../links.md}}

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.

Scenario Example
[for] statements for n in 0..100 { ... }
[in] expressions if n in 0..100 { ... }
[switch] expressions switch n { 0..100 => ... }
[Bit-fields] access let x = n[2..6];
Bits iteration for bit in n.bits(2..=9) { ... }
[Array] range-based APIs array.extract(2..8)
[BLOB] range-based APIs blob.parse_le_int(4..8)
[String] range-based APIs string.sub_string(4..=12)
[Characters] iteration for ch in string.bits(4..=12) { ... }
[Custom types] my_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.


`..` (exclusive range) and `..=` (inclusive range) are _different_ types to Rhai
and they do not interoperate.

Two different versions of the same API must be registered to handle both range styles.
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.

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][built-in packages] but excluded when using a [raw Engine]) operate on ranges.

Function Parameter(s) Description
start method and property beginning of the range
end method and property end of the range
contains, [in] operator number to check does this range contain the specified number?
is_empty method and property returns true if the range contains no items
is_inclusive method and property is the range inclusive?
is_exclusive method and property is the range exclusive?

TL;DR


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][function overloading] 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:

~~~rust
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.

~~~rust
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'
~~~