Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Parse query parameters

URLs can have optional query parameters. They are the key-value pairs that follow the path.

Write a function that takes a URL (represented as a string) and parses out the query parameters into a hashmap.

Notes:

  • The query string is the string containing the query parameters. It appears after the ? and before a #. Ex: https://lispcast.com/search?q=clojure#page2
  • The keys and values are URL Encoded. You can use java.net.URLDecoder/decode to decode them.
  • The key-value pairs are separated by &. Ex: a=1&b=2
  • Each key-value pair contains the key, followed by =, followed by the value. Ex: a=1

Bonus:

Query parameters can contain duplicate keys. Handle them gracefully.

(defn query-string [url]
(-> url
(clojure.string/split #"\?")
second
not-empty))
(defn key-value-strings [qs]
(clojure.string/split qs #"&"))
(defn key-value-pair [kvs]
(mapv #(java.net.URLDecoder/decode %) (clojure.string/split kvs #"=")))
(defn conj-multi [mp [k v]]
(cond
(string? (get mp k))
(update mp k #(vector % v))
(vector? (get mp k))
(update mp k conj v)
:else
(assoc mp k v)))
(defn parse-query-params [url]
(let [kvs (some->> url
(query-string)
(key-value-strings)
(mapv key-value-pair))]
(reduce conj-multi {} kvs)))
(defn- decode [val]
(java.net.URLDecoder/decode val "UTF-8"))
(decode "q=%23%5E%21%26*")
;; => "q=#^!&*"
(defn- decode-param-string
"Converts a string like 'a=b' to a vector ot two strings [a b]"
[param-string]
(let [name+val (str/split param-string #"=")]
(mapv decode name+val)))
(defn query-params
"Expects url as string and return a map with query string parameters."
[url]
(let [query-params-strings (some-> url
(str/split #"\?")
second
(str/split #"&"))
params (into {} (map decode-param-string query-params-strings))]
params))
(deftest test-query-params
(testing "no params"
(is (= {} (query-params "https://google.sk")))
(is (= {} (query-params "https://google.sk?"))))
(testing "simple params"
(is (= {"a" "b", "z" "y"} (query-params "https://google.sk?a=b&z=y"))))
(testing "params with special chars"
(is (= {"q" "#^!&*", "source" "hp"} (query-params "https://google.sk?q=%23%5E%21%26*&source=hp"))))
(testing "duplicate param"
(is (= {"a" "c", "z" "y"} (query-params "https://google.sk?a=b&z=y&a=c"))))
)
@KingCode
Copy link

KingCode commented May 7, 2020

With handling of duplicate and multiple-valued parameters and single pass-through, ideal for those million-parameter URLs ;)

(defn decode [s]
  (java.net.URLDecoder/decode s))

(defn query-params [url]
  (let [params-str (second (clojure.string/split url #"\?"))
        attrs-xf (comp (partition-by #(= \& %))
                       (remove #{[\&]})
                       (map #(apply str %))
                       (map #(clojure.string/split % #"="))
                       (map #(mapv decode %)))]
    (->> (sequence attrs-xf params-str) 
         (reduce (fn [m [k v]]
                   (update m k (fn [v x] 
                                 (cond
                                   (and (string? v) (= v x)) v
                                   (string? v) (conj [] v x)
                                   (nil? v) x 
                                   :else (conj v x))) v))
                 {}))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment