Skip to content

Instantly share code, notes, and snippets.

@SneakyPeet
Last active August 8, 2017 11:14
Show Gist options
  • Save SneakyPeet/2a2ae8cc5ff03753d99594556e820e77 to your computer and use it in GitHub Desktop.
Save SneakyPeet/2a2ae8cc5ff03753d99594556e820e77 to your computer and use it in GitHub Desktop.
South African Id Number Validator
;; Validates an RSA Identity Number
;; compatible With Clojure and Clojurescript
;; Requires clj-time for clojure and cljs-time for clojurescript
;; Based on the validator by Evan Knowles
;; http://knowles.co.za/generating-south-african-id-numbers/
(ns rsa-id-number
(:require
[clojure.string :as string]
#?(:clj [clj-time.format :as time.format]
:cljs [cljs-time.format :as time.format])
#?(:clj [clj-time.core :as time]
:cljs [cljs-time.core :as time])))
#?(:clj (defn- parse-int [d] (-> d String/valueOf Integer/parseInt))
:cljs (defn- parse-int [d] (js/parseInt d)))
(defn- generate-luhn-digit
([input]
(generate-luhn-digit input 0 0))
([input total l-count]
(if (empty? input)
(-> total (* 9) (mod 10))
(let [multiple (-> l-count (mod 2) inc)
parsed-digit (->> input first parse-int)
digit-multiple (* multiple parsed-digit)
floored-multiple (-> digit-multiple (/ 10) Math/floor int)
multiple-mod (mod digit-multiple 10)
new-total (+ total (+ floored-multiple multiple-mod))
next-l-count (inc l-count)]
(recur (rest input) new-total next-l-count)))))
(defn- parse-section [a v]
(-> v (subs a (+ a 2)) parse-int))
(defn- between? [a b x] (and (>= x a) (<= x b)))
(def expected-date-formatter (time.format/formatter "yyMMdd"))
(def format-expected-date (partial time.format/unparse expected-date-formatter))
(defn ->expected-date [year month day]
(let [current-year (-> (time/now) time/year (mod 100))
prefixed-year (+ year (if (< year current-year) 2000 1900))]
(-> (time/date-time prefixed-year)
(time/plus (time/months (dec month)))
(time/plus (time/days (dec day)))
format-expected-date)))
(defn- validate-date-of-birth? [v]
(let [actual-date (subs v 0 6)
year (parse-section 0 v)
month (parse-section 2 v)
day (parse-section 4 v)]
(and (between? 1 12 month)
(between? 1 31 day)
(= actual-date (->expected-date year month day)))))
(defn valid? [v]
(boolean
(and
(string? v)
(re-matches #"[0-9]{13}" v)
(validate-date-of-birth? v)
(= (parse-int (last v)) (generate-luhn-digit (drop-last v))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment