是否有一个 clojure 函数来"join"两个地图列表?



我正在寻找一个类似于 sql 中的连接函数,例如:

以下是两个地图列表:

(def a [{:user_id 1 :name "user 1"} 
        {:user_id 2 :name "user 2"}])
(def b [{:user_id 2 :email "e 2"} 
        {:user_id 1 :email "e 1"}])

我想在user_id上加入 a 和 b 以获得:

[{:user_id 1 :name "user 1" :email "e 1"} 
 {:user_id 2 :name "user 2" :email "e 2"}]

clojure或其他库中是否有一些功能可以实现这一点?

clojure.set/join将做这件事。

(require '[clojure.set :as set])
(set/join a b) ; => #{{:email "e 1", :name "user 1", :user_id 1} {:email "e 2", :name "user 2", :user_id 2}}

如果不提供第三个参数,函数将连接所有公共键:

(def a [{:id1 1 :id2 2 :name "n 1"} {:id1 2 :id2 3 :name "n 2"}])
(def b [{:id1 1 :id2 2 :url "u 1"} {:id1 2 :id2 4 :url "u 2"}])
(def c [{:id1 1 :id2 2 :url "u 1"} {:id1 2 :url "u 2"}]) ; :id2 is missing in 2nd record
(set/join a b) ; #{{:name "n 1", :url "u 1", :id1 1, :id2 2}}
(set/join a c) ; #{{:name "n 2", :url "u 2", :id1 2, :id2 3} {:name "n 1", :url "u 1", :id1 1, :id2 2}}

要仅在 id1 上加入 a 和 b:

(set/join a b {:id1 :id1}) ; #{{:name "n 2", :url "u 2", :id1 2, :id2 4} {:name "n 1", :url "u 1", :id1 1, :id2 2}}

我们甚至可以通过来自不同集合的不同键加入:

(set/join a b {:id1 :id2}) ; #{{:name "n 2", :url "u 1", :id1 1, :id2 2}}

另一种选择,我认为更简单一些:

user=> (map #(apply merge %) (vals (group-by :user_id (concat a b))))
({:email "e 1", :name "user 1", :user_id 1} {:email "e 2", :name "user 2", :user_id 2})

group-by创建从:user_id到包含给定值的所有映射的映射,vals仅获取值(每个值都是向量),最后对于每个值向量,将它们合并。

我认为没有任何简单的函数已经这样做了,但我可能是错的。

如果您知道每个序列中都存在每个user_id,那么您可以按user_id排序,然后将合并应用于相应的映射:

(defn sort-by-user-id 
  [m]
  (sort #(< (:user_id %1) (:user_id %2)) m))
(map merge (sort-by-user-id a) (sort-by-user-id b))
; => ({:email "e 1", :name "user 1", :user_id 1} {:email "e 2", :name "user 2", :user_id 2})

如果你不能假设每个序列中都存在所有相同的user_id,我认为你需要做一些稍微复杂一些的事情来匹配user_id s。 我假设如果名称映射没有相应的电子邮件映射,则希望保持名称映射不变(对于缺少的名称映射,反之亦然)。 如果没有,那么一种选择是去除这些地图并使用上面给出的方法。

这是合并相应名称和电子邮件映射的一种方法。 我们可以将user_id s用作地图地图中的键,以便匹配相应的地图。 首先创建包含所有以 user_ids 作为键的映射的映射,例如,如下所示:

(def az (zipmap (map :user_id a) a)) ; => {2 {:name "user 2", :user_id 2}, 1 {:name "user 1", :user_id 1}}
(def bz (zipmap (map :user_id b) b)) ; => {1 {:email "e 1", :user_id 1}, 2 {:email "e 2", :user_id 2}}

然后像这样合并各个映射,在过程结束时去除键:

(vals (merge-with merge az bz))
; => ({:email "e 2", :name "user 2", :user_id 2} {:email "e 1", :name "user 1", :user_id 1})

把所有的东西放在一起:

(defn map-of-maps
  [cm]
  (zipmap (map :user_id cm) cm))
(defn merge-maps
  [& cms]
  (vals 
    (apply merge-with merge 
           (map map-of-maps cms))))

让我们确保它适用于缺少的user_id

(def a+ (conj a {:name "user 3", :user_id 3}))
(def b+ (conj b {:email "e 4", :user_id 4}))
(merge-maps a+ b+)
; => ({:email "e 4", :user_id 4} {:name "user 3", :user_id 3} {:email "e 2", :name "user 2", :user_id 2} {:email "e 1", :name "user 1", :user_id 1})

如果有更简单或更优雅的方法,我不会感到惊讶。 这只是我想到的一种策略。

最新更新