July 23, 2020

Clojure Goodness: Query Set Of Maps With index Function

The namespace clojure.set has useful functions when we work with sets. One of the functions is the index function. The index function can be used for a set with map elements to create a new map based on distinct values for one or more keys. The first argument of the function is the set we transform and the second argument is a vector of one or more keys we want to index on. The keys in the new map are maps themselves. The value for each key is a set of maps that have the given keyword/value combination. The new map can be easily queried with the get function to get the values for a key.

In the next example code we see how we can use the clojure.set/index function to first transform a set with map elements to the new map and how to work with the resulting map:

(ns mrhaki.set.index
  (:require [clojure.test :refer [is]]
            [clojure.set :refer [index]]))

(def languages #{{:platform :jvm :name "Clojure"}
                 {:platform :jvm :name "Groovy"}
                 {:platform :native :name "Ruby"}
                 {:platform :jvm :name "JRuby"}
                 {:platform :native :name "Rust"}})

;; index function returns a map with a key for
;; each unique key/value combination for the keys
;; passed as second argument.
;; The value of each key is a set of the 
;; map that comply with the key/value combination.
(is (= {{:platform :jvm} #{{:platform :jvm :name "Clojure"}
                           {:platform :jvm :name "Groovy"}
                           {:platform :jvm :name "JRuby"}}
        {:platform :native} #{{:platform :native :name "Ruby"}
                              {:platform :native :name "Rust"}}}
        (index languages [:platform])))

;; We can use all collection functions on the map result
;; of the index function.
(is (= ["Clojure" "Groovy" "JRuby"]
       (map :name (get (index languages [:platform]) {:platform :jvm}))))

;; Set with sample data describing a shape
;; at a x and y location.
(def data #{{:shape :rectangle :x 100 :y 100}
            {:shape :circle :x 100 :y 100}
            {:shape :circle :x 100 :y 0}
            {:shape :circle :x 0 :y 100}})

;; We can use multiple keys as second argument of the
;; index function if we want to index on values of 
;; more thane one key.
(is (= {{:x 0 :y 100} #{{:shape :circle :x 0 :y 100}}
        {:x 100 :y 0} #{{:shape :circle :x 100 :y 0}}
        {:x 100 :y 100} #{{:shape :circle :x 100 :y 100}
                          {:shape :rectangle :x 100 :y 100}}}
       (index data [:x :y])))

(is (= #{{:shape :circle :x 100 :y 100}
         {:shape :rectangle :x 100 :y 100}}
         (get (index data [:x :y]) {:x 100 :y 100})))

Written with Clojure 1.10.1.