Reputation: 45
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
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
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
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