The parser below is not designed for every single US address. I am parsing through a file where each line may or may not be an address. I am more focused on speed rather than robustness.
state-abbreviation-lookup
andstreet-suffix-lookup
are booth used for consistency (addresses will always be "ST" instead of "STREET" or "STR")find-last-occurence-index-of
searches through a list of string and finds the last string that matches a key in a hash-map. This is used withstate-abbreviation-lookup
andstreet-suffix-lookup
to find where in the address the state and or street is.split-string
removes commas, capitalizes everything and then splits on whitespace.
Next in the code is some checks for invalid values. I finally have to split the address into the appropriate parts: street
, city
, state
, zipcode
.
Overall, it feels like too much is going on, but I'm not sure how best to make it the most Clojure-like.
(def
^{:const true
:source "https://www.usps.com/send/official-abbreviations.htm"}
state-abbreviation-lookup
(hash-map
"AL" "AL"
"ALABAMA" "AL"
"AK" "AK"
"ALASKA" "AK"
;... removed for brevity
))
(def
^{:const true
:source "https://www.usps.com/send/official-abbreviations.htm"}
street-suffix-lookup
(hash-map
;... removed for brevity
"STREET" "ST"
"ST" "ST"
"STR" "ST"
"STRT" "ST"
;... removed for brevity
))
(defrecord UsAddress [street city state zipcode])
(letfn
[(find-last-occurence-index-of [lookup-map coll]
(last (keep-indexed #(when (contains? lookup-map %2) %1) coll)))
(split-string [s]
((comp #(clojure.string/split % #"\s+")
clojure.string/upper-case
#(clojure.string/replace % #"," ""))
s))]
(defn extract-us-address [s]
(let [parts (split-string s)]
(when (re-matches #"^\d{5}$" (last parts))
(let [state-index (find-last-occurence-index-of state-abbreviation-lookup parts)
street-index (find-last-occurence-index-of street-suffix-lookup parts)]
(when (and state-index street-index)
(UsAddress.
(clojure.string/join " " (take (inc street-index) parts))
(clojure.string/join " " (drop (inc street-index) (take state-index parts)))
(state-abbreviation-lookup (nth parts state-index))
(last parts))))))))
Tests:
(deftest extract-us-address-test
(testing "extract-us-address"
(testing "should determine if string is a US address"
(are [i o] (= (extract-us-address i) o)
"3526 HIGH ST SACRAMENTO CA 95838" (UsAddress. "3526 HIGH ST" "SACRAMENTO" "CA" "95838")
"3526 HIGH ST SACRAMENTO, CA 95838" (UsAddress. "3526 HIGH ST" "SACRAMENTO" "CA" "95838")
"3526 HIGH ST SACRAMENTO California 95838" (UsAddress. "3526 HIGH ST" "SACRAMENTO" "CA" "95838")
"3526 HIGH ST SACRAMENTO, California 95838" (UsAddress. "3526 HIGH ST" "SACRAMENTO" "CA" "95838")
"3526 high st sacramento ca 95838" (UsAddress. "3526 HIGH ST" "SACRAMENTO" "CA" "95838")
"3526 high st sacramento, ca 95838" (UsAddress. "3526 HIGH ST" "SACRAMENTO" "CA" "95838")
"3526 CALIFORNIA ST SACRAMENTO CA 95838" (UsAddress. "3526 CALIFORNIA ST" "SACRAMENTO" "CA" "95838")
"3526 CALIFORNIA ST SACRAMENTO, CA 95838" (UsAddress. "3526 CALIFORNIA ST" "SACRAMENTO" "CA" "95838")
"3526 AVENUE ST SACRAMENTO CA 95838" (UsAddress. "3526 AVENUE ST" "SACRAMENTO" "CA" "95838")
"3526 AVENUE ST SACRAMENTO, CA 95838" (UsAddress. "3526 AVENUE ST" "SACRAMENTO" "CA" "95838")
"California" nil
"CA" nil
"" nil
"SACRAMENTO" nil
"95838" nil
"3526 HIGH STREET" nil
"304 867 5309" nil))))