bigspace bigspace
bigspace bigspace

Reputation: 41

clojure - correct way to apply transformations to map of data?

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

Answers (5)

dhineshns
dhineshns

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

Alan Thompson
Alan Thompson

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

Michiel Borkent
Michiel Borkent

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

leetwinski
leetwinski

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

rmcv
rmcv

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

Related Questions