将具有模式的 Clojure 映射条目提取到映射列表中?
Extract Clojure map entries with a pattern into a list of maps?
我有一张这样的地图(1 个或多个项目混合在一起):
{:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"}
我想把它变成这样:
[{:item_quantity "1"
:item_options "blah"
:item_name "Great Deal"
:item_price "9.99"}
{:item_name "Awesome Deal"
:item_options "foo"
:item_quantity "1"
:item_price "9.99"}]
所以,我想通过项目键将它们分开:
(def item-keys [:item_name :item_options :item_price :item_quantity])
我想我可以以某种方式使用 map
或 walk
,但我不知道该怎么做——我是 Clojure 的新手。
我会从
开始
(defn parse-items
[mixed-map]
(let [num-items (Integer/parseInt (:itemCount mixed-map))]
(into []
(do-something mixed-map))))
如果不强制使用正则表达式,假设 mixed-map
键中的 "suffixes" 是从 1 到 num-items
的数字,问题可以直接解决。
结果中的哈希映射应该与项目的数量一样多。我们有 num-items
,因此我们可以映射从 1 到 num-items
的范围。在每一步中,我们都可以为当前项目编号创建一个哈希映射。要创建每个单独的哈希映射,我们可以映射 item-keys
并将每个项目键转换为映射条目。地图条目中的键是项目键本身。该值来自mixed-map
。我们只需要一种方法来根据项目编号和项目键为 mixed-map
创建键。在此过程中,我们不应忘记并非每个项目都有每个键,因此我们需要处理 nil 值。综上所述,我们有以下内容。
(def items {:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"})
(def item-keys [:item_name :item_options :item_price :item_quantity])
(defn parse-items
[mixed-map]
(let [num-items (Integer/parseInt (:itemCount mixed-map))
all-item-numbers (range 1 (inc num-items))
mixed-map-key (fn [n k] (keyword (str (name k) "_" n)))
map-entry (fn [n k]
(when-let [v (mixed-map (mixed-map-key n k))]
[k v]))
map-entries (fn [n] (map #(map-entry n %) item-keys))]
(mapv #(into {} (map-entries %)) all-item-numbers)))
(clojure.pprint/pprint (parse-items items))
; => [{:item_name "Great Deal",
; => :item_options "foo: 3",
; => :item_price "9.99",
; => :item_quantity "1"}
; => {:item_name "Awesome Deal",
; => :item_options "blah: 2",
; => :item_price "9.99"}]
; => nil
我想问题可以重新定义如下。
- 按关键字后缀对给定映射中的键值对进行分组。
- 为每个分组创建地图并将它们倒入一个新向量中。
如果这些假设是正确的,这就是我的解决方案。
首先,定义一个辅助函数,称为kv->skv
,它将原始键值对([k v]
)转换为后缀向量和修改后的键值对([suffix [k' v]
).
user> (def items {:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"})
#'user/items
user> (defn- kv->skv
[[k v]]
(let [[_ k' s] (re-find #"(.+)_(\d+)" (name k))]
[s [k' v]]))
#'user/kv->skv
user> (def items' (map kv->skv items))
#'user/items'
user> (clojure.pprint/pprint items')
(["1" ["item_name" "Great Deal"]]
["2" ["item_options" "blah: 2"]]
["2" ["item_name" "Awesome Deal"]]
["1" ["item_options" "foo: 3"]]
["1" ["item_quantity" "1"]]
["2" ["item_price" "9.99"]]
["1" ["item_price" "9.99"]]
[nil [nil "2"]])
nil
然后,使用项目键过滤项目。
user> (def item-keys #{:item_name :item_options :item_price :item_quantity})
#'user/item-keys
user> (def items-filtered (filter (comp item-keys keyword first second) items'))
#'user/items-filtered
user> (clojure.pprint/pprint items-filtered)
(["1" ["item_name" "Great Deal"]]
["2" ["item_options" "blah: 2"]]
["2" ["item_name" "Awesome Deal"]]
["1" ["item_options" "foo: 3"]]
["1" ["item_quantity" "1"]]
["2" ["item_price" "9.99"]]
["1" ["item_price" "9.99"]])
nil
其次,使用 group-by
函数按后缀对修改后的键值对进行分组。
user> (def groupings (group-by first items-filtered))
#'user/groupings
user> (clojure.pprint/pprint groupings)
{"1"
[["1" ["item_name" "Great Deal"]]
["1" ["item_options" "foo: 3"]]
["1" ["item_quantity" "1"]]
["1" ["item_price" "9.99"]]],
"2"
[["2" ["item_options" "blah: 2"]]
["2" ["item_name" "Awesome Deal"]]
["2" ["item_price" "9.99"]]]}
nil
并将分组转换为地图。
user> (def what-you-want (->> (vals groupings)
(map #(->> %
(map second)
(into {})))))
#'user/what-you-want
user> (clojure.pprint/pprint what-you-want)
({"item_name" "Great Deal",
"item_options" "foo: 3",
"item_quantity" "1",
"item_price" "9.99"}
{"item_options" "blah: 2",
"item_name" "Awesome Deal",
"item_price" "9.99"})
nil
最后将这些步骤整合成一个函数。
(defn extract-items
[items item-keys]
(let [kv->skv (fn
[[k v]]
(let [[_ k' s] (re-find #"(.+)_(\d+)" (name k))]
[s [k' v]]))]
(->> items
(map kv->skv)
(filter (comp item-keys keyword first second))
(group-by first)
vals
(map #(->> %
(map second)
(into {}))))))
有效。
user> (clojure.pprint/pprint (extract-items items item-keys))
({"item_name" "Great Deal",
"item_options" "foo: 3",
"item_quantity" "1",
"item_price" "9.99"}
{"item_options" "blah: 2",
"item_name" "Awesome Deal",
"item_price" "9.99"})
nil
我希望这个循序渐进的方法对你有所帮助。
一个完整而直接的解决方案是:
(->> item-map
(keep (fn [[k v]]
(let [[_ name id] (re-find #"(.+)_(\d+)$" (name k))]
(if id
[[(dec (Integer/parseInt id)) (keyword name)] v]))))
(sort-by ffirst)
(reduce (partial apply assoc-in) []))
如果您希望允许非连续的 ID 或者事先不知道它们是否为 0 索引,您可以像这样修改算法:
(->> item-map
(keep (fn [[k v]]
(let [[_ name id] (re-find #"(.+)_(\d+)$" (name k))]
(if id
[id [(keyword name) v]]))) )
(sort-by first)
(partition-by first)
(map #(->> %
(map second)
(into {}))))
请注意,由于此算法允许取消无损转换的保证,因此松散的输入要求(假设除了 :item-count
之外没有未编号的键)。例如不能期望向后转换算法再次从结果中产生与 item-map
相等的值。
为了清楚起见,我省略了 item-keys
的过滤,因为它是一个单独的问题。您可以通过将 item-keys
定义为哈希集并将 lambda 更改为 keep
来将其集成到两种算法中,如下所示:
(let [[_ name id] (re-find #"(.+)_(\d+)$" (name k))
k (-> name keyword item-keys)]
(if (and id k)
;; ...
(require '[clojure.string :as s])
(defn key-and-index
"Given a string like 'foo_bar_7' return ['foo_bar' 7]"
[s]
(let [segments (s/split s #"_")
k (s/join "_" (drop-last segments))
index (read-string (last segments))]
[k index]))
(defn item-map
"Reducing fn: given an accumulated nested map of index:key:val,
and a current item, parse the current item into the same shape
and add it to the map."
[m [old-key v]]
(let [[k i] (key-and-index (name old-key))]
(if (not (empty? k)) ; drop extraneous input data
(assoc-in m [i k] v)
m)))
(vals (reduce item-map {} items))
(与其他发布的答案一样,它忽略了您指定的从 "foo:3" 和 "blah: 2" 到 "foo" 和 "blah" 的转换,在我看来应该单独处理.)
我很担心你的数据。
- 我们不需要
:itemCount
。
- 数字应该是数字,而不是字符串。
- 如果顾名思义,可能会有好几个
:item_options_...
对于每个项目,该值应该是一个映射,而不是一个字符串。
因此您的数据应该如下所示:
(def data {:item_name_1 "Great Deal"
:item_options_2 {blah: 2}
:item_name_2 "Awesome Deal"
:item_options_1 {foo: 3}
:item_quantity_1 1
:item_price_2 9.99
:item_price_1 9.99})
现在我们或多或少地遵循与 相同的过程,只是进行了一两次改进。
data
中的每个键包含
- 一个 标识符 用于条目适用的事物并且
- 事物的属性。
为了使我们的解决方案普遍有用,它将采用 函数参数 将密钥拆分为这两个方面。对于您的数据,合适的函数是:
(defn dissect [kw]
(let [text (name kw)
point (.lastIndexOf text "_")]
((comp
(partial mapv keyword)
(juxt #(subs % 0 point) #(subs % (inc point)))
name)
kw)))
(dissect :item_name_1)
;[:item_name :1]
现在我们需要对您的数据进行相应的分类。我建议这样做:
(defn classify [cracker m]
(->> m
(map (juxt (comp cracker key) val)) ; crack the keys into [id attribute] pairs
(group-by (comp second first)) ; group by id
vals ; discard the id keys
(mapv #(->> % (map (juxt ffirst second)) (into {}))) ; assemble the maps
))
评论解释了级联函数中每个阶段的情况。您可以注释掉级联中的所有功能,然后从顶部开始一次一个地重新引入它们。这将向您展示每行完成的内容。
让我们将该函数应用到我们的 data
:
(classify dissect data)
;[{:item_name "Great Deal", :item_options {:foo 3}, :item_quantity 1, :item_price 9.99} {:item_options {:blah 2}, :item_name "Awesome Deal", :item_price 9.99}]
有效! Yippee!
如果你想坚持你的旧数据,你必须清除流氓 :itemCount
密钥:
(def old-data {:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"})
(classify dissect (dissoc old-data :itemCount))
;[{:item_name "Great Deal", :item_options "foo: 3", :item_quantity "1", :item_price "9.99"} {:item_options "blah: 2", :item_name "Awesome Deal", :item_price "9.99"}]
我有一张这样的地图(1 个或多个项目混合在一起):
{:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"}
我想把它变成这样:
[{:item_quantity "1"
:item_options "blah"
:item_name "Great Deal"
:item_price "9.99"}
{:item_name "Awesome Deal"
:item_options "foo"
:item_quantity "1"
:item_price "9.99"}]
所以,我想通过项目键将它们分开:
(def item-keys [:item_name :item_options :item_price :item_quantity])
我想我可以以某种方式使用 map
或 walk
,但我不知道该怎么做——我是 Clojure 的新手。
我会从
开始(defn parse-items
[mixed-map]
(let [num-items (Integer/parseInt (:itemCount mixed-map))]
(into []
(do-something mixed-map))))
如果不强制使用正则表达式,假设 mixed-map
键中的 "suffixes" 是从 1 到 num-items
的数字,问题可以直接解决。
结果中的哈希映射应该与项目的数量一样多。我们有 num-items
,因此我们可以映射从 1 到 num-items
的范围。在每一步中,我们都可以为当前项目编号创建一个哈希映射。要创建每个单独的哈希映射,我们可以映射 item-keys
并将每个项目键转换为映射条目。地图条目中的键是项目键本身。该值来自mixed-map
。我们只需要一种方法来根据项目编号和项目键为 mixed-map
创建键。在此过程中,我们不应忘记并非每个项目都有每个键,因此我们需要处理 nil 值。综上所述,我们有以下内容。
(def items {:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"})
(def item-keys [:item_name :item_options :item_price :item_quantity])
(defn parse-items
[mixed-map]
(let [num-items (Integer/parseInt (:itemCount mixed-map))
all-item-numbers (range 1 (inc num-items))
mixed-map-key (fn [n k] (keyword (str (name k) "_" n)))
map-entry (fn [n k]
(when-let [v (mixed-map (mixed-map-key n k))]
[k v]))
map-entries (fn [n] (map #(map-entry n %) item-keys))]
(mapv #(into {} (map-entries %)) all-item-numbers)))
(clojure.pprint/pprint (parse-items items))
; => [{:item_name "Great Deal",
; => :item_options "foo: 3",
; => :item_price "9.99",
; => :item_quantity "1"}
; => {:item_name "Awesome Deal",
; => :item_options "blah: 2",
; => :item_price "9.99"}]
; => nil
我想问题可以重新定义如下。
- 按关键字后缀对给定映射中的键值对进行分组。
- 为每个分组创建地图并将它们倒入一个新向量中。
如果这些假设是正确的,这就是我的解决方案。
首先,定义一个辅助函数,称为kv->skv
,它将原始键值对([k v]
)转换为后缀向量和修改后的键值对([suffix [k' v]
).
user> (def items {:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"})
#'user/items
user> (defn- kv->skv
[[k v]]
(let [[_ k' s] (re-find #"(.+)_(\d+)" (name k))]
[s [k' v]]))
#'user/kv->skv
user> (def items' (map kv->skv items))
#'user/items'
user> (clojure.pprint/pprint items')
(["1" ["item_name" "Great Deal"]]
["2" ["item_options" "blah: 2"]]
["2" ["item_name" "Awesome Deal"]]
["1" ["item_options" "foo: 3"]]
["1" ["item_quantity" "1"]]
["2" ["item_price" "9.99"]]
["1" ["item_price" "9.99"]]
[nil [nil "2"]])
nil
然后,使用项目键过滤项目。
user> (def item-keys #{:item_name :item_options :item_price :item_quantity})
#'user/item-keys
user> (def items-filtered (filter (comp item-keys keyword first second) items'))
#'user/items-filtered
user> (clojure.pprint/pprint items-filtered)
(["1" ["item_name" "Great Deal"]]
["2" ["item_options" "blah: 2"]]
["2" ["item_name" "Awesome Deal"]]
["1" ["item_options" "foo: 3"]]
["1" ["item_quantity" "1"]]
["2" ["item_price" "9.99"]]
["1" ["item_price" "9.99"]])
nil
其次,使用 group-by
函数按后缀对修改后的键值对进行分组。
user> (def groupings (group-by first items-filtered))
#'user/groupings
user> (clojure.pprint/pprint groupings)
{"1"
[["1" ["item_name" "Great Deal"]]
["1" ["item_options" "foo: 3"]]
["1" ["item_quantity" "1"]]
["1" ["item_price" "9.99"]]],
"2"
[["2" ["item_options" "blah: 2"]]
["2" ["item_name" "Awesome Deal"]]
["2" ["item_price" "9.99"]]]}
nil
并将分组转换为地图。
user> (def what-you-want (->> (vals groupings)
(map #(->> %
(map second)
(into {})))))
#'user/what-you-want
user> (clojure.pprint/pprint what-you-want)
({"item_name" "Great Deal",
"item_options" "foo: 3",
"item_quantity" "1",
"item_price" "9.99"}
{"item_options" "blah: 2",
"item_name" "Awesome Deal",
"item_price" "9.99"})
nil
最后将这些步骤整合成一个函数。
(defn extract-items
[items item-keys]
(let [kv->skv (fn
[[k v]]
(let [[_ k' s] (re-find #"(.+)_(\d+)" (name k))]
[s [k' v]]))]
(->> items
(map kv->skv)
(filter (comp item-keys keyword first second))
(group-by first)
vals
(map #(->> %
(map second)
(into {}))))))
有效。
user> (clojure.pprint/pprint (extract-items items item-keys))
({"item_name" "Great Deal",
"item_options" "foo: 3",
"item_quantity" "1",
"item_price" "9.99"}
{"item_options" "blah: 2",
"item_name" "Awesome Deal",
"item_price" "9.99"})
nil
我希望这个循序渐进的方法对你有所帮助。
一个完整而直接的解决方案是:
(->> item-map
(keep (fn [[k v]]
(let [[_ name id] (re-find #"(.+)_(\d+)$" (name k))]
(if id
[[(dec (Integer/parseInt id)) (keyword name)] v]))))
(sort-by ffirst)
(reduce (partial apply assoc-in) []))
如果您希望允许非连续的 ID 或者事先不知道它们是否为 0 索引,您可以像这样修改算法:
(->> item-map
(keep (fn [[k v]]
(let [[_ name id] (re-find #"(.+)_(\d+)$" (name k))]
(if id
[id [(keyword name) v]]))) )
(sort-by first)
(partition-by first)
(map #(->> %
(map second)
(into {}))))
请注意,由于此算法允许取消无损转换的保证,因此松散的输入要求(假设除了 :item-count
之外没有未编号的键)。例如不能期望向后转换算法再次从结果中产生与 item-map
相等的值。
为了清楚起见,我省略了 item-keys
的过滤,因为它是一个单独的问题。您可以通过将 item-keys
定义为哈希集并将 lambda 更改为 keep
来将其集成到两种算法中,如下所示:
(let [[_ name id] (re-find #"(.+)_(\d+)$" (name k))
k (-> name keyword item-keys)]
(if (and id k)
;; ...
(require '[clojure.string :as s])
(defn key-and-index
"Given a string like 'foo_bar_7' return ['foo_bar' 7]"
[s]
(let [segments (s/split s #"_")
k (s/join "_" (drop-last segments))
index (read-string (last segments))]
[k index]))
(defn item-map
"Reducing fn: given an accumulated nested map of index:key:val,
and a current item, parse the current item into the same shape
and add it to the map."
[m [old-key v]]
(let [[k i] (key-and-index (name old-key))]
(if (not (empty? k)) ; drop extraneous input data
(assoc-in m [i k] v)
m)))
(vals (reduce item-map {} items))
(与其他发布的答案一样,它忽略了您指定的从 "foo:3" 和 "blah: 2" 到 "foo" 和 "blah" 的转换,在我看来应该单独处理.)
我很担心你的数据。
- 我们不需要
:itemCount
。 - 数字应该是数字,而不是字符串。
- 如果顾名思义,可能会有好几个
:item_options_...
对于每个项目,该值应该是一个映射,而不是一个字符串。
因此您的数据应该如下所示:
(def data {:item_name_1 "Great Deal"
:item_options_2 {blah: 2}
:item_name_2 "Awesome Deal"
:item_options_1 {foo: 3}
:item_quantity_1 1
:item_price_2 9.99
:item_price_1 9.99})
现在我们或多或少地遵循与
data
中的每个键包含
- 一个 标识符 用于条目适用的事物并且
- 事物的属性。
为了使我们的解决方案普遍有用,它将采用 函数参数 将密钥拆分为这两个方面。对于您的数据,合适的函数是:
(defn dissect [kw]
(let [text (name kw)
point (.lastIndexOf text "_")]
((comp
(partial mapv keyword)
(juxt #(subs % 0 point) #(subs % (inc point)))
name)
kw)))
(dissect :item_name_1)
;[:item_name :1]
现在我们需要对您的数据进行相应的分类。我建议这样做:
(defn classify [cracker m]
(->> m
(map (juxt (comp cracker key) val)) ; crack the keys into [id attribute] pairs
(group-by (comp second first)) ; group by id
vals ; discard the id keys
(mapv #(->> % (map (juxt ffirst second)) (into {}))) ; assemble the maps
))
评论解释了级联函数中每个阶段的情况。您可以注释掉级联中的所有功能,然后从顶部开始一次一个地重新引入它们。这将向您展示每行完成的内容。
让我们将该函数应用到我们的 data
:
(classify dissect data)
;[{:item_name "Great Deal", :item_options {:foo 3}, :item_quantity 1, :item_price 9.99} {:item_options {:blah 2}, :item_name "Awesome Deal", :item_price 9.99}]
有效! Yippee!
如果你想坚持你的旧数据,你必须清除流氓 :itemCount
密钥:
(def old-data {:item_name_1 "Great Deal"
:item_options_2 "blah: 2"
:item_name_2 "Awesome Deal"
:item_options_1 "foo: 3"
:item_quantity_1 "1"
:item_price_2 "9.99"
:item_price_1 "9.99"
:itemCount "2"})
(classify dissect (dissoc old-data :itemCount))
;[{:item_name "Great Deal", :item_options "foo: 3", :item_quantity "1", :item_price "9.99"} {:item_options "blah: 2", :item_name "Awesome Deal", :item_price "9.99"}]