Crate ranged_integers

Crate ranged_integers 

Source
Expand description

§Ranged integers [nightly only]

The crate provides an integer type restricted to a compile time defined range with automatic data size selection, automatic bounds calculation for arithmetics and the possibility of fixed-size array indexing and range iteration.

The library relies to the unstable const generics-related features. It causes ICEs on some Rust toolchains and may once stop working. It will require nightly Rust until the generic-const-exprs feature is stabilized. Consider the other stable ranged integers crates, allowing to ensure the bounds and perform the arithmetic operations withing the type:

  • deranged. It provides checked/saturating and unsafe unchecked arithmetics within the specified bounds, equalities and comparisons. There is a procedural macro to select a type of ranged based on literal bounds.
  • light_ranged_integers provides a nice way to choose the type of arithmetic operation (wrapping/saturating/panicking) with the type-level mark.

§Version info

The version 0.10.1 was built for nightly-2025-06-24 toolchain.

The const arithmetics were added. The const fn-s are used for compile-time arithmetics, since the std::opts traits are mostly non-const. The i128 representation was removed from Ranged. This has been done because the bounds violation generates an unpleasant error in which the place of occurrence is not specified, only the violated line inside ranged_integers crate. So, the possibility to violate the i128 bounds during the automatic bounds calculation is eliminated. The bounds of i64/u64 are explicitly written as a trait constraint, so it generates a much more helpful error message.

§Prerequisites

The library usage requires the following Rust features enabled in the user crate or application:

// Without this rustc generates errors and sometimes panics.
#![feature(adt_const_params, generic_const_exprs)]

Note that the features needed depend on the package version.

§Usage and examples

§Ranged semantics

Use Ranged<MIN, MAX> type to be sure of the value range:

fn move_player(dice_roll: Ranged<1, 6>) {  // Ranged<1, 6> is automatically of 1 byte
    let x : i32 = dice_roll.into(); // Conversion is allowed, i32 can store 1..=6
}

§Contents

§Data layout paradigm

The Ranged automatically chooses the smallest size possible according to MIN..=MAX range. It supports i8, u8, i16, u16, i32, u32, i64 and u64 layouts (i128 and u128 are not supported), and a special zero-size layout for “constant” values with MIN==MAX.

use core::mem::size_of;
assert_eq!(size_of::< Ranged<42, 42> >(), 0); // It's always 42, no need to store it
 
assert_eq!(size_of::< Ranged<-1, 127> >(), 1); // Fits i8
assert_eq!(size_of::< Ranged<0,  200> >(), 1); // Fits u8
assert_eq!(size_of::< Ranged<-1, 200> >(), 2); // Fits i16, doesn't fit i8 or u8
 
assert_eq!(size_of::< Ranged<0, 90000> >(),  4); // The range fits i32

The implementation heavily relies on the optimizer.

§Ranged and integer primitives

§Creation of Ranged at compile time

The Ranged::create_const can be used to create a Ranged value checking it at compile time. The macro r! does the same but a bit shorter.

// Way 1: specify the bounds explicitly
move_player(Ranged::<1,6>::create_const::<4>());
move_player(r!([1 6] 4));  // Same thing

// Way 2: do not specify the bounds when possible
move_player(Ranged::create_const::<4>());
move_player(r!([] 4));  // Same thing
let x: Ranged::<0, 100> = Ranged::create_const::<42>();
let y: Ranged::<0, 100> = r!([] 42);  // Same thing

// Way 3: a special case with the single possible value
let x = Ranged::<4, 4>::create_const::<4>();
let y = r!(4);  // Same thing

It fails if the bounds are corrupted:

move_player(r!([] 7)); // Error: Can't store 7 in [1 6] interval
move_player(r!([1 7] 7)); // Error: type mismatch, move_player() requires Ranged<1, 6>

§Ranged -> Ranged conversion

The Ranged can be converted to the type with different bounds using expand() generic method (compile-time check) and the methods fit(), fit_min(), fit_max() for runtime check. The Ranged values may be compared to each other yielding the shrunk bounds using fit_less_than(), fit_less_eq(), fit_greater_than(), and fit_greater_eq() methods.

let expandable: Ranged<4, 5> = r!([] 5);  // Fits Ranged<1,6> accepted by move_player
let overlapping: Ranged<4, 9> = r!([] 5);  // Doesn't fit, but the value 5 is acceptable
move_player(expandable.expand());
move_player(overlapping.fit().unwrap());

Shrinking with expand() is forbidden:

move_player(overlapping.expand());  // Error: the bounds 4..=9 can't fit in 1..=6

§int -> Ranged conversion

Way 1: ensure the bounds with Ranged::new(i128) -> Option<Ranged> function.

Note: due to incomplete features limitations, the function may fail to compile with the ‘trait bound is not satisfied’ if the bounds are not explicitly specified.

let some_int = 4;
let some_wrong_int = 8;
assert!(Ranged::<0, 6>::new(some_int) == Some(r!([0 6] 4)));
assert!(Ranged::<0, 6>::new(some_wrong_int) == None);

move_player(Ranged::new(some_int).unwrap());  // this call may fail to compile
                                              // use Ranged::<0, 6>::new instead

Way 2: use the Remainder operation with the “const” divisor

let x: Ranged<-9, 9> = 15_i32 % r!(10);
let y: Ranged<0, 9> = 15_u32 % r!(10);
assert!(x == r!(5));
assert!(y == r!(5));

Way 3: Convert the primitive types to Ranged with their native bounds using AsRanged

use ranged_integers::AsRanged;
let x = 15_u8.as_ranged();  // Ranged<0, 255>
let y = 15_i16.as_ranged(); // Ranged<-32768, 32767>

§Ranged -> int conversion

int::From trait is implemented when the value is proved to fit into the result type:

let x = r!([0 200] 20);
let y: u8 = x.into();  // 0..=200 fits u8
let x = r!([0 200] 20);
let y: i8 = x.into();  // 0..=200 doesn't fit i8

From and Into operations can’t be used in const context. A set of const fns allows const conversions to any integer primitive except for u128:

let x = r!([0 200] 20);
let y = x.u8(); // y is u8
let z = x.i16(); // z is i16
let w = x.usize(); // w is usize
let x = r!([0 200] 20);
let err = x.i8();  // Error: 0..=200 doesn't fit i8

§Array indexing, slicing and iteration

The ConstInclusiveRange<MIN,MAX> zero-size type is a range MIN..=MAX capable to create the iterator (IntoIterator trait implemented) with Ranged<MIN, MAX> output type. The r! macro can be used instead.

The Ranged::iter_up method creates an iterator from the current value up to MAX.

The arrays [T; N] may be indexed with Ranged<0, {N-1}> and sliced with r!(MIN..END) range with a reference to fixed-size array output.

let arr = [r!([1 6] 2), r!([] 3), r!([] 4), r!([] 5)];

assert_eq!(arr[r!(1..3)], [3,4]);  // Slicing with array reference output
assert_eq!(arr[r!([0 3] 1)], 3);  // Slicing with array reference output

// Not recommended to use this:
    for i in ConstInclusiveRange::<0, 3> {
        move_player(arr[i])  // iters through 0,1,2,3
    }
for i in r!(0..4) {
    move_player(arr[i])  // iters through 0,1,2,3
}
for i in r!(0..=3) {
    move_player(arr[i])  // iters through 0,1,2,3
}
for mv in r!([1 6] 3).iter_up() {
    move_player(mv)  // calls with 3,4,5,6
}

§Comparison

All Eq and Ord operations between different Ranged types are allowed, so as Ranged vs integer comparisons:

assert!(r!([1 6] 4) == r!([1 10] 4));
assert!(r!([1 6] 4) != r!([1 6] 5));
assert!(r!(4) == 4);
assert!(5 != r!([1 6] 4));

assert!(r!(5) > r!([1 6] 4));
assert!(4 < r!([1 6] 5));

To constrain the output type ruled by comparison, one may use Ranged::fit_less_than function and its siblings Ranged::fit_less_eq, Ranged::fit_greater_than, and Ranged::fit_greater_eq.

§Arithmetics

The bounds of arithmetic operations results are automatically recalculated.

Currently supported:

let x = r!([1 6] 5);
let y = r!([1 6] 4);

let a = x + y;  // The minimum is (1+1)=2, the maximum is (6+6)=12
let check_add: Ranged<2, 12> = a;  // Range assertion assignment
assert_eq!(check_add, r!(9));

let s = x - y;  // The minimum is (1-6)=-5, the maximum is (6-1)=5
let check_sub: Ranged<-5, 5> = s;  // Range assertion assignment
assert_eq!(check_sub, r!(1));

let m = x * y;  // The minimum is (1*1)=1, the maximum is (6*6)=36
let check_mul: Ranged<1, 36> = m;  // Range assertion assignment
assert_eq!(check_mul, r!(20));

let d = x / y;  // The minimum is (1/6)=0, the maximum is (6/1)=6
let check_div: Ranged<0, 6> = d;  // Range assertion assignment
assert_eq!(check_div, r!(1));

let r = x % y;
let check_rem: Ranged<0, 5> = r;  // Range assertion assignment
assert_eq!(check_rem, r!(1));

let n = -x;
let check_neg: Ranged<-6, -1> = n;  // Range assertion assignment
assert_eq!(check_neg, r!(-5));
 
let min: Ranged<1,6> = x.min(a);
let max: Ranged<2,12> = x.max(a);
let abs: Ranged<0,6> = r!([-1 6] -1).abs();
let neg: Ranged<-6,1> = r!([-1 6] -1).neg();

The division and remainder are allowed only if it’s impossible to store “0” in the divisor:

let x = r!([1 6] 4);
let y = r!([0 6] 5);
let z = r!([-1 6] 5);

let d = x / y; // Error: y can be 0
let e = x % z; // Error: z can be 0

The true bounds calculation routine for Rem operation is far too complex. In this library the calculated bounds will never exceed 1-DMAXABS..=DMAXABS-1 where DMAXABS is the maximum of the divisor absolute value.

This kind of Rem followed by expand is available for any dividend:

let x = r!([-1000 1000] 500);
let y = r!([-1 1000] 500);
let d = r!([1 10] 7);

let r: Ranged<-9, 9> = (x%d).expand();
// In this case, it expands just from Ranged<-9, 9> to itself

let r: Ranged<-9, 9> = (y%d).expand();
// In this case, it expands from Ranged<-1, 9>

But the actual calculation routine can produce smaller bounds:

// In the general case the output is Ranged<1-MAX, MAX-1>, MAX from divisor (by absolute value)
let x: Ranged<-9, 9> = (r!([-1000 1000] 500) % r!([1 10] 7));
let x: Ranged<-9, 9> = (r!([-1000 1000] 500) % r!([-10 -1] -7));
 
// If the dividend is nonnegative or nonpositive,
// the output range is limited to 0 from one side.
let x: Ranged<0, 9> = r!([0 100] 15) % r!(10);
let x: Ranged<-9, 0> = r!([-100 0] -15) % r!(10);

// The limit can't exceed the dividend's MIN(if negative) or MAX(if positive):
let x: Ranged<-10, 10> = r!([-10 10] 4) % r!([1 1000] 70);

// If the divisor is "constant", the output bounds are the true bounds:
let x: Ranged<4, 7> = r!([14 17] 15) % r!(10);

// In particular, if both operands are "constant", the result is "constant"
let x: Ranged<5, 5> = r!(15) % r!(10);

Following these rules, the calculated bounds may be wider than the true ones, like Ranged<36289, 36292> % Ranged<6, 9> = Ranged<0, 8> while the result never exceeds Ranged<1, 4>.

§Pattern matching

A limited version is implemented with rmatch! macro.

Modules§

value_check
The compile-time infrastructure for Ranged.

Macros§

r
Create a ranged value or a range at compile time
rmatch
Ranged pattern matching macro

Structs§

ConstInclusiveRange
Const range for iterators with Ranged output and array indexing
Ranged
A value restricted to the given bounds

Traits§

AsRanged
Convert an integer value to Ranged according to its own bounds.