fl00r
fl00r

Reputation: 83680

Translating vector into map

I've got this list of fields (that's Facebook's graph API fields list).

["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"]

I want to generate a map out of it. The convention is following, if after a key vector follows, then its an inner object for the key. Example vector could be represented as a map as:

{"a" "value"
 "b" {"c" {"t" "value"} "d" "value"}
 "e" {"f" "value"}
 "g" "value"}

So I have this solution so far

(defn traverse
  [data]
  (mapcat (fn [[left right]]
            (if (vector? right)
              (let [traversed (traverse right)]
                (mapv (partial into [left]) traversed))
              [[right]]))
          (partition 2 1 (into [nil] data))))

(defn facebook-fields->map
  [fields default-value]
  (->> fields
       (traverse)
       (reduce #(assoc-in %1 %2 nil) {})
       (clojure.walk/postwalk #(or % default-value))))

(let [data ["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"]]
  (facebook-fields->map data "value"))
#=> {"a" "value", "b" {"c" {"t" "value"}, "d" "value"}, "e" {"f" "value"}, "g" "value"}

But it is fat and difficult to follow. I am wondering if there is a more elegant solution.

Upvotes: 1

Views: 136

Answers (3)

madstap
madstap

Reputation: 1552

Since you're asking for a cleaner solution as opposed to a solution, and because I thought it was a neat little problem, here's another one.

(defn facebook-fields->map [coll]
  (into {}
        (keep (fn [[x y]]
                (when-not (vector? x)
                  (if (vector? y)
                    [x (facebook-fields->map y)]
                    [x "value"]))))
        (partition-all 2 1 coll)))

Upvotes: 1

Alan Thompson
Alan Thompson

Reputation: 29958

Trying to read heavily nested code makes my head hurt. It is worse when the answer is something of a "force-fit" with postwalk, which does things in a sort of "inside out" manner. Also, using partition-all is a bit of a waste, since we need to discard any pairs with two non-vectors.

To me, the most natural solution is a simple top-down recursion. The only problem is that we don't know in advance if we need to remove one or two items from the head of the input sequence. Thus, we can't use a simple for loop or map.

So, just write it as a straightforward recursion, and use an if to determine whether we consume 1 or 2 items from the head of the list.

  1. If the 2nd item is a value, we consume one item and add in :dummy-value to make a map entry.
  2. If the 2nd item is a vector, we recurse and use that as the value in the map entry.

The code:

(ns tst.demo.core
  (:require [clojure.walk :as walk] ))

(def data ["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"])

(defn parse [data]
  (loop [result {}
         data   data]
    (if (empty? data)
      (walk/keywordize-keys result)
      (let [a (first data)
            b (second data)]
        (if (sequential? b)
          (recur
            (into result {a (parse b)})
            (drop 2 data))
          (recur
            (into result {a :dummy-value})
            (drop 1 data)))))))

with result:

(parse data) => 
{:a :dummy-value,
 :b {:c {:t :dummy-value}, :d :dummy-value},
 :e {:f :dummy-value},
 :g :dummy-value}

I added keywordize-keys at then end just to make the result a little more "Clojurey".

Upvotes: 3

Taylor Wood
Taylor Wood

Reputation: 16194

Here's another way to do it using postwalk for the whole traversal, rather than using it only for default-value replacement:

(defn facebook-fields->map
  [fields default-value]
  (clojure.walk/postwalk
    (fn [v] (if (coll? v)
              (->> (partition-all 2 1 v)
                   (remove (comp coll? first))
                   (map (fn [[l r]] [l (if (coll? r) r default-value)]))
                   (into {}))
              v))
    fields))

(facebook-fields->map ["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"] "value")
=> {"a" "value",
    "b" {"c" {"t" "value"}, "d" "value"},
    "e" {"f" "value"},
    "g" "value"}

Upvotes: 3

Related Questions