clojure - 将转换应用于数据映射的正确方法?

时间:2016-10-20 22:21:52

标签: clojure transformation

我试图想出一种将特定转换函数应用于数据映射的好方法。

以示例地图:

为例
{:wrapper {:firstName "foo" 
           :lastName "bar"
           :addressLine1 "line1"
           :addressLine2 "line2"
           :birthDate {:iso "1930-03-12"}}}

并将其转换为:

{:name "foo bar"
 :address "line1 /n line2"
 :age 86}

我也希望转换能够反过来工作,尽管我不介意编写单独的转换。

到目前为止,我已经尝试编写转换函数列表:(伪)

(-> start-map
    transform-name
    transform-address
    transform-age)

每个变换采用[start-map {accumulator-map}]。我还尝试编写一个包含变换后的地图的键的地图,并将变换函数(和参数)作为它们的值。我觉得我错过了一个技巧。

6 个答案:

答案 0 :(得分:1)

你有正确的基本想法。我将如何做到这一点:

(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)))
)

结果:

> 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.

答案 1 :(得分:1)

变换:

(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)))})

逆变换。请注意,日期丢失精度。

(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))))}}}))

答案 2 :(得分:1)

zipmap,juxt和destructuring对于地图转换非常方便。

(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))))

答案 3 :(得分:0)

您还可以使用您提供的键和转换功能将映射定义为数据结构。 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])]])

,其中

(defn ->fullname [& s] (str/join " " s))

(defn ->age [s]
  (let [now (Date.)
        d   (Date. s)]
    (- (.getYear now) (.getYear d))))

然后实现一个函数来使用映射规则和源映射进行转换:

(transform a->b {:wrapper {:firstName    "foo" 
                           :lastName     "bar"
                           :addressLine1 "line1"
                           :addressLine2 "line2"
                           :birthDate    {:iso "1930/03/12"}}})

=>
{:name "foo bar", :address "line1", :age 86}

快速实施可以是这样的:

(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))

答案 4 :(得分:0)

为了使它成为通用的,我将创建转换函数,从源对象中选择路径,将选定的值处理为目标路径到值的映射:

(defn transform [source target paths transformation]
  (reduce (partial apply assoc-in)
          target
          (apply transformation
                 (map #(get-in source %) paths))))

然后你可以像这样使用它:

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)}

反之亦然:

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"}}}

答案 5 :(得分:0)

使用perc,您可以像这样轻松完成此操作:

(#%/%{:name (str %:firstName " " %:lastName)
      :address (str %:addressLine1 " \n " %:addressLine2)
      :age (-> %:birthDate :iso ->age)}
  (:wrapper original-map))

->age来自rmcv的示例。

很简单,对吧?而且,我们甚至不必为这些事情起一堆新的本地名称!