imcgaunn
imcgaunn

Reputation: 45

Restructuring Map in Clojure. Make corresponding values in current map into new map

I am wondering what the most idiomatic approach to accomplish the following goal is. I'm just getting started with Clojure and I'm struggling with finding strategies for manipulating data structures that don't rely on traditional iteration constructs (for, while, etc.)

If I have a map structure like the following:

(def test-map {:cat1-title "Title1", :cat1-val "Value1", :cat2-title "Title2", :cat2-val "Value2"})

And I would like to transform it into the following structure:

{"Title1" "Value1", "Title2" "Value2"}

Essentially, I would like to make a new map whose keys are the values of the *title keys, and the values are the values of the corresponding *value keys.

What would be the best clojure approach for doing this?

What I've attempted is the following (which I don't know if it will always work). It essentially extracts the *title values and then zips them with extracted *val values

(let [titles
  (vals (filter #(re-matches #".*-title"
                             (str (key %)))
                test-map))
  values
  (vals (filter #(re-matches #".*-val"
                             (str (key %)))
                test-map))]
  (zipmap titles values))

This successfully extracts the keys and values, but I'm not sure if zipping these together with zipmap is the best way, or most idiomatic way to combine them.

Upvotes: 2

Views: 216

Answers (3)

Alan Thompson
Alan Thompson

Reputation: 29958

I would answer this question similarly to Timothy, but for production code I think it is best to spread things out some and be a bit more explicit:

(ns clj.core
  (:require [clojure.string :as str] )
  (:use tupelo.core)) ; it->

(defn is-title-kw       [arg] (re-matches #".*-title" (name arg)))
(defn title-kw->val-kw  [arg] (it-> arg
                                    (name it)
                                    (str/replace it #"-title" "-val")
                                    (keyword it)))

(defn transform [map-arg]
  (let [title-kws (filter is-title-kw (keys map-arg)) ]
    (into {}
      (for [title-kw title-kws]
        (let [val-kw      (title-kw->val-kw title-kw)
              title-str   (title-kw map-arg)
              val-str     (val-kw   map-arg) ]
          {title-str val-str} )))))

And of course, some unit tests:

(ns tst.clj.core
  (:use clj.core 
        clojure.test 
        tupelo.core))

(def test-map   { :cat1-title "Title1", :cat1-val "Value1", 
                  :cat2-title "Title2", :cat2-val "Value2" } )

(deftest t-is-title-kw
  (is      (is-title-kw :cat1-title))
  (is      (is-title-kw :cat2-title))
  (is (not (is-title-kw :cat1-val)))
  (is (not (is-title-kw :cat2-val))))

(deftest t-title-kw->val-kw
  (is (= :cat1-val (title-kw->val-kw :cat1-title)))
  (is (= :cat2-val (title-kw->val-kw :cat2-title))))

(deftest t-transform
  (is (=  (transform test-map)
          { "Title1" "Value1", 
            "Title2" "Value2" } )))

Running the tests:

~/clj > lein test

lein test tst.clj.core

Ran 3 tests containing 7 assertions.
0 failures, 0 errors.

Upvotes: 2

leetwinski
leetwinski

Reputation: 17859

i would rather prefer using reduce-kv for that:

(defn transform [items-map]
  (reduce-kv (fn [result k v]
               (if-let [[_ name] (re-find #"^:(.+)-title$" (str k))]
                 (assoc result v (items-map (keyword (str name "-val"))))
                 result))
             {} items-map))

in repl:

user> (def test-map {:cat-1-title "Title1", :cat-1-val "Value1", 
                     :cat2-title "Title2", :cat2-val "Value2", 
                     :cat3-title "Title3", :cat3-val "Value3"})
#'user/test-map
user> (transform test-map)
{"Title1" "Value1", "Title2" "Value2", "Title3" "Value3"}

Upvotes: 2

Timothy Pratley
Timothy Pratley

Reputation: 10662

Zipmap will fail for larger maps because key/value pairs do not have a strict ordering inside a map. For small maps, they often follow the order you created them because small maps are created as PersistentArrayMaps. Large maps are PersistentHashMap. Observe the result of (apply hash-map (range 100)) vs (apply hash-map (range 10)) So if you have a bigger map, your -titles will not be aligned with your -vals

Unfortunately this means you really need to look up vals that match your titles explicitly. Here is one way to do that:

(defn transform [m]
  (into {} (for [[k v] m
                 :let [title (name k)]
                 :when (.endsWith title "-title")
                 :let [val-name (clojure.string/replace title #"-title$" "-val")]]
             [v  (m (keyword val-name))])))

For each key ending in title, look up the val with the same prefix, and put it all into a map.

Upvotes: 2

Related Questions