Search code examples
ruby

How to generate a 16-digit robust not guessable coupon code?


I am looking for a robust ruby code to create 16 digit not guessable coupon codes.

I.e. AH9A-TE9A-443G-TGRW

There is a promising pseudocode at this answer written by Neil Slater

# Random, unguessable number as a base20 string
#  .rjust(12, '0') covers for unlikely, but possible small numbers
#  .reverse ensures we don't use first character (which may not take all values)
raw = SecureRandom.random_number( 2**80 ).to_s( 20 ).rjust(12, '0').reverse
# e.g. "3ecg4f2f3d2ei0236gi"


# Convert Ruby base 20 to better characters for user experience
long_code = raw.tr( '0123456789abcdefghij', '234679QWERTYUPADFGHX' )
# e.g. "6AUF7D4D6P4AH246QFH"


# Format the code for printing
short_code = long_code[0..3] + '-' + long_code[4..7] + '-' + long_code[8..11]
# e.g. "6AUF-7D4D-6P4A"

Now I need help to turn this pseudo-code into watertight code for my use-case. I cannot and do not want to use ruby gem to create the codes.


Solution

  • Ruby master (what may become 3.4) introduces a more robust formatting interface for SecureRandom. It's present in Ruby 3.3, but it does not appear to be fully baked. SecureRandom in 3.3.0 claims to have a bunch of Random::Formatter methods which Random::Formatter does not document. There is no guarantee it will continue to work in 3.3.x nor that 3.4 will work as currently documented in master. Use at your own risk. Unit test it to catch a breaking change quickly.

    That new method makes this very simple...

    COUPON_CHARS = '234679QWERTYUPADFGHX'.split(//)
    SecureRandom.alphanumeric(4, chars: COUPON_CHARS)
    

    To be compatible,we can adapt Ruby on Rails' base58 method for compatibility.

    ALPHANUMERIC = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a
    
    def secure_alphanumeric(n, chars: ALPHANUMERIC)
      alphanumeric_size = chars.size
    
      return SecureRandom.random_bytes(n).unpack("C*").map do |byte|
        idx = byte % 64
        idx = SecureRandom.random_number(alphanumeric_size) if idx >= alphanumeric_size
        chars[idx]
      end.join
    end
    

    Then use scan + join to add the dashes.

    ALPHABET = '234679QWERTYUPADFGHX'.split(//)
    
    puts secure_alphanumeric(16, chars: ALPHABET).scan(/.{4}/).join('-')