MBI Generation/Validation with clojure.spec

Based on something at work I was looking at the new Medicare identifier (MBI) that the CMS is rolling out starting next year. It seemed like a handy way to do some exercising of the generative testing capabilities of clojure.spec.

The MBI is an 11 character alpha-numeric string The full specification (pdf) shows all the details but the relevant part of the spec looks like this:

There are 4 different types of letter/number combinations that fit into each of the 11 slots. I’ll use the brand-spanking new 1.9 release of clojure with a few requires.

(ns specking.core
  (:require [clojure.spec.alpha :as s]
            [clojure.set :refer [difference]]
            [clojure.spec.gen.alpha :as gen]))

Now let’s just define those 4 letter/number types as specs (along with a simple char-range convenience function).

(defn char-range [start end]
  (set (map char (range start (inc end)))))

(s/def ::C (char-range 49 57)) ; 49 is \1 and 57 is \9

(s/def ::N (char-range 48 57)) ; 48 is \0

(def excluded-alphas #{\S \L \O \I \B \Z})

(s/def ::A (difference (char-range 65 90) ; 65 is \A and 90 is \Z
                       excluded-alphas))

(s/def ::AN (s/or :A ::A :N ::N))

With these definitions we can s/def an ::MBI spec

(s/def ::MBI (s/cat :pos1 ::C :pos2 ::A :pos3 ::AN
                    :pos4 ::N :pos5 ::A :pos6 ::AN
                    :pos7 ::N :pos8 ::A :pos9 ::A
                    :pos10 ::N :pos11 ::N))

I created a key for each position labeled with its ordinal value. With that I can test a string for validity like this

user> (s/valid? :specking.core/MBI (seq "6KX8DM0RD51"))
true

I need to add a call to seq for the string so that it will be split into a sequence of characters. What happens if I try to call an invalid MBI? I’ll add an extra 3 at the end so it is now a 12 character string.

user> (s/valid? :specking.core/MBI (seq "6KX8DM0RD513"))
false
user> (s/explain :specking.core/MBI (seq "6KX8DM0RD513"))
In: [11] val: (\3) fails spec: :specking.core/MBI predicate: 
(cat :pos1 :specking.core/C :pos2 :specking.core/A :pos3 :specking.core/AN 
:pos4 :specking.core/N :pos5 :specking.core/A :pos6 :specking.core/AN 
:pos7 :specking.core/N :pos8 :specking.core/A :pos9 :specking.core/A 
:pos10 :specking.core/N :pos11 :specking.core/N),  Extra input
nil

Note the call to s/valid? returns false. I can also use s/explain to get a detailed output as to what it wrong. Let’s create an MBI with an invalid character at position 9

user> (s/explain :specking.core/MBI (seq "1JK8E17YZ57"))
In: [8] val: \Z fails spec: :specking.core/A at: 
[:pos9] predicate: (difference (char-range 65 90) excluded-alphas)

The Z character is not allowed. You can see it tells me exactly where the error is and how it failed. The s/explain-data function returns the same as a parseable hashmap.

But there’s more! I can also use the clojure.spec.gen.alpha functions to create random MBIs for testing. It couldn’t be much simpler in this case.

user> (apply str (gen/generate (s/gen :specking.core/MBI)))
"1KV9NP2QF07"
;Let's get 10 of 'em
user> (repeatedly 10 #(apply str (gen/generate (s/gen :specking.core/MBI))))
("7VU5NJ2NX58" "5G34N68CR83" "3N65Q63TF72" "6R66FN3MD95" 
"2QA8PP9FR01" "8NN5H49AQ17" "1T17DH1AX91" "5T61GU2MC64" 
"2T16D99CA09" "1W47X41PY58")

The s/gen function returns a generator for the specified spec. The gen/generate function uses that generator to output a valid example.

I won’t do it here but with clojure.spec you’d also be able to write functions that require an MBI parameter or return a valid MBI as output. Pretty cool!

Hi Stan,
This is interesting but I have a couple of MBI questions that I don’t know the answers to. 1) How ‘dense’ is the MBI address space? If you have a valid MBI and you substitute a ‘5’ where it currently has a ‘7’ is there a chance that you have just created a different but still valid MBI? 2) If an MBI becomes compromised, e.g. in an identity theft situation, is there a way to invalidate it and get a different one?

In that case, the MBI with the 5 is valid in the clojure.spec sense since a call to s/valid? would return true. Now it’s a separate question as to whether the new MBI with a 5 is an already issued one. It is possible, but I would think the chances are pretty small. The CMS claims that MBIs have no “intelligence” built into them. In other words, they are truly random. The address space given this specification is gargantuan. Roughly 10^13 if my back of the envelope calculations are correct.

That I cannot say. Presumably.