pntripathi9417
pntripathi9417

Reputation: 258

Clojure: How to merge vector of maps on the basis of index?

I am trying to merge vector of maps.

I tried doing it using the reduce method but unable to retrieve the expected result.

(def data '([{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}] [{:margin "40px"}]))

(reduce #(hash-map %1 %2) () data)

Input data:

(def data '([{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}] [{:margin "40px"}]))

(defn merge-data
  [data]
)

Expected Output:

(merge-data data)

({:padding-top "30px" :margin "40px"}
  {:padding-top "40px"}
  {:padding-top "50px"})

Coming from the JS background, I can easily do it using something like forEach and conditionals to build expected output. But how to do it in functional way?

SOLUTION:

I was able to solve this problem in the following way

(defn merge-styles
  [& args]
  (let [max-count (apply max (map #(count %1) args))
        items (map #(take max-count (concat %1 (repeat nil))) args)]
    (apply map merge items)))

The code snippet makes it much clearer and leaner. Thanks a lot for all the answers which helped me get up to this point.

Upvotes: 0

Views: 443

Answers (3)

Alan Thompson
Alan Thompson

Reputation: 29958

I would break it down into multiple simple steps like so:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(def data [[{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}]
           [{:margin "40px"}]])

; be clear about the 2 sequences we are combining
(def data-1 (first data))
(def data-2 (second data))

(defn do-merge []
  (let [N (max (count data-1) (count data-2))]
    (vec (for [i (range N)]
           (let [map-1 (get data-1 i)
                 map-2 (get data-2 i)] ; returns `nil` if no value present
             (merge map-1 map-2) ; if merge a map & nil, it's a noop
             )))))

(dotest
  (is= (do-merge)
    [{:padding-top "30px", :margin "40px"}
     {:padding-top "40px"}
     {:padding-top "50px"}] ) )

You could also pad the shorter sequence with nils, then use (map merge ...), but you would have to make sure you get the lengths just right first which would still involve count and max....not sure if that is simpler.

Upvotes: 0

Stefan Pfeiffer
Stefan Pfeiffer

Reputation: 111

(map into [{:a 1} {:c 3}] (concat [{:b 2}] (repeat nil))) yields ({:a 1, :b 2} {:c 3}).

map reads from both vectors and applies the respective indexes to into, merging the maps for each index. As map stops when one of the collections is exhausted, we need to concat nil values to the shorter collection.

Edit: As always, this has already been answered before: Using 'map' with different sized collections in clojure

Upvotes: 0

Denis Fuenzalida
Denis Fuenzalida

Reputation: 3346

Generally, you can just use map and merge to merge collections of hashmaps, but it will end merging when one of the collections is exhausted.

You can create a function like the following to "extend" the collections to have the same length, then merge as usual:

(defn merge-all
  "Merges two sequences of maps using merge. Consumes all entries."
  [xs ys]
  (let [n  (max (count xs) (count ys))
        xs (take n (concat xs (repeat nil)))
        ys (take n (concat ys (repeat nil)))]
    (map merge xs ys)))

(def data [[{:padding-top "30px"} {:padding-top "40px"} {:padding-top "50px"}]
           [{:margin "40px"}]])

;; (apply merge-all data)
;; => ({:padding-top "30px", :margin "40px"} {:padding-top "40px"} {:padding-top "50px"})

Note that in your example, you used a parenthesis around the data, but in Clojure this means you want to call it as if it were a function. In the example above I switched it to a [ and ] instead. Also, note that this function depends in the fact that you can actually count the collections that you pass to it (in Clojure you can have "infinite" collections, such as (range)).

Upvotes: 1

Related Questions