How does rand::distributions::Uniform::new_inclusive differ from new for range sampling boundaries?

Uniform::new creates a uniform distribution over the half-open range [low, high) where the upper bound is excluded from possible samples, while Uniform::new_inclusive creates a distribution over the closed range [low, high] where the upper bound is included as a possible sample value. The distinction mirrors Rust's range syntax: new(low, high) corresponds to low..high, and new_inclusive(low, high) corresponds to low..=high, making the boundary handling consistent with Rust's range conventions.

Uniform Distribution Basics

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn uniform_basics() {
    // Uniform distribution samples values with equal probability
    // Within a specified range
    
    // Create a uniform distribution over [0, 10) - exclusive upper
    let range = Uniform::new(0, 10);
    
    // Sample from the distribution
    let mut rng = thread_rng();
    let value: i32 = rng.sample(range);
    
    // Possible values: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    // NOT including 10
    
    println!("Sample: {}", value);
}

The Uniform distribution samples integers or floats uniformly within a specified range.

new: Half-Open Range [low, high)

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn new_exclusive() {
    // Uniform::new creates [low, high) range
    // Upper bound is EXCLUDED from possible samples
    
    let dist = Uniform::new(1, 6);  // Dice: 1..6
    
    let mut rng = thread_rng();
    
    // Possible values: 1, 2, 3, 4, 5
    // NOT including 6
    
    for _ in 0..10 {
        let roll: i32 = rng.sample(dist.clone());
        println!("Roll: {}", roll);
        assert!(roll >= 1 && roll < 6);
    }
    
    // This matches Rust's half-open range: 1..6
    // for i in 1..6 { } iterates 1, 2, 3, 4, 5
}

new(low, high) creates a distribution that can produce low but never high.

new_inclusive: Closed Range [low, high]

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn new_inclusive_closed() {
    // Uniform::new_inclusive creates [low, high] range
    // Upper bound is INCLUDED in possible samples
    
    let dist = Uniform::new_inclusive(1, 6);  // Dice: 1..=6
    
    let mut rng = thread_rng();
    
    // Possible values: 1, 2, 3, 4, 5, 6
    // INCLUDING 6
    
    for _ in 0..10 {
        let roll: i32 = rng.sample(dist.clone());
        println!("Roll: {}", roll);
        assert!(roll >= 1 && roll <= 6);
    }
    
    // This matches Rust's inclusive range: 1..=6
    // for i in 1..=6 { } iterates 1, 2, 3, 4, 5, 6
}

new_inclusive(low, high) creates a distribution that can produce both low and high.

The Range Analogy

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn range_analogy() {
    // Rust's range syntax provides a helpful analogy:
    
    // Half-open range (exclusive upper):
    // 0..10  -> values 0, 1, 2, ..., 9
    for i in 0..10 {
        println!("{}", i);  // Prints 0-9
    }
    
    // Closed range (inclusive upper):
    // 0..=10 -> values 0, 1, 2, ..., 10
    for i in 0..=10 {
        println!("{}", i);  // Prints 0-10
    }
    
    // Uniform::new matches half-open:
    let dist_exclusive = Uniform::new(0, 10);  // Like 0..10
    
    // Uniform::new_inclusive matches closed:
    let dist_inclusive = Uniform::new_inclusive(0, 10);  // Like 0..=10
    
    // Both use the same naming convention as ranges!
}

The naming mirrors Rust's .. (exclusive) and ..= (inclusive) range syntax.

Dice Rolling Example

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn dice_rolling() {
    let mut rng = thread_rng();
    
    // Standard 6-sided die: values 1 through 6
    // We want INCLUSIVE range: 1..=6
    
    // CORRECT: Use new_inclusive for dice
    let die = Uniform::new_inclusive(1, 6);
    let roll: i32 = rng.sample(die);
    println!("Die roll: {}", roll);  // 1-6 inclusive
    
    // WRONG: new(1, 6) would only give 1-5
    let wrong_die = Uniform::new(1, 6);
    let wrong_roll: i32 = rng.sample(wrong_die);
    println!("Wrong roll: {}", wrong_roll);  // 1-5 only!
    
    // For dice, use new_inclusive because we want all faces
}

Dice and games of chance typically need new_inclusive to include all possible outcomes.

Zero-Indexed Collections

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn zero_indexed() {
    let items = ['a', 'b', 'c', 'd', 'e'];
    
    // To pick a random element from the array:
    // Indices are 0..len (exclusive)
    
    // CORRECT: Use new for zero-indexed selection
    let index_dist = Uniform::new(0, items.len());
    let mut rng = thread_rng();
    
    let idx: usize = rng.sample(index_dist);
    let item = items[idx];
    println!("Random item: {}", item);
    
    // This is correct because valid indices are 0..len
    // items.len() == 5, valid indices are 0, 1, 2, 3, 4
    
    // WRONG: new_inclusive would include out-of-bounds
    let wrong_dist = Uniform::new_inclusive(0, items.len());
    // Could produce index 5, which is out of bounds!
}

For array indexing and zero-based collections, new(0, len) is correct—new_inclusive could produce out-of-bounds indices.

Floating-Point Ranges

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn floating_point() {
    let mut rng = thread_rng();
    
    // Floats also support both range types
    
    // new: [0.0, 1.0) - can produce 0.0, never 1.0
    let dist = Uniform::new(0.0, 1.0);
    let f: f64 = rng.sample(dist);
    assert!(f >= 0.0 && f < 1.0);
    
    // new_inclusive: [0.0, 1.0] - can produce 0.0 AND 1.0
    let dist_inc = Uniform::new_inclusive(0.0, 1.0);
    let f_inc: f64 = rng.sample(dist_inc);
    assert!(f_inc >= 0.0 && f_inc <= 1.0);
    
    // For floats, the difference matters for:
    // - Probability calculations where exact 1.0 matters
    // - Percentages that should include 100%
    // - Angle ranges [0, 2π] or [0, 360]
}

Floating-point distributions support both range types with the same boundary semantics.

When to Use Each

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn when_to_use() {
    // Use new(low, high) for:
    // - Zero-indexed array access
    // - Loop iteration counts
    // - Sizes and counts matching array bounds
    // - Any [0, n) range convention
    
    let arr = [1, 2, 3, 4, 5];
    let index = Uniform::new(0, arr.len());  // Correct for arrays
    
    // Use new_inclusive(low, high) for:
    // - Dice and game outcomes
    // - Card values (1-13)
    // - Human-indexed ranges (1-10, not 0-9)
    // - Inclusive mathematical intervals
    
    let die = Uniform::new_inclusive(1, 6);  // Standard die
    let card_rank = Uniform::new_inclusive(1, 13);  // Ace(1) to King(13)
    
    // | Scenario | Range Type | Method |
    // |----------|------------|--------|
    // | Array index | [0, len) | new(0, len) |
    // | Dice roll | [1, 6] | new_inclusive(1, 6) |
    // | Loop count | [0, n) | new(0, n) |
    // | Percent 0-100% | [0, 100] | new_inclusive(0, 100) |
}

Choose based on whether the upper bound should be a valid outcome.

Common Mistakes

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn common_mistakes() {
    let mut rng = thread_rng();
    
    // MISTAKE 1: Using new for dice
    let die_wrong = Uniform::new(1, 6);  // Only produces 1-5!
    // Always get values 1-5, never 6
    // Player can never roll a 6!
    
    // MISTAKE 2: Using new_inclusive for array index
    let arr = [10, 20, 30, 40, 50];
    let idx_wrong = Uniform::new_inclusive(0, arr.len() - 1);  // Works but confusing
    let idx_right = Uniform::new(0, arr.len());  // Clearer: matches array bounds
    
    // MISTAKE 3: Off-by-one confusion
    // Want values 0-9 inclusive? Two ways:
    let dist1 = Uniform::new_inclusive(0, 9);  // Explicit inclusive
    let dist2 = Uniform::new(0, 10);          // Half-open, equivalent for integers
    
    // Both produce 0-9 for integers
    // But dist2 is idiomatic for zero-indexed ranges
    
    // MISTAKE 4: Float precision edge case
    // new_inclusive(0.0, 1.0) can produce exactly 1.0
    // new(0.0, 1.0) cannot produce 1.0
    // This matters for probabilities and percentages
}

The most common mistake is using the wrong range type for the problem domain.

Integer Equivalence

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn integer_equivalence() {
    // For integers, these are equivalent:
    
    // Range [a, b] inclusive:
    let inclusive = Uniform::new_inclusive(0, 9);
    
    // Range [a, b+1) exclusive:
    let exclusive = Uniform::new(0, 10);
    
    // Both produce 0-9 for integers
    // Because new(0, 10) excludes 10, leaving 0-9
    
    // Use new_inclusive when you have the exact bounds
    // Use new when bounds match array/collection conventions
    
    // Example: Values 1-10
    let by_inclusive = Uniform::new_inclusive(1, 10);
    let by_exclusive = Uniform::new(1, 11);  // Need to add 1
    
    // new_inclusive(1, 10) is clearer: "values 1 through 10"
    // new(1, 11) is error-prone: need to remember +1
}

For integers, new_inclusive(a, b) and new(a, b+1) are equivalent but new_inclusive is clearer when bounds are natural.

Float Differences Matter

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn float_differences() {
    let mut rng = thread_rng();
    
    // For floats, new and new_inclusive are NOT equivalent!
    
    // new(0.0, 1.0): Can produce 0.999999... but NEVER 1.0
    let dist_exclusive = Uniform::new(0.0, 1.0);
    // Probability of exactly 0.0: very low but possible
    // Probability of exactly 1.0: ZERO (excluded)
    
    // new_inclusive(0.0, 1.0): Can produce 1.0
    let dist_inclusive = Uniform::new_inclusive(0.0, 1.0);
    // Probability of exactly 0.0: very low but possible
    // Probability of exactly 1.0: very low but possible
    
    // This matters for:
    // - Percentages: 0-100% should include 100%
    let percent = Uniform::new_inclusive(0.0, 100.0);
    
    // - Normalized coordinates: [0, 1] should include 1.0
    let normalized = Uniform::new_inclusive(0.0, 1.0);
    
    // - But for array indexing with floats, new(0.0, len as f64) may be appropriate
}

For floats, the difference between new and new_inclusive is semantically significant.

Sampling Multiple Values

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn sampling_multiple() {
    let mut rng = thread_rng();
    
    // Create distribution once, sample many times
    let die = Uniform::new_inclusive(1, 6);
    
    // Sample 10 dice rolls
    let rolls: Vec<i32> = (0..10)
        .map(|_| rng.sample(die.clone()))
        .collect();
    
    // Or use sample_iter for convenience
    let rolls2: Vec<i32> = rng.sample_iter(die.clone()).take(10).collect();
    
    // Check all values are valid
    for roll in &rolls2 {
        assert!(*roll >= 1 && *roll <= 6);
    }
    
    // The distribution is created once and reused
    // More efficient than creating new distribution each sample
}

Create the distribution once and sample multiple times for efficiency.

Distribution Creation Performance

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn performance() {
    // Both new and new_inclusive have similar creation cost
    // For integers: O(1) - simple range setup
    
    // For floats: Slightly more complex
    // Uses the algorithm from "Uniform Random Floating-Point Numbers"
    // by G. Marsaglia and W. W. Tsang
    
    // Once created, sampling is O(1)
    
    // Create distribution once for repeated sampling:
    let dist = Uniform::new_inclusive(1, 100);
    let mut rng = thread_rng();
    
    for _ in 0..1_000_000 {
        let value: i32 = rng.sample(dist.clone());
        // Use value...
    }
    
    // Don't create distribution in loop:
    // BAD:
    // for _ in 0..1_000_000 {
    //     let dist = Uniform::new_inclusive(1, 100);  // Wasteful!
    //     let value = rng.sample(dist);
    // }
}

Both methods have similar creation cost; reuse distributions across multiple samples.

The Standard Library Rng::gen_range

use rand::{thread_rng, Rng};
 
fn gen_range() {
    let mut rng = thread_rng();
    
    // Rng::gen_range uses Uniform internally
    // It supports both exclusive and inclusive ranges:
    
    // Exclusive: gen_range(low..high)
    let excl: i32 = rng.gen_range(0..10);  // 0-9
    
    // Inclusive: gen_range(low..=high)
    let incl: i32 = rng.gen_range(0..=10); // 0-10
    
    // These use Uniform::new and Uniform::new_inclusive under the hood
    
    // gen_range is convenient for one-off samples
    // Uniform is better for repeated sampling
    
    // One-off:
    let roll = rng.gen_range(1..=6);
    
    // Repeated:
    let die = Uniform::new_inclusive(1, 6);
    let rolls: Vec<_> = (0..100).map(|_| rng.sample(die.clone())).collect();
}

Rng::gen_range provides a convenient interface using .. and ..= syntax matching Uniform::new and new_inclusive.

Range Types and Bounds Checking

use rand::distributions::Uniform;
use rand::distributions::uniform::UniformInt;
 
fn bounds_checking() {
    // Both methods panic on invalid bounds:
    
    // Empty range: low >= high for new
    // let bad = Uniform::new(10, 5);  // Panics: range is empty
    
    // Empty range: low > high for new_inclusive
    // let bad = Uniform::new_inclusive(10, 5);  // Panics
    
    // Single value range works for new_inclusive:
    let single = Uniform::new_inclusive(5, 5);  // Always returns 5
    
    // But not for new (empty range):
    // let bad = Uniform::new(5, 5);  // Panics: no values in range
    
    // This difference is important:
    // - new_inclusive(a, a) = always sample 'a'
    // - new(a, a) = empty range, panic
    
    // For integers:
    // - new requires low < high
    // - new_inclusive requires low <= high
    
    // For floats:
    // - Similar rules, but NaN causes panic
    // - Infinity handling depends on implementation
}

new panics if low >= high; new_inclusive panics if low > high, allowing single-value distributions.

Single-Value Distributions

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn single_value() {
    let mut rng = thread_rng();
    
    // new_inclusive allows single-value distributions
    let always_five = Uniform::new_inclusive(5, 5);
    let value: i32 = rng.sample(always_five);
    assert_eq!(value, 5);  // Always 5
    
    // Useful for:
    // - Constant parameters in generic code
    // - Placeholder distributions
    // - Testing with fixed values
    
    // new does NOT allow this:
    // let bad = Uniform::new(5, 5);  // Panics!
    
    // Workaround for new: add 1 to upper bound
    let almost_always_five = Uniform::new(5, 6);  // Can produce 5 only
    // But this is misleading - use new_inclusive instead
}

new_inclusive(a, a) creates a distribution that always returns a; new(a, a) panics.

Practical Decision Guide

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn decision_guide() {
    // Decision flow:
    
    // 1. Is the upper bound a valid outcome?
    //    YES -> Use new_inclusive
    //    NO -> Use new
    
    // 2. Is this for array indexing?
    //    YES -> Use new(0, len)
    //    NO -> Consider natural range
    
    // 3. What's the domain?
    //    - Game outcomes (dice, cards) -> new_inclusive
    //    - Array indices -> new
    //    - Percentages -> new_inclusive(0, 100) or new_inclusive(0.0, 100.0)
    //    - Coordinates -> depends on convention
    
    // 4. What range syntax matches the intent?
    //    - "from a up to b" -> new(a, b)
    //    - "from a through b" -> new_inclusive(a, b)
    
    // Quick reference:
    // | Use new | Use new_inclusive |
    // |---------|-------------------|
    // | Array indices | Dice rolls |
    // | Loop counters | Card values |
    // | Buffer sizes | Percentages |
    // | Offsets | Human ranges (1-N) |
    // | [0, n) | [a, b] |
}

Choose based on whether the domain naturally includes the upper bound.

Summary Table

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn summary() {
    // | Aspect | new(low, high) | new_inclusive(low, high) |
    // |--------|----------------|--------------------------|
    // | Range | [low, high) | [low, high] |
    // | Upper bound | Excluded | Included |
    // | Rust analog | low..high | low..=high |
    // | Valid bounds | low < high | low <= high |
    // | Single value | Panics | Allowed |
    // | Typical use | Array indices | Game outcomes |
    
    // Both produce uniform distributions with equal probability
    // Both work for integers and floats
    // Both have O(1) creation and sampling
    
    // Key insight: Match your mental model of the range
    // "0 to 9" (exclusive) -> new(0, 10)
    // "1 through 6" (inclusive) -> new_inclusive(1, 6)
}

Synthesis

Quick reference:

use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
 
fn quick_reference() {
    let mut rng = thread_rng();
    
    // new(low, high) - [low, high) - upper excluded
    let exclusive = Uniform::new(0, 10);       // Values: 0-9
    let index_dist = Uniform::new(0, arr.len()); // Array safe
    
    // new_inclusive(low, high) - [low, high] - upper included
    let inclusive = Uniform::new_inclusive(0, 10);  // Values: 0-10
    let die = Uniform::new_inclusive(1, 6);        // Standard die
    
    // Rule of thumb:
    // - "up to N" -> new(0, N)
    // - "through N" -> new_inclusive(0, N)
    // - Array index -> new(0, len)
    // - Game outcome -> new_inclusive(min, max)
}

Key insight: Uniform::new and Uniform::new_inclusive differ only in their handling of the upper bound—new creates a half-open range [low, high) where high can never be sampled, while new_inclusive creates a closed range [low, high] where high is a valid outcome with equal probability. The distinction mirrors Rust's range syntax (low..high vs low..=high), making the API intuitive for Rust programmers. The practical difference manifests in two common scenarios: for array indexing where valid indices are [0, len), new(0, len) correctly excludes the out-of-bounds index len; for game mechanics like dice where all faces [1, 6] should be possible, new_inclusive(1, 6) correctly includes the maximum value. For integers, new_inclusive(a, b) and new(a, b+1) are mathematically equivalent but convey different intent—new_inclusive is clearer when bounds are naturally inclusive, while new is clearer for zero-indexed collections. For floats, the difference is more significant: new(0.0, 1.0) can never produce exactly 1.0, while new_inclusive(0.0, 1.0) can, which matters for probabilities and percentages that should include the maximum value. The new_inclusive variant also allows single-value distributions (new_inclusive(a, a) always samples a), while new(a, a) panics as an empty range.