I was working on the "Robot Name" exercise over on exercism.io (neat site, by the way), the gist of which is: Generate random, unique strings that match /[A-Za-z]{2}[0-9]{3}/
, e.g. "xY123", "Gv020", and so on. So there are 2,704,000 possible names out there.
Now, the exercise's tests do not do a full-on check for uniqueness or randomness, but making the names random and collision-free is something you're encouraged to try yourself. So I wanted to try that.
One option is of course to generate each and every name sequentially and shuffle
them. But that seems pretty inelegant.
So went looking for something cooler and came across this neat method for generating random, non-repeating 32 bit integers using "quadratic prime residue" (which sounds like something you'd find on a mathematician's shoes).
That method is really neat, but it makes use of C's strict 32-bit ints, and some XOR magic that I can't use for the smaller range I'm dealing with here.
So I modified it a little bit, and came up with what's below. I did a quick and dirty test, printing all the names to a file, and running checking for uniqueness thusly:
$ sort -u names.txt | wc -l
and indeed it claims that there are 2,704,000 names after sorting and removing duplicates. So yay for that. Distribution is probably skewed all sorts of ways, but it appears to work, and at least it's not sequential.
But I'm wondering if there's a simpler way that I'm just missing here. Or, failing that, if there are improvements that could be made to the thing below.
Note that the code is very much fixed to the 0..2,704,000 range. This is on purpose (hence the hard-coded prime number etc.). I'm not really interested in tweaking this particular code to make it more flexible in that regard. But if someone has an already-flexible approach that could replace it, that'd be neat.
require "singleton"
class RobotNameGenerator
include Singleton
# 2x fifty-two letters (A-Z and a-z), 3 digits = 2,704,000 combinations
BOUNDARY = 2_704_000
# Largest prime below BOUNDARY that also satisfies p % 4 == 3
PRIME = 2_703_983
# Another prime used as step
STEP = 23
# Initializes the instance, and sets a random offset (seed value)
def initialize
@offset = rand(0...BOUNDARY)
end
# Get a new pseudo-random name
def next
serial = next_serial_number
serial, digits = serial.divmod(1000)
serial, b = serial.divmod(52)
serial, a = serial.divmod(52)
"%s%s%03i" % [letter_for(a), letter_for(b), digits]
end
private
# Get the next serial number
def next_serial_number
serial = permute(permute(@offset % BOUNDARY))
@offset += STEP
serial
end
# Do a one-to-one permutation of a number
def permute(n)
return n if n > PRIME
residue = n**2 % PRIME
n <= PRIME / 2 ? residue : PRIME - residue
end
# Encode a 0-51 number to A-Z/a-z
def letter_for(n)
offset = n < 26 ? 'A'.ord : 'a'.ord
ascii = offset + (n % 26)
ascii.chr
end
end
next = last * prime % 2_704_000
should generate non-repeating sequence (when the modulo is not divisable by the prime) and you can convert the number to the string. I am no ruby expert and this is not a code-review but rathre SO-answer about how to do it. \$\endgroup\$