如何在Clojure中迭代嵌套的dict / hash-map来自定义/变换我的数据结构?

时间:2017-04-17 18:53:30

标签: clojure

我有一些看起来像这样的东西:

{:person-123 {:xxx [1 5]
              :zzz [2 3 4]}
 :person-456 {:yyy [6 7]}}

我想改造它,看起来像这样:

[{:person "123" :item "xxx"}
 {:person "123" :item "zzz"}
 {:person "456" :item "yyy"}]

这是类似flatten的问题,我知道我可以通过调用name将关键字转换为字符串,但我无法通过方便的方式来实现这一点。< / p>

我就是这样做的,但它似乎不太优雅(嵌套for循环,我在看着你):

(require '[clojure.string :refer [split]])
(into [] 
      (apply concat
             (for [[person nested-data] input-data]
                  (for [[item _] nested-data]
                       {:person (last (split (name person) #"person-"))
                        :item (name item)}))))

5 个答案:

答案 0 :(得分:7)

你的解决方案也不错,对于嵌套for循环,for实际上支持嵌套循环,所以你可以把它写成:

(vec 
  (for [[person nested-data] input-data
       [item _] nested-data]
    {:person (last (clojure.string/split (name person) #"person-"))
     :item   (name item)}))
个人而言,我倾向于专门为此目的使用for(嵌套循环),否则我通常会更熟悉map等。但这只是个人偏好。

我也非常赞同@ amalloy对这个问题的评论,我会付出一些努力来开始寻找一个更好看的地图结构。

答案 1 :(得分:0)

(let [x {:person-123 {:xxx [1 5]
                              :zzz [2 3 4]}
                 :person-456 {:yyy [6 7]}}]

            (clojure.pprint/pprint
                (mapcat
                    (fn [[k v]]
                        (map (fn [[k1 v1]]
                                 {:person (clojure.string/replace (name k) #"person-" "") :item (name k1)}) v))
                    x))
            )

我不确定是否只有一个高阶函数,至少在核心中,能够一次性完成你想要的任务。

另一方面,GNU R重塑库中存在类似的方法,顺便说一下,它已经为clojure重新创建了: 你感兴趣的https://crossclj.info/ns/grafter/0.8.6/grafter.tabular.melt.html#_melt-column-groups

这就是它在Gnu R中的作用:http://www.statmethods.net/management/reshape.html

答案 2 :(得分:0)

到目前为止,有很多好的解决方案。我要添加的是keys

的简化
(vec
     (for [[person nested-data] input-data
           item (map name (keys nested-data))]
       {:person (clojure.string/replace-first
                  (name person)
                  #"person-" "")
        :item   item}))

注意btw近乎普遍的首选项替换为last / split。猜测转变的精神是“失去领先的人 - 前缀”,replace说更好。如果OTOH的精神是“找到数字并使用它”,那么用一些正则表达式来隔离数字将更加真实。

答案 3 :(得分:0)

(reduce-kv (fn [ret k v]
             (into ret (map (fn [v-k]
                              {:person (last (str/split (name k) #"-"))
                               :item   (name v-k)}) 
                            (keys v))))
           []
           {:person-123 {:xxx [1 5] :zzz [2 3 4]}
            :person-456 {:yyy [6 7]}})

=> [{:person "123", :item "xxx"} 
    {:person "123", :item "zzz"} 
    {:person "456", :item "yyy"}]

答案 4 :(得分:-1)

以下是三种解决方案。

第一个解决方案通过lazy-genyield函数from the Tupelo library使用Python风格的延迟生成器函数。我认为这个方法最简单,因为内部循环产生了映射,外部循环产生了一个序列。此外,内部循环可以为每个外部循环运行零次,一次或多次。使用yield,您无需考虑该部分。

(ns tst.clj.core
  (:use clj.core clojure.test tupelo.test)
  (:require
    [clojure.string :as str]
    [clojure.walk :as walk]
    [clojure.pprint :refer [pprint]]
    [tupelo.core :as t]
    [tupelo.string :as ts]
  ))
(t/refer-tupelo)

(def data
  {:person-123 {:xxx [1 5]
                :zzz [2 3 4]}
   :person-456 {:yyy [6 7]}})

(defn reformat-gen [data]
  (t/lazy-gen
    (doseq [[outer-key outer-val] data]
      (let [int-str (str/replace (name outer-key) "person-" "")]
        (doseq [[inner-key inner-val] outer-val]
          (let [inner-key-str (name inner-key)]
            (t/yield {:person int-str :item inner-key-str})))))))

如果你真的想要“纯粹”,以下是另一种解决方案。但是,使用此解决方案,我犯了一些错误,需要修复许多调试打印输出。此版本使用tupelo.core/glue代替concat,因为它“更安全”并验证集合是否都是地图,所有向量/列表等。

(defn reformat-glue [data]
  (apply t/glue
    (forv [[outer-key outer-val] data]
      (let [int-str (str/replace (name outer-key) "person-" "")]
        (forv [[inner-key inner-val] outer-val]
          (let [inner-key-str (name inner-key)]
            {:person int-str :item inner-key-str}))))))

两种方法都给出了相同的答案:

(newline) (println "reformat-gen:")
(pprint (reformat-gen data))
(newline) (println "reformat-glue:")
(pprint (reformat-glue data))

reformat-gen:
({:person "123", :item "xxx"}
 {:person "123", :item "zzz"}
 {:person "456", :item "yyy"})

reformat-glue:
[{:person "123", :item "xxx"}
 {:person "123", :item "zzz"}
 {:person "456", :item "yyy"}]

如果你想成为“超级纯粹”,这是第三个解决方案(虽然我认为这个尝试太难了!)。这里我们使用for宏的功能在单个表达式中嵌套元素。 for也可以在其中嵌入let个表达式,但这会导致对int-str的重复评估。

(defn reformat-overboard [data]
  (for [[outer-key outer-val] data
        [inner-key inner-val] outer-val
        :let [int-str       (str/replace (name outer-key) "person-" "") ; duplicate evaluation
              inner-key-str (name inner-key)]]
    {:person int-str :item inner-key-str}))
(newline)
(println "reformat-overboard:")
(pprint (reformat-overboard data))

reformat-overboard:
({:person "123", :item "xxx"}
 {:person "123", :item "zzz"}
 {:person "456", :item "yyy"})

我可能会坚持第一个,因为它(至少对我来说)更简单,更防弹。 YMMV。

更新

请注意,即使发生了2次嵌套for次迭代,第3种方法也会生成单个映射序列。这与两个嵌套的for表达式不同,后者将生成一系列映射序列。