Is SecureRandom.choose the wrong name for generating randomized strings with Ruby?
I frequently need to generate a short, random string:
- I need a non-numeric, non-enumerable url ID. e.g.
https://example.com/resources/TOKEN
- I need to generate a short, human-readable random activation token and I donβt want it to contain similar-looking characters like
1I
orO0
- I want to do something fun with strings of emojis.
Iβve been copy-pasting this snippet around for years:
def random_string(alphabet:, length:)
Array.new(length) { alphabet.chars[rand(alphabet.chars.size)] }.join
end
random_string(alphabet: "AaBbCc123", length: 7)
# => "C113A11", "Abcc3B2", "33cabbC", etc.
random_string(alphabet: "ππππ€ͺπ", length: 3)
# => "πππ", "ππ€ͺπ€ͺ", "πππ", etc.
The gems friendly_id
and uniquify
can do this too.
There was recently a discussion on Ruby on Rails Link Slack about generating random strings, and I went looking to see if there was a better implementation.
I found SecureRandom.choose
, which is built into the Ruby standard library, and as part of the SecureRandom
module, should be fairly trustworthy. Looks perfect:
# SecureRandom.choose generates a string that randomly draws from a source array of characters.
#
# The argument _source_ specifies the array of characters from which to generate the string.
# The argument _n_ specifies the length, in characters, of the string to be generated.
#
# The result may contain whatever characters are in the source array.
#
# require 'securerandom'
#
# SecureRandom.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron"
# SecureRandom.choose([*'0'..'9'], 5) #=> "27309"
β¦but there is a problem: SecureRandom.choose
is a private method. The choose
method is used to implement the public SecureRandom.alphanumeric
method, but is not itself exposed publicly. I went back to the initial feature request and found the reason:
I feel the method name, SecureRandom.choose, doesnβt represent the behavior well.
Fair enough. Until the name is figured out, Iβll still be using it:
require 'securerandom'
SecureRandom.send :choose, "ππππ€ͺπ".chars, 3
# => "πππ"