1

Just started learning Clojure, so I imagine my main issue is I don't know how to formulate the problem correctly to find an existing solution. I have a map:

{[0 1 "a"] 2, [0 1 "b"] 1, [1 1 "a"] 1}

and I'd like to "transform" it to:

{[0 1] "a", [1 1] "a"}

i.e. use the two first elements of the composite key as they new key and the third element as the value for the key-value pair that had the highest value in the original map.

I can easily create a new map structure:

=> (into {} (for [[[x y z] v] {[0 1 "a"] 2, [0 1 "b"] 1, [1 1 "a"] 1}] [[x y] {z v}]))
{[0 1] {"b" 1}, [1 1] {"a" 1}}

but into accepts no predicates so last one wins. I also experimented with :let and merge-with but can't seem to correctly refer to the map, eliminate the unwanted pairs or replace values of the map while processing.

Kalle
  • 2,157
  • 1
  • 22
  • 21

3 Answers3

1

You can do this by threading together a series of sequence transformations.

(->> data
     (group-by #(->> % key (take 2)))
     vals 
     (map (comp first first (partial sort-by (comp - val))))
     (map (juxt #(subvec % 0 2) #(% 2)))
     (into {}))

;{[0 1] "a", [1 1] "a"}

... where

(def data {[0 1 "a"] 2, [0 1 "b"] 1, [1 1 "a"] 1})

You build up the solution line by line. I recommend you follow in the footsteps of the construction, starting with ...

(->> data
     (group-by #(->> % key (take 2)))

;{(0 1) [[[0 1 "a"] 2] [[0 1 "b"] 1]], (1 1) [[[1 1 "a"] 1]]}

Stacking up layers of (lazy) sequences can run fairly slowly, but the transducers available in Clojure 1.7 will allow you to write faster code in this idiom, as seen in this excellent answer.

Community
  • 1
  • 1
Thumbnail
  • 13,293
  • 2
  • 29
  • 37
  • That looks very pretty to me! Intuitively, I imagined some series of transformations but could not have arrived at that while most of the core functions are still unfamiliar to me. I'll take it apart to study, thanks! – Kalle Nov 10 '14 at 17:26
0

Into tends to be most useful when you just need to take a seq of values and with no additional transformation construct a result from it using only conj. Anything else where you are performing construction tends to be better suited by preprocessing such as sorting, or by a reduction which allows you to perform accumulator introspection such as you want here.

First of all we have to be able to compare two strings..

(defn greater? [^String a ^String b]
  (> (.compareTo a b) 0))

Now we can write a transformation that compares the current value in the accumulator to the "next" value and keeps the maximum. -> used somewhat gratuitusly to make the update function more readable.

(defn transform [input]
  (-> (fn [acc [[x y z] _]]      ;; take the acc, [k, v], destructure k discard v
        (let [key [x y]]         ;; construct key into accumulator
          (if-let [v (acc key)]  ;; if the key is set
            (if (greater? z v)   ;;   and z (the new val) is greater
              (assoc acc key z)  ;;   then update
              acc)               ;;   else do nothing
            (assoc acc key z)))) ;; else update
      (reduce {} input)))        ;; do that over all [k, v]s from empty acc
arrdem
  • 2,365
  • 1
  • 16
  • 18
  • Not sure why you need the string comparator here. The OP's data needs to compare the value element from the original map which is an integer. ie. [0 1 "a"] = 2, [0 1 "b"] = 1. 2 > 1 so keep first key and transform to {[0 1] "a"} – Mark Fisher Nov 10 '14 at 13:43
  • Thanks, that gives me some ideas. However, you are discarding the original value and comparing z to v, which was not my intention. Rather, I think you have to keep the *v* for the earlier set *z* somewhere, then compare it to the *v* for the new *z* to test for. – Kalle Nov 10 '14 at 16:36
0
user> (def m {[0 1 "a"] 2, [0 1 "b"] 1, [1 1 "a"] 1})
#'user/m
user> (->> m
           keys
           sort
           reverse
           (mapcat (fn [x]
                     (vector (-> x butlast vec)
                             (last x))))
           (apply sorted-map))

;=> {[0 1] "a", [1 1] "a"}
runexec
  • 862
  • 4
  • 8