Generating Random Alphanumeric Codes in Clojure

Just a fun assortment of functional building blocks!

October 12, 2021

This post is really a survey of around 15 common clojure functions and how they are used to complete a somewhat arbitrary task. I've documented the "function palette" of each code snippet below. The code is loosely based on a similar snippet I recently encountered on a real project. I had never used the repeatedly function before and wanted to play around with it and the other mechanics involved. The "Bonus Task" below seemed like a natural extension beyond the scope of the original code. Enjoy!

The task: write a function in Clojure that generates randomized alphanumeric codes of specified length (A-Z, a-z, 0-9).

Step one: define the character space (palette: range int inc map char concat)

(defn- char-range [lo hi]
  (range (int lo) (inc (int hi))))

(def alpha-numeric
  (map char (concat (char-range \a \z)
                    (char-range \A \Z)
                    (char-range \0 \9))))

Demo:

user=> (apply str alpha-numeric)
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

Step two: Generate codes! (palette: rand-nth apply str take repeatedly)

(defn rand-alpha-numeric []
  (rand-nth alpha-numeric))

(defn generate-code []
  (apply str
         (take length
               (repeatedly rand-alpha-numeric))))

(NOTE: See yesterday's post about using apply vs. reduce here.)

Demo:

user=> (take 5 (repeatedly generate-code))
("yUpMw3D7" "6nn7rSy6" "LRqGVvee" "CqtWESyI" "XLrptrTN")

Task Complete!


Bonus Task: Generate UUID-like codes:

Step 0: Make generate-code more general-purpose (palette: #())

(defn generate-code [length pool]
  (apply str
         (take length
               (repeatedly #(rand-nth pool)))))

Demo:

user=> (take 5 (repeatedly #(generate-code 8 alpha-numeric)))
("RCQOgPnj" "cEMeuRP0" "opDRY4bo" "91dR9AoW" "YAOntGpp")

Step 1: Define the hexadecimal character space:

(def hex
  (map char (concat (char-range \a \f)
                    (char-range \0 \9))))

Demo:

user=> (apply str hex)
"abcdef0123456789"

Step 2: Generate the UUID! (palette: subs first second string/join)

(def uuid-group-substrings
  [[0 8]
   [8 12]
   [12 16]
   [16 20]
   [20 32]])

(defn uuid []
  (let [code   (generate-code 32 hex)
        groups (map #(subs code (first %) (second %))
                    uuid-group-substrings)]
    (string/join "-" groups)))

Demo:

user=> (take 5 (repeatedly uuid))
("fad14ce1-31fb-fc24-2ea0-727a39f4a30b"
 "429c472c-f80a-d118-c490-89d41a57650f"
 "cb03817e-12bf-28bb-da8e-6dd72cb41515"
 "49a8855e-731b-a255-24d9-4a59cd47625d"
 "7849d360-f4e5-eea2-8d07-582449e26b74")

It should be noted that UUIDs should really be thought of as groupings of raw bytes, which are most often represented visually using the above hexadecimal encoding. If we were serious about generating UUIDs we would work with raw bytes, not hex characters.

Also, while the above values do resemble UUIDs, they don't actually adhere to the official spec. But they are probably closest to UUID Version 4. We can approximate the "version 4" spec (somewhat humorously) by requiring the first character in the 3rd grouping to be a "4" (yes, all v4 UUIDs have a '4' in that position).

(def uuid4-marker-index 14) ; xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx
(def uuid4-marker "4")      ;               ^

(defn uuid4 []
  (let [uuid   (uuid)
        before (subs uuid 0 uuid4-marker-index)
        after  (subs uuid (inc uuid4-marker-index))]
    (str before uuid4-marker after)))

Demo (note the '4' characters in the column indicated below):

user=> (take 5 (repeatedly uuid4))
("4476d3fe-bab2-4f9c-af84-338d167b8af6"
 "2f4a6de8-7d6b-4fd1-1d97-70664b4588a9"
 "c1cb6ca1-8c85-4dcf-82a6-9b62f504f164"
 "e876f78c-6a67-48ad-4847-b55d8a8cea4a"
 "ccdd259f-873e-47a8-4a89-7ae8c201a4a4")
;               ^

I hope you enjoyed that little tour of Clojure's functional toolkit!

Here's the entire code snippet:

(ns alphanum.core
  (:require [clojure.string :as string]))

(defn- char-range [lo hi]
  (range (int lo) (inc (int hi))))

(def alpha-numeric
  (map char (concat (char-range \a \z)
                    (char-range \A \Z)
                    (char-range \0 \9))))

(def hex
  (map char (concat (char-range \a \f)
                    (char-range \0 \9))))

(defn generate-code [length pool]
  (apply str
         (take length
               (repeatedly #(rand-nth pool)))))

(def uuid-groups-substrings
  [[0 8]
   [8 12]
   [12 16]
   [16 20]
   [20 32]])

(defn uuid []
  (let [chars  (generate-code 32 hex)
        groups (map #(subs chars (first %) (second %))
                    uuid-groups-substrings)]
    (string/join "-" groups)))

(def uuid4-marker-index 14) ; abcdefab-cdef-4abc-...
(def uuid4-marker "4")      ;               ^

(defn uuid4 []
  (let [uuid   (uuid)
        before (subs uuid 0 uuid4-marker-index)
        after  (subs uuid (inc uuid4-marker-index))]
    (str before uuid4-marker after)))

-Michael Whatcott