如何遍历 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将关键字转换为字符串,但是我找不到一种方便的方法。

这就是我的做法,但它似乎不优雅(嵌套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)}))))

你的解决方案还不错,至于嵌套的 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对这个问题的评论,我会付出一些努力来建立一个更好看的地图结构。

(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 reshape 库中也存在类似的方法,顺便说一下,它已经为 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

到目前为止,有很多很好的解决方案。我要补充的只是keys的简化:

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

请注意,请注意几乎普遍倾向于替换而不是最后/拆分。猜测转型的精神是"失去领军人物前缀",replace说得更好。如果 OTOH 的精神是"找到数字并使用它",那么使用一些正则表达式来隔离数字会更真实。

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

这里有三种解决方案。

第一个解决方案通过 Tupelo 库中的lazy-genyield函数使用 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"})

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


更新:

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

最新更新