feat: add unbiased integer range helpers
Expand range sampling from the original u32-only helper to all primitive integer widths. The new gen_range_<int> methods cover 0..n sampling, while matching gen_range_<int>_in methods accept standard RangeBounds forms such as exclusive, inclusive, open-ended, and full-width ranges. Use rejection sampling against the matching primitive reader for each integer width. That keeps modulo bias out of the public helpers without introducing a general RNG trait or depending on rand. Full-width ranges fall back directly to the primitive reader because their span cannot be represented in the same integer type. Document the generated APIs with doctested examples and add a ranges example that demonstrates unsigned, signed, usize, and full-width sampling. Test Plan: - cargo test - cargo clippy - cargo clippy --benches - cargo clippy --tests - cargo +nightly fmt Refs: IDEAS.md ergonomics backlog
This commit is contained in:
@@ -11,6 +11,8 @@ fn main() -> std::io::Result<()> {
|
|||||||
println!("i32 : {}", rng.get_i32()?);
|
println!("i32 : {}", rng.get_i32()?);
|
||||||
|
|
||||||
println!("dice 1-6 : {}", rng.gen_range_u32(6)? + 1);
|
println!("dice 1-6 : {}", rng.gen_range_u32(6)? + 1);
|
||||||
|
println!("offset : {}", rng.gen_range_i32_in(-10..=10)?);
|
||||||
|
println!("index : {}", rng.gen_range_usize_in(0..16)?);
|
||||||
|
|
||||||
let token = rng.string_from(charset::ALPHANUMERIC, 24)?;
|
let token = rng.string_from(charset::ALPHANUMERIC, 24)?;
|
||||||
println!("token : {token}");
|
println!("token : {token}");
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//! Run with: `cargo run --example ranges`
|
||||||
|
|
||||||
|
use ez_urandom::OsRandom;
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
let mut rng = OsRandom::try_new()?;
|
||||||
|
|
||||||
|
let byte = rng.gen_range_u8(10)?;
|
||||||
|
let port = rng.gen_range_u16_in(49152..=65535)?;
|
||||||
|
let index = rng.gen_range_usize_in(0..8)?;
|
||||||
|
let offset = rng.gen_range_i32_in(-10..=10)?;
|
||||||
|
let full_width = rng.gen_range_i128_in(..)?;
|
||||||
|
|
||||||
|
println!("u8 below 10 : {byte}");
|
||||||
|
println!("ephemeral port : {port}");
|
||||||
|
println!("usize index : {index}");
|
||||||
|
println!("signed offset : {offset}");
|
||||||
|
println!("full-width i128 : {full_width}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+240
-22
@@ -19,6 +19,7 @@ use std::{
|
|||||||
fs::File,
|
fs::File,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
mem::size_of,
|
mem::size_of,
|
||||||
|
ops::{Bound, RangeBounds},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ::paste::paste;
|
use ::paste::paste;
|
||||||
@@ -104,6 +105,179 @@ macro_rules! os_random_get_integer_impls {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! os_random_unsigned_range_impls {
|
||||||
|
($(($t:ty, $method:ident, $range_method:ident, $below_method:ident, $get_method:ident)),*) => {
|
||||||
|
$(
|
||||||
|
/// Returns a uniformly distributed integer in `0..n`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if `n == 0`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn main() -> std::io::Result<()> {
|
||||||
|
/// let mut rng = ez_urandom::OsRandom::try_new()?;
|
||||||
|
#[doc = concat!("let value = rng.", stringify!($method), "(10)?;")]
|
||||||
|
/// assert!(value < 10);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn $method(&mut self, n: $t) -> io::Result<$t> {
|
||||||
|
assert!(n > 0, "n must be greater than zero");
|
||||||
|
self.$below_method(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a uniformly distributed integer from `range`.
|
||||||
|
///
|
||||||
|
/// The range may use any [`RangeBounds`] form, including `a..b`,
|
||||||
|
/// `a..=b`, `..b`, `..=b`, `a..`, and `..`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if the range is empty or if an excluded bound overflows
|
||||||
|
/// the integer type.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn main() -> std::io::Result<()> {
|
||||||
|
/// let mut rng = ez_urandom::OsRandom::try_new()?;
|
||||||
|
#[doc = concat!("let value = rng.", stringify!($range_method), "(4..=9)?;")]
|
||||||
|
/// assert!((4..=9).contains(&value));
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn $range_method<R>(&mut self, range: R) -> io::Result<$t>
|
||||||
|
where
|
||||||
|
R: RangeBounds<$t>,
|
||||||
|
{
|
||||||
|
let start = match range.start_bound() {
|
||||||
|
Bound::Included(&start) => start,
|
||||||
|
Bound::Excluded(&start) => start
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("excluded range start must not overflow"),
|
||||||
|
Bound::Unbounded => <$t>::MIN,
|
||||||
|
};
|
||||||
|
let end = match range.end_bound() {
|
||||||
|
Bound::Included(&end) => end,
|
||||||
|
Bound::Excluded(&end) => end
|
||||||
|
.checked_sub(1)
|
||||||
|
.expect("excluded range end must not overflow"),
|
||||||
|
Bound::Unbounded => <$t>::MAX,
|
||||||
|
};
|
||||||
|
assert!(start <= end, "range must not be empty");
|
||||||
|
|
||||||
|
let span = end.wrapping_sub(start).wrapping_add(1);
|
||||||
|
if span == 0 {
|
||||||
|
return self.$get_method();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(start.wrapping_add(self.$below_method(span)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn $below_method(&mut self, n: $t) -> io::Result<$t> {
|
||||||
|
let threshold = (0 as $t).wrapping_sub(n) % n;
|
||||||
|
loop {
|
||||||
|
let value = self.$get_method()?;
|
||||||
|
if value >= threshold {
|
||||||
|
return Ok(value % n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! os_random_signed_range_impls {
|
||||||
|
($(($t:ty, $u:ty, $method:ident, $range_method:ident, $below_method:ident, $get_method:ident)),*) => {
|
||||||
|
$(
|
||||||
|
/// Returns a uniformly distributed integer in `0..n`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if `n <= 0`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn main() -> std::io::Result<()> {
|
||||||
|
/// let mut rng = ez_urandom::OsRandom::try_new()?;
|
||||||
|
#[doc = concat!("let value = rng.", stringify!($method), "(10)?;")]
|
||||||
|
/// assert!((0..10).contains(&value));
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn $method(&mut self, n: $t) -> io::Result<$t> {
|
||||||
|
assert!(n > 0, "n must be greater than zero");
|
||||||
|
self.$range_method(0..n)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a uniformly distributed integer from `range`.
|
||||||
|
///
|
||||||
|
/// The range may use any [`RangeBounds`] form, including `a..b`,
|
||||||
|
/// `a..=b`, `..b`, `..=b`, `a..`, and `..`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if the range is empty or if an excluded bound overflows
|
||||||
|
/// the integer type.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # fn main() -> std::io::Result<()> {
|
||||||
|
/// let mut rng = ez_urandom::OsRandom::try_new()?;
|
||||||
|
#[doc = concat!("let value = rng.", stringify!($range_method), "(-4..=4)?;")]
|
||||||
|
/// assert!((-4..=4).contains(&value));
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn $range_method<R>(&mut self, range: R) -> io::Result<$t>
|
||||||
|
where
|
||||||
|
R: RangeBounds<$t>,
|
||||||
|
{
|
||||||
|
const SIGN_BIT: $u = (<$u>::MAX >> 1) + 1;
|
||||||
|
|
||||||
|
let start = match range.start_bound() {
|
||||||
|
Bound::Included(&start) => start,
|
||||||
|
Bound::Excluded(&start) => start
|
||||||
|
.checked_add(1)
|
||||||
|
.expect("excluded range start must not overflow"),
|
||||||
|
Bound::Unbounded => <$t>::MIN,
|
||||||
|
};
|
||||||
|
let end = match range.end_bound() {
|
||||||
|
Bound::Included(&end) => end,
|
||||||
|
Bound::Excluded(&end) => end
|
||||||
|
.checked_sub(1)
|
||||||
|
.expect("excluded range end must not overflow"),
|
||||||
|
Bound::Unbounded => <$t>::MAX,
|
||||||
|
};
|
||||||
|
assert!(start <= end, "range must not be empty");
|
||||||
|
|
||||||
|
let start = start.cast_unsigned() ^ SIGN_BIT;
|
||||||
|
let end = end.cast_unsigned() ^ SIGN_BIT;
|
||||||
|
let span = end.wrapping_sub(start).wrapping_add(1);
|
||||||
|
if span == 0 {
|
||||||
|
return self.$get_method();
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = start.wrapping_add(self.$below_method(span)?) ^ SIGN_BIT;
|
||||||
|
Ok(value.cast_signed())
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructor and primitive integer readers.
|
/// Constructor and primitive integer readers.
|
||||||
///
|
///
|
||||||
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
|
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
|
||||||
@@ -178,29 +352,73 @@ impl Default for OsRandom {
|
|||||||
|
|
||||||
/// Uniform sampling helpers.
|
/// Uniform sampling helpers.
|
||||||
///
|
///
|
||||||
/// All methods here use rejection sampling on top of [`OsRandom::get_u32`]
|
/// Range methods use rejection sampling on top of the matching primitive
|
||||||
/// so the result is exactly uniform — no modulo bias.
|
/// integer reader, so bounded results are exactly uniform — no modulo bias.
|
||||||
impl OsRandom {
|
impl OsRandom {
|
||||||
/// Returns a uniformly distributed integer in `0..n`.
|
os_random_unsigned_range_impls!(
|
||||||
///
|
(u8, gen_range_u8, gen_range_u8_in, gen_below_u8, get_u8),
|
||||||
/// # Panics
|
(u16, gen_range_u16, gen_range_u16_in, gen_below_u16, get_u16),
|
||||||
/// Panics if `n == 0`.
|
(u32, gen_range_u32, gen_range_u32_in, gen_below_u32, get_u32),
|
||||||
///
|
(u64, gen_range_u64, gen_range_u64_in, gen_below_u64, get_u64),
|
||||||
/// # Errors
|
(
|
||||||
/// Returns any I/O error produced while reading from `/dev/urandom`.
|
u128,
|
||||||
pub fn gen_range_u32(&mut self, n: u32) -> io::Result<u32> {
|
gen_range_u128,
|
||||||
assert!(n > 0, "n must be greater than zero");
|
gen_range_u128_in,
|
||||||
let n64 = u64::from(n);
|
gen_below_u128,
|
||||||
// Largest multiple of n that fits in 2^32. Values at or above the
|
get_u128
|
||||||
// cutoff would skew the modulo, so we discard and resample.
|
),
|
||||||
let cutoff = (1u64 << 32) - ((1u64 << 32) % n64);
|
(
|
||||||
loop {
|
usize,
|
||||||
let v = u64::from(self.get_u32()?);
|
gen_range_usize,
|
||||||
if v < cutoff {
|
gen_range_usize_in,
|
||||||
return Ok(u32::try_from(v % n64).expect("v % n is bounded by n which fits in u32"));
|
gen_below_usize,
|
||||||
}
|
get_usize
|
||||||
}
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
|
os_random_signed_range_impls!(
|
||||||
|
(i8, u8, gen_range_i8, gen_range_i8_in, gen_below_u8, get_i8),
|
||||||
|
(
|
||||||
|
i16,
|
||||||
|
u16,
|
||||||
|
gen_range_i16,
|
||||||
|
gen_range_i16_in,
|
||||||
|
gen_below_u16,
|
||||||
|
get_i16
|
||||||
|
),
|
||||||
|
(
|
||||||
|
i32,
|
||||||
|
u32,
|
||||||
|
gen_range_i32,
|
||||||
|
gen_range_i32_in,
|
||||||
|
gen_below_u32,
|
||||||
|
get_i32
|
||||||
|
),
|
||||||
|
(
|
||||||
|
i64,
|
||||||
|
u64,
|
||||||
|
gen_range_i64,
|
||||||
|
gen_range_i64_in,
|
||||||
|
gen_below_u64,
|
||||||
|
get_i64
|
||||||
|
),
|
||||||
|
(
|
||||||
|
i128,
|
||||||
|
u128,
|
||||||
|
gen_range_i128,
|
||||||
|
gen_range_i128_in,
|
||||||
|
gen_below_u128,
|
||||||
|
get_i128
|
||||||
|
),
|
||||||
|
(
|
||||||
|
isize,
|
||||||
|
usize,
|
||||||
|
gen_range_isize,
|
||||||
|
gen_range_isize_in,
|
||||||
|
gen_below_usize,
|
||||||
|
get_isize
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/// Returns a uniformly chosen byte from `set`.
|
/// Returns a uniformly chosen byte from `set`.
|
||||||
///
|
///
|
||||||
|
|||||||
Reference in New Issue
Block a user