Kreisquadratur
Kreisquadratur

Reputation: 999

Clojure: Deep-merge hash maps with dot-separated string keys

I'm looking for a function to achieve the following example result:

{"foo1"      "baz"
 "foo2.bar"  "baz"
 "foo2.bar2" "baz"
 "foo3_bar"  "baz"}
=>
{:foo1 "baz"
 :foo2 {:bar  "baz"
        :bar2 "baz"}
 :foo3 {:bar  "baz"}}

As one can see, it's a bit different from a classic deep-merge as the keys have to be keywordized first in a way that dot- and underscore postfixes are converted to hash maps (instead of the usual #[_\.]=> -).

Upvotes: 0

Views: 828

Answers (3)

Kreisquadratur
Kreisquadratur

Reputation: 999

With inspiration from @lgrapenthin I came up for this solution. It is on the upside short and concise and on the downside expensive (which is not to bad for my use case) and the overwriting strategy is determined by Clojure's hash map sorting (aka for user's undetermined):

(defn- deep-merge [& maps]
  (if (every? map? maps)
    (apply merge-with deep-merge maps)
    (last maps)))

(defn- str-keys-to-map [[k v]]
  (let [ks (map keyword (filter not-empty (string/split k #"[\._]")))]
    (when-not (empty? ks) (assoc-in {} ks v))))

(defn deep-keywordize-keys [m]
  (->> m (map str-keys-to-map) (apply deep-merge)))

Upvotes: 0

cotarmanach
cotarmanach

Reputation: 116

You could use a function like this one. Please note that it could be optimized to do tail recursion.

        (defn deep-hashmap-merge
          [ m ]
          (let
            [
            tget (fn [r k d]
                    (let
                      [ t (get r k d)]
                      (if (associative? t) t d))) 
            get-keylist-value (fn [r [k & ks] kv]
                              (if (nil? ks)
                                (assoc r k kv)
                                (assoc r k (get-keylist-value (tget r k {}) ks kv))))     
            ]
              (reduce #(get-keylist-value %1 (map keyword (clojure.string/split (first %2) #"[_\.]")) ( second %2)) {} m)
            )
          )

And the output would then be :

              user=> (deep-hashmap-merge 
              #_=> {"foo"      "baz"
              #_=>  "foo.bar"  "baz"
              #_=>  "foo.bar2" "baz"
              #_=>  "foo2_bar" "baz"})

              {:foo {:bar "baz", :bar2 "baz"}, :foo2 {:bar "baz"}}

Upvotes: -1

Leon Grapenthin
Leon Grapenthin

Reputation: 9266

(defn parse-keys-and-merge
  [hm]
  (reduce-kv (fn [hm k v]
               (assoc-in hm (map keyword (clojure.string/split k #"[\._]"))
                         (if (map? v)
                           (parse-keys-and-merge v)
                           v)))
             {} hm))

This does not work for your hash-map because your hash-map does not clarify whether the entry for :foo should be "baz" or {:bar "baz", :bar2 "baz"}. With a fixed hash-map it works:

(parse-keys-and-merge {"foo2_bar" "baz", "foo.bar2" "baz", "foo.bar" "baz"})
;; {:foo {:bar "baz", :bar2 "baz"}, :foo2 {:bar "baz"}}

Upvotes: 2

Related Questions