Skip to content

Commit 188307a

Browse files
author
Paul Dicker
committed
Allow sampling from a closed integer range
1 parent 2a59377 commit 188307a

1 file changed

Lines changed: 45 additions & 20 deletions

File tree

src/distributions/range2.rs

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -117,29 +117,49 @@ macro_rules! range_int_impl {
117117
}
118118

119119
impl RangeImpl for RangeInt<$ty> {
120-
// we play free and fast with unsigned vs signed here
120+
// We play free and fast with unsigned vs signed here
121121
// (when $ty is signed), but that's fine, since the
122122
// contract of this macro is for $ty and $unsigned to be
123-
// "bit-equal", so casting between them is a no-op & a
124-
// bijection.
123+
// "bit-equal", so casting between them is a no-op.
125124

126125
type X = $ty;
127126

128127
fn new(low: Self::X, high: Self::X) -> Self {
129-
let range = (w(high as $unsigned) - w(low as $unsigned)).0;
128+
new_closed(low, high - 1)
129+
}
130+
131+
fn new_closed(low: Self::X, high: Self::X) -> Self {
132+
// For a closed range the number of possible numbers we should
133+
// generate is `range = (high - low + 1)`. It is not possible to
134+
// end up with a uniform distribution if we map _all_ the random
135+
// integers that can be generated to this range. We have to map
136+
// integers from a `zone` that is a multiple of the range. The
137+
// rest of the integers, that cause a bias, are rejected. The
138+
// sampled number is `zone % range`.
139+
//
140+
// The problem with `range` is that to cover the full range of
141+
// the type, it has to store `unsigned_max + 1`, which can't be
142+
// represented. But a range of size 0 can't exist, and a
143+
// modulus op `unsigned_max + 1` is a no-op. So we treat this as
144+
// a special case. Wrapping arithmetic makes representing
145+
// `unsigned_max + 1` as 0 even simple.
146+
//
147+
// We don't calculate zone directly, but first calculate the
148+
// number of integers to reject first. With a wrikle to handle
149+
// `unsigned_max + 1` not fitting in the type, this is:
150+
// ints_to_reject = (unsigned_max + 1) % range;
151+
// ints_to_reject = (unsigned_max - range + 1) % range;
152+
130153
let unsigned_max: $unsigned = ::core::$unsigned::MAX;
131154

132-
// We want to calculate type_range % range where type_range is
133-
// pow(2, n_bits($ty)), but we can't represent type_range.
134-
// (type_range - range) % range is equivalent, since we know
135-
// type_range > range. Since range >= 1,
136-
// type_range - range = (unsigned_max - range) + 1.
137-
let ignore_zone = ((unsigned_max - range) + 1) % range;
138-
// We want to sample from the zone
139-
// [0, (type_range - ignore_zone))
140-
// however, ignore_zone may be zero. Instead use a closed range
141-
// from zero to:
142-
let zone = unsigned_max - ignore_zone;
155+
let range = (w(high as $unsigned) - w(low as $unsigned)).0;
156+
let ints_to_reject =
157+
if range > 0 {
158+
(unsigned_max - range + 1) % range
159+
} else {
160+
0
161+
};
162+
let zone = unsigned_max - ints_to_reject;
143163

144164
RangeInt {
145165
low: low,
@@ -148,16 +168,21 @@ macro_rules! range_int_impl {
148168
zone: zone as $ty
149169
}
150170
}
151-
171+
152172
fn sample<R: Rng+?Sized>(&self, rng: &mut R) -> Self::X {
153173
use $crate::distributions::uniform;
174+
let range = self.range as $unsigned;
154175
loop {
155176
let v: $unsigned = uniform(rng);
156-
// Reject samples not between 0 and zone:
157-
if v <= self.zone as $unsigned {
158-
// Adjustment sample for range and low value:
159-
return (w(self.low) + w((v % self.range as $unsigned) as $ty)).0;
177+
// The modulus operator is incredibly slow. Even skipping
178+
// it for the small chance `v` falls in the target range
179+
// makes it a few percent faster.
180+
if v <= range || self.range == 0 {
181+
return (w(self.low) + w(v as $ty)).0;
182+
} else if v <= self.zone as $unsigned {
183+
return (w(self.low) + w((v % range) as $ty)).0;
160184
}
185+
// Sample does not fall in `zone`, so reject it and retry.
161186
}
162187
}
163188
}

0 commit comments

Comments
 (0)