【问题标题】:Extract Clojure map entries with a pattern into a list of maps?将具有模式的 Clojure 映射条目提取到映射列表中?
【发布时间】:2015-05-01 03:00:51
【问题描述】:

我有一张这样的地图(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))))

【问题讨论】:

  • 问题的核心是,“如何从 1 个哈希映射中提取 N 个哈希映射?”。 (name :item_price_1) 将给出字符串形式,#"" regex.. 以某种方式使用 into []...
  • 感谢所有详细的回答!我仍在研究和评估它们。根据@seanomlor 离线的帮助,我最终用更多单独的功能做了一些不同的事情,我将在此发布并很快会在这里选择最有用的(对我而言)答案。
  • 这就是我现在决定的。 gist.github.com/sventech/cdc4f0a662192980dd03

标签: dictionary clojure functional-programming


【解决方案1】:

我猜这个问题可以重新定义如下。

  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

希望这个循序渐进的方法对您有所帮助。

【讨论】:

  • 感谢您花时间解释!看到这个过程很有帮助。
【解决方案2】:

一个完整而直接的解决方案是:

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

【讨论】:

  • 您可能希望将(comp first first) 替换为ffirst
  • 您不想在正则表达式末尾使用$ 以确保数字位于后缀位置吗?
【解决方案3】:

如果不强制使用正则表达式,并且假设 mixed-map 键中的“后缀”是从 1 到 num-items 的数字,则可以直接解决问题。

结果中应该有与项目一样多的哈希映射。我们有num-items,所以我们可以映射从1到num-items的范围在每一步我们都可以为当前项目编号创建一个哈希映射。要创建每个单独的哈希映射,我们可以映射 item-keys 并将每个项目键转换为映射条目。地图条目中的键是 item-key 本身。该值来自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

【讨论】:

  • 谢谢!你的回答与我最终做的最相似,但我想奖励@tnoda 在解释中所做的工作。
【解决方案4】:
(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”的转换,我认为应该单独处理。)

【讨论】:

    【解决方案5】:

    我担心你的数据。

    • 我们不需要: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})
    

    现在我们遵循与@tnoda 或多或少相同的过程,只是进行了一两个改进。

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

    cmets 解释了函数级联中每个阶段的情况。您可以注释掉级联中的所有功能,然后从顶部一次重新引入它们。这将向您展示每行完成的内容。

    让我们将函数应用到我们的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}]
    

    有效!伊皮!

    如果您想保留旧数据,则必须清除流氓: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"}]
    

    【讨论】:

    • 谢谢,@Thumbnail。我正在解析依赖于 HTML localStorage 的 SimpleCart(js) 的输出。我认为他们使用了一些奇怪的数据结构来支持不兼容的 Web 浏览器的旧 poly-fill。如今,JSON 文档可能比表单的 POST/GET 更有意义......无论如何,:item_options 是一个嵌入了更多名称/值对数据的单个字符串。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-16
    • 2021-01-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多