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:
+240
-22
@@ -19,6 +19,7 @@ use std::{
|
||||
fs::File,
|
||||
io::{self, Read},
|
||||
mem::size_of,
|
||||
ops::{Bound, RangeBounds},
|
||||
};
|
||||
|
||||
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.
|
||||
///
|
||||
/// Each `get_<int>` method reads exactly `size_of::<T>()` bytes from
|
||||
@@ -178,29 +352,73 @@ impl Default for OsRandom {
|
||||
|
||||
/// Uniform sampling helpers.
|
||||
///
|
||||
/// All methods here use rejection sampling on top of [`OsRandom::get_u32`]
|
||||
/// so the result is exactly uniform — no modulo bias.
|
||||
/// Range methods use rejection sampling on top of the matching primitive
|
||||
/// integer reader, so bounded results are exactly uniform — no modulo bias.
|
||||
impl OsRandom {
|
||||
/// 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`.
|
||||
pub fn gen_range_u32(&mut self, n: u32) -> io::Result<u32> {
|
||||
assert!(n > 0, "n must be greater than zero");
|
||||
let n64 = u64::from(n);
|
||||
// Largest multiple of n that fits in 2^32. Values at or above the
|
||||
// cutoff would skew the modulo, so we discard and resample.
|
||||
let cutoff = (1u64 << 32) - ((1u64 << 32) % n64);
|
||||
loop {
|
||||
let v = u64::from(self.get_u32()?);
|
||||
if v < cutoff {
|
||||
return Ok(u32::try_from(v % n64).expect("v % n is bounded by n which fits in u32"));
|
||||
}
|
||||
}
|
||||
}
|
||||
os_random_unsigned_range_impls!(
|
||||
(u8, gen_range_u8, gen_range_u8_in, gen_below_u8, get_u8),
|
||||
(u16, gen_range_u16, gen_range_u16_in, gen_below_u16, get_u16),
|
||||
(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),
|
||||
(
|
||||
u128,
|
||||
gen_range_u128,
|
||||
gen_range_u128_in,
|
||||
gen_below_u128,
|
||||
get_u128
|
||||
),
|
||||
(
|
||||
usize,
|
||||
gen_range_usize,
|
||||
gen_range_usize_in,
|
||||
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`.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user