I’m currently building an app where I’ll be creating random discount codes in bulk. Probably up to 5,000 at a time.
The business rules are the discount codes should be 19 characters long and should be broken up in groups of 4 characters like this XXXX-XXXX-XXXX-XXXX
and they should only be composed of upper case letters and numbers.
Ideally, the entire charset should be CDEFGHJKLMNPQRTUVWXYZ23679
. The missing letters and numbers are to avoid situations where a user might not quickly be able to tell if it’s a 0 or O, 5 or S, etc… So I removed a bunch of problem characters.
I’m still quite new to Elixir but a first shot implementation I came up with (with some Googling, etc.) was this:
def random_code() do
charset = "CDEFGHJKLMNPQRTUVWXYZ23679" |> String.split("", trim: true)
random_chars = Enum.reduce((0..16), [], fn (_i, acc) ->
[Enum.random(charset) | acc]
end) |> Enum.join("")
group_a = String.slice(random_chars, 1..4)
group_b = String.slice(random_chars, 5..8)
group_c = String.slice(random_chars, 9..12)
group_d = String.slice(random_chars, 13..16)
"#{group_a}-#{group_b}-#{group_c}-#{group_d}"
end
On an i5 3.2ghz quad core running in Docker, that code was able to generate 5,000 codes in about 730ms (no DB writes). Not too shabby, but I’m not just looking to complete my app and ship it. I want to really learn Elixir and Erlang along the way, so I took some time looking up alternative solutions.
Here’s another implementation that was roughly 10x faster, which generated 5,000 codes in 70ms:
defp generate_strong_random_string(length, case \\ :upper) do
# TODO: limit this to charset: CDEFGHJKLMNPQRTUVWXYZ23679
:crypto.strong_rand_bytes(length)
|> Base.encode32(case: case)
|> binary_part(0, length)
end
def generate_random_discount_code() do
group_a = generate_strong_random_string(4)
group_b = generate_strong_random_string(4)
group_c = generate_strong_random_string(4)
group_d = generate_strong_random_string(4)
"#{group_a}-#{group_b}-#{group_c}-#{group_d}"
end
As you can see, I kind of hit a brick wall in how to implement a custom character set into the faster solution.
So my questions to you are:
- How can I use my custom character set?
- Is there a different / better way to do this?
Side note:
For benchmarking, I Googled around and found this:
defmodule TimeFrame do
defmacro execute(name, units \\ :microsecond, do: yield) do
quote do
start = System.monotonic_time(unquote(units))
result = unquote(yield)
time_spent = System.monotonic_time(unquote(units)) - start
IO.puts("Executed #{unquote(name)} in #{time_spent} #{unquote(units)}")
result
end
end
end
# And then you use it like this:
require TimeFrame
def random_codes() do
TimeFrame.execute "original" do
for x <- 0..4999 do
random_code()
end
end
TimeFrame.execute "improved" do
for x <- 0..4999 do
generate_random_discount_code()
end
end
end