DiceDecide

How to Correctly Generate a Random Number That Skips Certain Values

Be the first to rate this page.

Consider a random number generator meant to draw from 1 through 100 while excluding the specific values 13 and 47 — perhaps because those numbers are reserved elsewhere in a system. A common but broken implementation draws a number from 1 to 100 and, whenever it lands on an excluded value, silently bumps the result up by one instead of drawing again: a draw of 13 becomes 14, and a draw of 47 becomes 48. This looks harmless because it only ever outputs valid numbers, but it quietly breaks uniformity: the value 14 is now produced whenever the underlying draw is naturally 14 or is bumped up from 13, giving it a 2 ÷ 100 = 2% chance — exactly double every other valid value’s correct 1 ÷ 98 ≈ 1.0204% share, and the same doubling happens to 48 from the 47-to-48 bump. The fix is rejection sampling: draw from the full 1–100 range, and if the result is 13 or 47, discard it and draw again rather than adjusting it. That gives every one of the 98 valid values an exactly equal 1 ÷ 98 ≈ 1.0204% chance, at a cost of an average of 100 ÷ 98 ≈ 1.0204 draws per valid result — barely more than one draw on average, for a 2-value exclusion out of 100.

A worked example with numbers

The expected number of draws needed scales predictably with how much of the range gets excluded: for a range of N with M values blocked, rejection sampling needs an average of N ÷ (N − M) draws per valid result. Excluding just 1 value from 100 needs about 1.0101 draws on average; excluding 10 needs about 1.1111; excluding 25 needs about 1.3333; excluding 50 needs exactly 2.0; and excluding 90 of the 100 values needs an average of 10 draws before landing on one of the 10 that remain valid. That last figure is a useful practical warning: once the excluded fraction gets large, rejection sampling still produces exactly correct, uniform odds among the remaining values, but it becomes increasingly inefficient, and an implementation relying on it should include a sane retry limit rather than assuming a valid draw always arrives quickly.

How to set the rule before the result

To check whether an existing implementation has the “bump the excluded value up” bug rather than genuine rejection sampling, generate several thousand values from it and tally the observed frequency of every valid output, paying particular attention to whichever value sits immediately after each excluded one — a roughly doubled count there, relative to every other valid value’s frequency, is the specific, distinctive signature this bug leaves behind, rather than a generic overall skew that a broader statistical test would be needed to catch. A second, independent implementation bug to check for separately is an unsorted or incorrectly cumulative shift-based remap: correctly mapping a fresh draw from 1–98 onto the 98 valid values from a 1–100 range with 13 and 47 excluded requires applying both exclusions in ascending order, and a draw landing between the two excluded values needs exactly one cumulative shift applied, not zero or two — getting this ordering wrong produces a different, subtler bias than the bump-up bug, but is still detectable with the same frequency-tally check.

Common mistakes that change the odds or the process

A common mistake is assuming rejection sampling is simply “wasteful” compared to a shift-based remap and switching to the remap without implementing it correctly; a correctly implemented remap is a legitimate, exact alternative to rejection sampling, but it requires the excluded values to be sorted and each cumulative shift applied precisely — an off-by-one in that logic produces the exact same kind of silent bias rejection sampling naturally avoids by construction. A second mistake is treating an excluded value that comes up mid-loop as an error condition to throw or return null on, rather than as a signal to simply loop again; that turns a routine, expected outcome — hitting an excluded value is not unusual, especially with a larger excluded set — into an unnecessary failure state instead of a normal part of the draw. A third mistake is assuming the exclusion set is always small and fixed; a system that needs to exclude a large, changing set of values on every draw, such as values already used elsewhere in the same session, is closer to the without-replacement, no-repeat picker mechanism covered elsewhere on this site than to this fixed-blocklist case, and benefits from a different implementation approach built around a shrinking valid-value list rather than repeated rejection against a static excluded set.

Where this method stops being appropriate

This method assumes the excluded set is known in advance and reasonably small relative to the full range; it does not describe a dynamically changing exclusion list that grows as values are used, which is a related but different no-repeat mechanism with its own separate guide on this site. Rejection sampling’s expected-draws cost becomes impractical, though never incorrect, as the excluded fraction approaches the size of the entire range — at that extreme, explicitly enumerating the small remaining list of valid values and picking directly from it is simpler and exactly as fair as continuing to reject against an ever-shrinking pool of acceptable draws.

How the random source fits into the rule

MDN’s Math.random reference documents the uniform random source both the broken bump-up method and the correct rejection-sampling method start from before either one applies its own exclusion logic; MDN’s Crypto.getRandomValues reference documents the cryptographically strong version of that same uniform source, appropriate whenever the excluded values matter for more than a casual use.

Expected draws needed, by how much of the range is excluded

Excluded values (M) out of 100Expected draws per valid result
11.0101
21.0204
51.0526
101.1111
251.3333
502.0000
9010.0000

Every figure comes from N ÷ (N − M) with N = 100. The cost stays close to 1 draw for a small exclusion and only becomes noticeable once a large fraction of the range is blocked — a rejection-sampling implementation does not need a retry cap for a small exclusion list, but should have one once the excluded fraction climbs much past the 50% row.

A second case: the bump-up bug’s exact numbers, worked through fully

With 1–100 and {13, 47} excluded, a bump-up implementation produces the value 14 whenever the underlying draw is naturally 14 (a 1-in-100 event) or is 13 and gets bumped (another 1-in-100 event), giving 14 a true 2-in-100 = 2% chance — exactly double the correct 1 ÷ 98 ≈ 1.0204% every other valid value should carry. The same doubling applies to 48 from the 47-to-48 bump. Checking that every value sums to 100%: 96 unaffected values at 1% each contribute 96%, plus 14 and 48 at 2% each contribute another 4%, totalling exactly 100% — the bug does not lose or invent probability, it just concentrates an extra 1 percentage point of it onto each of two specific, predictable values.

When to stop rejecting and just pick from a list instead

Once the excluded fraction climbs high enough — blocking 90 of 100 possible values, for instance — rejection sampling’s average of 10 draws per valid result, while still perfectly fair, starts to look wasteful next to the alternative of explicitly listing the 10 remaining valid values and picking directly and uniformly among them with a plain random-index selection. Both approaches give identical, correct odds; the crossover point where a direct list-based pick becomes clearly simpler than continued rejection is a practical implementation choice, not a fairness one, and depends mainly on how easy it is to enumerate the small remaining valid set in a given system.

Excluding a contiguous block of values is a much simpler special case

Excluding ten consecutive values, such as 40 through 49, from a 1–100 range costs the exact same 100 ÷ 90 ≈ 1.1111 expected draws as excluding any other 10 scattered values, since rejection sampling’s cost depends only on how many values are blocked, not on their arrangement. A shift-based remap, though, becomes noticeably simpler for a contiguous block than for scattered individual values: mapping a fresh draw from 1–90 onto the 90 valid values needs only one cumulative shift, applied once a candidate value reaches 40, rather than the several separately tracked shift points a scattered exclusion set like {13, 47} requires. Recognizing whether an exclusion set is contiguous or scattered before choosing an implementation approach can meaningfully simplify the remap method’s logic, even though it changes nothing about rejection sampling’s cost either way.

Related DiceDecide tools

Enter your values, review the result, then use it with confidence.

Rate this page

Be the first to rate this page.