将具有模式的 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])

我想我可以以某种方式使用 mapwalk,但我不知道该怎么做——我是 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

我想问题可以重新定义如下。

  1. 按关键字后缀对给定映射中的键值对进行分组。
  2. 为每个分组创建地图并将它们倒入一个新向量中。

如果这些假设是正确的,这就是我的解决方案。

首先,定义一个辅助函数,称为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"}]