How does rand::Rng::gen_range handle inclusive vs exclusive bounds for different numeric types?

gen_range accepts different range types that determine bound inclusivity: Range (x..y) uses an exclusive upper bound while RangeInclusive (x..=y) uses an inclusive upper bound, with both supporting inclusive lower bounds. The range type determines the exact set of possible values, and gen_range uniformly samples from that set for integer types or the continuous interval for floating-point types.

Basic gen_range Usage

use rand::Rng;
 
fn basic_usage() {
    let mut rng = rand::thread_rng();
    
    // Range x..y: exclusive upper bound
    let value: i32 = rng.gen_range(0..10);  // 0 to 9, inclusive start, exclusive end
    
    // RangeInclusive x..=y: inclusive upper bound
    let value: i32 = rng.gen_range(0..=10); // 0 to 10, both bounds inclusive
    
    // Works with various types
    let byte: u8 = rng.gen_range(0..100);
    let big: u64 = rng.gen_range(0..1_000_000);
    let signed: i32 = rng.gen_range(-50..50);
}

The range syntax determines whether the upper bound is included.

Range vs RangeInclusive for Integers

use rand::Rng;
 
fn range_types_integers() {
    let mut rng = rand::thread_rng();
    
    // Range: exclusive upper bound (start..end)
    // Possible values: start, start+1, ..., end-1
    let v: i32 = rng.gen_range(1..5);  // Values: 1, 2, 3, 4 (not 5)
    
    // RangeInclusive: inclusive upper bound (start..=end)
    // Possible values: start, start+1, ..., end
    let v: i32 = rng.gen_range(1..=5); // Values: 1, 2, 3, 4, 5
    
    // Demonstration
    let mut counts_exclusive = [0i32; 5];  // Indices 0-4
    let mut counts_inclusive = [0i32; 6];  // Indices 0-5
    
    for _ in 0..10_000 {
        let e: usize = rng.gen_range(0..5);   // Only generates 0-4
        let i: usize = rng.gen_range(0..=5);  // Can generate 0-5
        counts_exclusive[e] += 1;
        counts_inclusive[i] += 1;
    }
    
    // counts_exclusive[4] has values
    // counts_exclusive[5] doesn't exist - index 5 is never generated
    // counts_inclusive[5] has values - index 5 can be generated
}

The .. syntax excludes the upper bound; ..= includes it.

Float Range Behavior

use rand::Rng;
 
fn float_ranges() {
    let mut rng = rand::thread_rng();
    
    // Floating-point ranges work similarly
    let f: f64 = rng.gen_range(0.0..1.0);   // [0.0, 1.0)
    let f: f64 = rng.gen_range(0.0..=1.0);  // [0.0, 1.0]
    
    // Exclusive upper bound: samples from [0.0, 1.0)
    // Value can be exactly 0.0, but never exactly 1.0
    // (though it can be extremely close to 1.0)
    
    // Inclusive upper bound: samples from [0.0, 1.0]
    // Value can be exactly 0.0 or exactly 1.0
    
    // Other float ranges
    let angle: f64 = rng.gen_range(0.0..std::f64::consts::TAU);
    let temp: f32 = rng.gen_range(-40.0..=40.0);
}

For floats, gen_range samples uniformly from a continuous interval.

Uniform Distribution for Integers

use rand::Rng;
use rand::distributions::Uniform;
 
fn uniform_distribution() {
    let mut rng = rand::thread_rng();
    
    // gen_range uses Uniform distribution internally
    let die_roll: i32 = rng.gen_range(1..=6);  // Fair die: 1-6 each with probability 1/6
    
    // Each value in range has equal probability
    // For gen_range(0..10), values 0-9 each have probability 1/10
    // For gen_range(0..=10), values 0-10 each have probability 1/11
    
    // Using Uniform distribution directly
    let uniform = Uniform::new(0, 10);  // Exclusive upper bound
    let v: i32 = rng.sample(uniform);    // Same as gen_range(0..10)
    
    let uniform_inclusive = Uniform::new_inclusive(0, 10);
    let v: i32 = rng.sample(uniform_inclusive);  // Same as gen_range(0..=10)
}

Integer ranges produce uniform discrete distributions.

Edge Cases with Integer Types

use rand::Rng;
 
fn integer_edge_cases() {
    let mut rng = rand::thread_rng();
    
    // Single value range
    let v: i32 = rng.gen_range(5..5);    // Empty range - panics!
    let v: i32 = rng.gen_range(5..=5);   // Always returns 5
    
    // Range limits for each type
    let v: u8 = rng.gen_range(0..=255);  // Full u8 range
    let v: i8 = rng.gen_range(-128..=127);  // Full i8 range
    
    // Reversed range - panics!
    // let v: i32 = rng.gen_range(10..5);   // Panics: start > end
    // let v: i32 = rng.gen_range(10..=5);  // Panics: start > end
    
    // Large ranges
    let v: u64 = rng.gen_range(0..u64::MAX);     // Almost full u64 range (excludes MAX)
    let v: u64 = rng.gen_range(0..=u64::MAX);     // Full u64 range (includes MAX)
    
    // For u8, full range can be expressed two ways:
    let v1: u8 = rng.gen_range(0..255);   // Values 0-254
    let v2: u8 = rng.gen_range(0..=255);  // Values 0-255
}

Empty ranges and reversed ranges panic at runtime.

Handling Full Type Range

use rand::Rng;
 
fn full_type_range() {
    let mut rng = rand::thread_rng();
    
    // For types where the full range is needed:
    // Using RangeInclusive with the type's MAX and MIN
    
    // Full u8 range: 0 to 255
    let v: u8 = rng.gen_range(u8::MIN..=u8::MAX);
    
    // Full i32 range: -2147483648 to 2147483647
    let v: i32 = rng.gen_range(i32::MIN..=i32::MAX);
    
    // Note: gen_range(0..256) for u8 would overflow
    // let v: u8 = rng.gen_range(0..256);  // Compile error: 256 doesn't fit in u8
    
    // For exclusive upper bound with full range:
    // Can't use gen_range(0..256) for u8
    // Must use inclusive: gen_range(0..=255)
    // Or use gen() for any value in type
    let v: u8 = rng.gen();  // Generates any u8 value
}

RangeInclusive is necessary when the upper bound is the type's maximum value.

Float Edge Cases

use rand::Rng;
 
fn float_edge_cases() {
    let mut rng = rand::thread_rng();
    
    // Empty range - panics!
    // let v: f64 = rng.gen_range(5.0..5.0);   // Panics (no values in range)
    
    // Range where start == end
    let v: f64 = rng.gen_range(5.0..=5.0);  // Always returns exactly 5.0
    
    // Reversed range - panics!
    // let v: f64 = rng.gen_range(10.0..5.0);  // Panics: start > end
    
    // Very small range
    let v: f64 = rng.gen_range(0.0..1e-300);  // Very small numbers
    
    // Range including infinity - panics or undefined!
    // let v: f64 = rng.gen_range(0.0..f64::INFINITY);  // Problematic
    
    // NaN in range - panics!
    // let v: f64 = rng.gen_range(0.0..f64::NAN);  // Panics
    
    // Proper float range usage
    let v: f64 = rng.gen_range(0.0..=1.0);  // Standard [0, 1]
    let angle: f64 = rng.gen_range(0.0..std::f64::consts::TAU);  // [0, 2π)
}

Float ranges have special considerations for edge cases.

Bounds Trait Requirements

use rand::Rng;
use rand::distributions::uniform::SampleUniform;
 
// gen_range works with types that implement SampleUniform
// This includes: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64
 
fn supported_types() {
    let mut rng = rand::thread_rng();
    
    // Signed integers
    let i8_v: i8 = rng.gen_range(-10..=10);
    let i16_v: i16 = rng.gen_range(-100..100);
    let i32_v: i32 = rng.gen_range(-1000..=1000);
    let i64_v: i64 = rng.gen_range(-10000..10000);
    let i128_v: i128 = rng.gen_range(-100000..=100000);
    
    // Unsigned integers
    let u8_v: u8 = rng.gen_range(0..100);
    let u16_v: u16 = rng.gen_range(0..=1000);
    let u32_v: u32 = rng.gen_range(0..10000);
    let u64_v: u64 = rng.gen_range(0..=100000);
    let u128_v: u128 = rng.gen_range(0..1000000);
    
    // Pointer-sized integers
    let isize_v: isize = rng.gen_range(-100..100);
    let usize_v: usize = rng.gen_range(0..=100);
    
    // Floating-point
    let f32_v: f32 = rng.gen_range(0.0..1.0);
    let f64_v: f64 = rng.gen_range(0.0..=1.0);
}

gen_range supports all primitive numeric types that implement SampleUniform.

Custom Types with gen_range

use rand::Rng;
use rand::distributions::uniform::{Uniform, SampleUniform, SampleBorrow};
 
// Custom types can implement SampleUniform to work with gen_range
// This requires implementing the distribution machinery
 
// For simple wrapped types, you can map:
fn custom_wrapped() {
    let mut rng = rand::thread_rng();
    
    // If you have a custom type wrapping a numeric:
    #[derive(Clone, Copy, Debug)]
    struct Percentage(u8);  // 0-100
    
    // Map from u8 to Percentage
    let percent = Percentage(rng.gen_range(0..=100));
    
    // Or for degrees:
    struct Degrees(f32);
    let angle = Degrees(rng.gen_range(0.0..=360.0));
}

Custom types require implementing SampleUniform or mapping from supported types.

Comparison: Range Types

use rand::Rng;
 
fn range_type_comparison() {
    let mut rng = rand::thread_rng();
    
    // Range (start..end) - exclusive upper bound
    let v: i32 = rng.gen_range(1..10);
    // Values: 1, 2, 3, 4, 5, 6, 7, 8, 9 (9 values)
    // Count: end - start = 10 - 1 = 9
    
    // RangeInclusive (start..=end) - inclusive upper bound
    let v: i32 = rng.gen_range(1..=10);
    // Values: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 (10 values)
    // Count: end - start + 1 = 10 - 1 + 1 = 10
    
    // RangeFrom (start..) - unbounded upper
    // NOT supported by gen_range - panics or doesn't compile
    // let v: i32 = rng.gen_range(0..);  // Won't work
    
    // RangeFull (..) - entire range
    // NOT supported by gen_range - use gen() instead
    let v: i32 = rng.gen();  // Generates any i32
    
    // RangeTo (..end) - unbounded lower
    // NOT supported by gen_range
    // let v: i32 = rng.gen_range(..10);  // Won't work
    
    // RangeToInclusive (..=end) - unbounded lower, inclusive upper
    // NOT supported by gen_range
    // let v: i32 = rng.gen_range(..=10);  // Won't work
}

Only Range and RangeInclusive are supported by gen_range.

Common Patterns

use rand::Rng;
 
fn common_patterns() {
    let mut rng = rand::thread_rng();
    
    // Array/Vec indexing - exclusive is common
    let items = ["a", "b", "c", "d", "e"];
    let idx: usize = rng.gen_range(0..items.len());
    println!("Random item: {}", items[idx]);
    
    // Die roll - inclusive is natural
    let die: u8 = rng.gen_range(1..=6);
    
    // Percent - inclusive makes sense
    let percent: u8 = rng.gen_range(0..=100);
    
    // Port range - inclusive both bounds
    let port: u16 = rng.gen_range(49152..=65535);  // Ephemeral ports
    
    // Buffer size limit - exclusive upper
    let size: usize = rng.gen_range(1..1024);  // 1 to 1023 bytes
    
    // Coordinate system - depends on bounds
    let x: f64 = rng.gen_range(-1.0..=1.0);  // Unit square
    let y: f64 = rng.gen_range(-1.0..=1.0);
    
    // Zero-indexed array position - exclusive is natural
    let row: usize = rng.gen_range(0..10);
    let col: usize = rng.gen_range(0..10);
}

Choose range type based on whether the upper bound should be included.

Float Precision Considerations

use rand::Rng;
 
fn float_precision() {
    let mut rng = rand::thread_rng();
    
    // Floating-point precision affects distribution
    // gen_range uses uniform sampling within the float representation
    
    // For Range(0.0..1.0):
    // - Samples uniformly from representable floats in [0, 1)
    // - Not all real numbers in [0, 1) are representable
    // - Some floats may be more likely due to representation
    
    // For RangeInclusive(0.0..=1.0):
    // - Includes exactly 1.0 as a possible value
    // - 1.0 has same probability as any other float step
    
    // Example: probability of exact 0.5
    let f: f64 = rng.gen_range(0.0..=1.0);
    // Probability of exactly 0.5 is extremely low
    // (only one specific bit pattern out of many)
    
    // For most applications, this precision is sufficient
    // For cryptographic randomness, consider specialized crates
}

Float ranges sample from representable values, not from the mathematical continuum.

Performance Characteristics

use rand::Rng;
use rand::distributions::Uniform;
 
fn performance() {
    let mut rng = rand::thread_rng();
    
    // Direct gen_range call
    let v: i32 = rng.gen_range(0..1000);
    // Creates Uniform distribution each call
    
    // Reusing the distribution is more efficient for many samples
    let uniform = Uniform::new(0, 1000);
    for _ in 0..10_000 {
        let v: i32 = rng.sample(uniform);
    }
    
    // For Range vs RangeInclusive, performance is similar
    // Both compile to similar internal code
    // The difference is in the range boundary check
    
    // For floats, gen_range involves floating-point operations
    // Slightly more complex than integer sampling
    for _ in 0..10_000 {
        let f: f64 = rng.gen_range(0.0..1.0);
    }
}

For many samples, create a Uniform distribution once and reuse it.

Synthesis

Quick reference:

Range Type Syntax Lower Bound Upper Bound Possible Values
Range x..y Inclusive Exclusive x, x+1, ..., y-1
RangeInclusive x..=y Inclusive Inclusive x, x+1, ..., y

Integer bounds:

use rand::Rng;
 
fn integer_bounds() {
    let mut rng = rand::thread_rng();
    
    // Exclusive: count = high - low
    // Range 1..5 has 4 values: 1, 2, 3, 4
    let v: i32 = rng.gen_range(1..5);   // 4 possible values
    
    // Inclusive: count = high - low + 1
    // Range 1..=5 has 5 values: 1, 2, 3, 4, 5
    let v: i32 = rng.gen_range(1..=5);  // 5 possible values
    
    // Use inclusive when upper bound should be included
    // Use exclusive for zero-indexed ranges (array indices)
}

Float bounds:

use rand::Rng;
 
fn float_bounds() {
    let mut rng = rand::thread_rng();
    
    // Exclusive: continuous interval [low, high)
    let f: f64 = rng.gen_range(0.0..1.0);   // [0, 1)
    // Value can be exactly 0.0, never exactly 1.0
    
    // Inclusive: continuous interval [low, high]
    let f: f64 = rng.gen_range(0.0..=1.0);  // [0, 1]
    // Value can be exactly 0.0 or exactly 1.0
    
    // For percentages, inclusive is natural
    let p: f32 = rng.gen_range(0.0..=100.0);
}

Key insight: gen_range uses the Rust range syntax to determine bound inclusivity. The Range type (x..y) follows Rust's convention of exclusive upper bounds, which is natural for zero-indexed collections where 0..len covers all indices. The RangeInclusive type (x..=y) includes the upper bound, which is more natural for "counting" semantics like dice (1-6), percentages (0-100), or when the upper bound represents the maximum value of a type. Both range types guarantee inclusive lower bounds. The method panics on empty ranges (x..x or x..=y where x > y) to prevent undefined behavior. For floating-point types, gen_range samples uniformly from the representable values within the interval, making both bounds achievable when specified as inclusive. The underlying implementation uses Uniform distribution, which can be created once and sampled multiple times for better performance in tight loops.