Reputation: 41
I'm trying to come up with a good way of applying specific transformation functions to a map of data.
Take the example map:
{:wrapper {:firstName "foo"
:lastName "bar"
:addressLine1 "line1"
:addressLine2 "line2"
:birthDate {:iso "1930-03-12"}}}
And transform it to:
{:name "foo bar"
:address "line1 /n line2"
:age 86}
I also want the transform to work the other way round, although I wouldn't mind writing a separate transform.
So far, I've tried writing a list of transformation functions: (pseudo)
(-> start-map
transform-name
transform-address
transform-age)
each transform taking [start-map {accumulator-map}]. I've also attempted writing a map containing the keys of the transformed map, and the transform functions (and arguments) as their values. I feel like I'm missing a trick.
Upvotes: 4
Views: 1787
Reputation: 11
zipmap, juxt and destructuring are pretty handy with map transformations.
(defn unwrap [{person :wrapper}]
(let [date-format (java.text.SimpleDateFormat. "yyyy-MM-dd")
name-fn #(str (:firstName %) " " (:lastName %))
address-fn #(str (:addressLine1 %) \newline (:addressLine1 %))
age-fn #(- (.getYear (java.util.Date.))
(.getYear (.parse date-format (get-in % [:birthDate :iso]))))]
(zipmap [:name :address :age]
((juxt name-fn address-fn age-fn) person))))
Upvotes: 1
Reputation: 29984
You have the basic idea right. Here is how I would do it:
(ns tst.clj.core
(:use clj.core
clojure.test))
(def data
{:firstName "foo"
:lastName "bar"
:addressLine1 "line1"
:addressLine2 "line2"
:birthDate {:iso "1930-03-12"}}
)
(def target
{:name "foo bar"
:address "line1\nline2"
; :age 86 ; left as an excercise to the reader :)
})
(defn transform-name [m]
{:name (str (:firstName m) " "
(:lastName m))})
(defn transform-addr [m]
{:address (str (:addressLine1 m) \newline
(:addressLine2 m))})
(defn transform-person-simple [m]
(merge (transform-name m)
(transform-addr m)))
; You could also use the obscure function `juxt`, although this is
; more likely to confuse people.
; See http://clojuredocs.org/clojure.core/juxt
(defn transform-person-juxt [m]
(let [tx-juxt (juxt transform-name transform-addr)
juxt-answers (tx-juxt m)
result (into {} juxt-answers) ]
result ))
(deftest t-tx
(is (= target (transform-person-simple data)))
(is (= target (transform-person-juxt data)))
)
with results:
> lein test
(:repositories detected in user-level profiles! [:user]
See https://github.com/technomancy/leiningen/wiki/Repeatability)
lein test tst.clj.core
Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
Upvotes: 2
Reputation: 34870
Transform:
(require '[clj-time.core :as t])
(require '[clj-time.format :as f])
(def data {:wrapper {:firstName "foo"
:lastName "bar"
:addressLine1 "line1"
:addressLine2 "line2"
:birthDate {:iso "1930-03-12"}}})
(def transformed
{:name (str (get-in data [:wrapper :firstName])
" "
(get-in data [:wrapper :lastName]))
:address (str (get-in data [:wrapper :addressLine1])
"\n"
(get-in data [:wrapper :addressLine2]))
:age (t/in-years (t/interval (f/parse
(get-in data [:wrapper :birthDate :iso] data))
(t/now)))})
Inverse transform. Note that the date lost precision.
(require '[clojure.string :as str])
(def untransformed
(let [[firstname lastname] (str/split
(:name transformed)
#" ")
[addressline1 addressline2] (str/split
(:address transformed)
#"\n")]
{:wrapper
{:firstName firstname
:lastName lastname
:addressLine1 addressline1
:addressLine2 addressline2
:birthDate
{:iso (f/unparse
(f/formatters :date)
(t/minus (t/now)
(t/years (:age transformed))))}}}))
Upvotes: 2
Reputation: 17849
To make it universal, i would make the transformation function that would select paths from source object, process the selected values to the map of path-in-target to value:
(defn transform [source target paths transformation]
(reduce (partial apply assoc-in)
target
(apply transformation
(map #(get-in source %) paths))))
then you could use it like this:
user> (def data {:wrapper {:firstName "foo"
:lastName "bar"
:addressLine1 "line1"
:addressLine2 "line2"
:birthDate {:iso "1930-03-12"}}})
#'user/data
user> (def data-2
(let [tr (partial transform data)]
(-> {}
(tr [[:wrapper :firstName] [:wrapper :lastName]]
(fn [f l] {[:name] (str f \space l)}))
(tr [[:wrapper :addressLine1] [:wrapper :addressLine2]]
(fn [a1 a2] {[:address] (str a1 \newline a2)}))
(tr [[:wrapper :birthDate :iso]]
(fn [d] {[:age] (reverse d)})))))
#'user/data-2
;;{:name "foo bar",
;; :address "line1\nline2",
;; :age (\2 \1 \- \3 \0 \- \0 \3 \9 \1)}
and vice versa:
user> (let [tr (partial transform data-2)]
(-> {}
(tr [[:name]]
(fn [n]
(let [[n1 n2] (clojure.string/split n #"\s")]
{[:wrapper :firstName] n1
[:wrapper :lastName] n2})))
(tr [[:address]]
(fn [a]
(let [[a1 a2] (clojure.string/split a #"\n")]
{[:wrapper :addressLine1] a1
[:wrapper :addressLine2] a2})))
(tr [[:age]]
(fn [a] {[:wrapper :birthDate :iso]
(apply str (reverse a))}))))
;;{:wrapper {:firstName "foo",
;; :lastName "bar",
;; :addressLine1 "line1",
;; :addressLine2 "line2",
;; :birthDate {:iso "1930-03-12"}}}
Upvotes: 0
Reputation: 1976
You can also define your mapping as a data structure with the keys and transformation functions you provided. E.g.
(def a->b
'[[:name (->fullname [:wrapper :firstName] [:wrapper :lastName])]
[:address [:wrapper :addressLine1]] ;; left as an exercise for the reader :)
[:age (->age [:wrapper :birthDate :iso])]])
where
(defn ->fullname [& s] (str/join " " s))
(defn ->age [s]
(let [now (Date.)
d (Date. s)]
(- (.getYear now) (.getYear d))))
then implement a function to do the transformation with your mapping rule and your source map:
(transform a->b {:wrapper {:firstName "foo"
:lastName "bar"
:addressLine1 "line1"
:addressLine2 "line2"
:birthDate {:iso "1930/03/12"}}})
=>
{:name "foo bar", :address "line1", :age 86}
A quick implementation can be like this:
(defn get-val [src s]
(if-let [v (or (get src s)
(get-in src s))]
v
(let [[f & ss] s
mf (resolve f)]
(apply mf (map (partial get-val src) ss)))))
(defn transform [m src]
(reduce (fn [ans [t s]]
(let [af (if (coll? t) assoc-in assoc)]
(af ans t (get-val src s))))
(empty src)
m))
Upvotes: 0