Very simple code for shuffling short lists.
The initial code is in Python, but is self-contained, uses no Python-specific features, no Python libraries, no floats, and stores no ints larger than 32 bits. Intermediate results only care about the last 32 bits. uint32_t suffices in C; in Python they're masked to the least-significant 32 bits; in JavaScript no intermediate is wider than 50 bits, so are computed exactly using IEEE-754 doubles.
The intent is that it be easy to recode in any language for any non-trivial platform, and generate identical results on all.
The driving use case is for "last resort" tie-breaking in election scoring systems. In those, a randomly permuted list of candidates can be established, and if there's no other way to break a tie, candidate C beats D if and only if C appears before D in the list. In Python:
import tinyrand
# `tiebreaker` is a list of candidates.
# This must be in a fixed (but arbitrary) order.
t = tinyrand.get(seed=some_integer_seed) # use default version
t.shuffle(tiebreaker)Speed isn't important here. Code simplicity, portability, and reproducibilty are. Quality of results isn't especially important either. If all permutations of lists of length <= 10 are about equally likely, "good enough".
- 0: 32-bit state, and no int larger than a 32-bit unsigned int is created. All implementations should support version 0.
tinyrand supplies a few constants and a factory function.
-
tinyrand.SUPPORTED_VERSIONSA tuple of supported version numbers (small integers) in sorted order.
-
tinyrand.DEFAULT_VERSIONThe version returned if a version isn't passed to
get(). -
tinyrand.get(version=tinyrand.DEFAULT_VERSION, seed=0)Return an object that can be used for shuffling.
versionis an integer in SUPPORTED_VERSIONS.seedis an integer >= 0 used to initialize the internal random number generator's state. Only the trailing bits are used - the state is typically no larger than 32 bits.It's intended that results be exactly reproducible "forever", across all implementations, for objects created with the same
versionandseed.
After t = tinyrand.get(...), t has one method of primary interest:
-
t.shuffle(xs)xsis a list, which is randomly permuted in place.Noneis returned, as is the usual case for Python's mutating functions.ValueErroris raised iflen(xs) > t.NSTATES. This can't be used for very long lists. In the context of elections, this isn't a real limitation.
Of less interest,
-
t.seed(seed)Sets
t's state to the same as if it had just been created by.get()with the sameseed.
It also has a few constants of minor interest:
-
t.VERSIONThe version argument passed to
.get(). -
t.BITSThe number of trailing bits used in a seed.
-
t.NSTATESThe total number of distinct seeds = 2BITS.
Create a version 0 object (function, whatever) seeded with 42. Then
get the first 10 outputs with the implementation's version of ._get().
The results must be:
2975584321 1915941066 2870739003 2578514367 4127658768 324409786 1476451891 1236111477 2281477571 616879951
Then (re)seed it with 42, and try again. It should deliver the same 10 results.
In Python,
>>> t = tinyrand.get(0, 42)
>>> [t._get() for i in range(10)]
[2975584321, 1915941066, 2870739003, 2578514367, 4127658768,
324409786, 1476451891, 1236111477, 2281477571, 616879951]
>>> t.seed(42)
>>> [t._get() for i in range(10)]
[2975584321, 1915941066, 2870739003, 2578514367, 4127658768,
324409786, 1476451891, 1236111477, 2281477571, 616879951]You can also check these permutations:
def drive(version, SEED):
letters = list('abcdefghijklmnopqrstuvwxyz')
assert len(letters) == 26
assert len(set(letters)) == 26
t = tinyrand.get(version, SEED)
def getone():
xs = letters.copy()
t.shuffle(xs)
return ''.join(xs)
for _ in range(10):
print(getone())
drive(0. 42)displays:
yojnpldsihgubxteczkqrfmwva
ivdleyxqpkhfmbcszwrugtjnao
utcmjevqlxwyazbsdknpohfirg
rsbxdtcmepiqflyhzognjkvuwa
vcmgphafiznqxlejuktdsorwby
pelsocnbhativxrfkwdjugmqzy
hukoawzxjtbmdipgfrelcynqvs
yiltkvxpfmcuoghjazqerdbwns
gkniojhfsamturzxwebycqvpdl
tfdykvzomnbuhplgcisejxqraw