Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find indexes in deeply nested data structure(vectors and lists) in Clojure?

Tags:

clojure

I'm trying to get index route of a value from a nested data structure.

I've written some code but it does not work properly

(defn helper
  [val form c index]
  (loop [f form
         i index
         l 0]
    (if (seq f)
      (if (or (list? (first f)) (vector? (first f)))
        (helper val (first f) (conj c l) (inc i))
        (if (= val (first f))
          (conj c l)
          (recur (rest f) (inc i) (inc l))))
      nil)))

(defn find-indexes
  [val form c index]
  (when (seq form)
    (if-let [result (helper val form c index)]
      result
      (find-indexes val (rest form) c (inc index)))))

(defn find-index-route
  [val form]
  (find-indexes val form [] 0))

Current behaviour:

(find-index-route :my-key '(1 2 ("a" :my-key))) ;=> [2 1] "Works"

(find-index-route :my-key '(1 2 ("a" ["b" 3 :my-key]))) ;=> [2 1 2] "Works"

(find-index-route :my-key '(1 2 ("a" ["b" 3 () :my-key]))) ;=> nil "Does NOT Work"

(find-index-route :my-key '(1 2 ("a" [] ["b" 3 :my-key]))) ;=> nil "Does NOT Work"

(find-index-route :my-key '(1 2 [] ("a" ["b" 3 :my-key]))) ;=> [0 1 2] "It has to be [3 1 2]"

The thing is if the function hits empty list or vector before finding the value it returns nil or 0(just for first the level)

The behaviour I need:

                                          ;=> [indexes...]

(find-index-route :my-key '(1 2 :my-key)) ;=> [2]

(find-index-route :my-key '(1 2 "a" :my-key "b")) ;=> [3]

(find-index-route :my-key '(1 2 [:my-key] "c")) ;=> [2 0]

(find-index-route :my-key '(1 2 [3 [:my-key]])) ;=> [2 1 0]

(find-index-route :my-key '(1 2 [3 [[] :my-key]])) ;=> [2 1 1]

(find-index-route :my-key '(1 2 [3 [4 5 6 (:my-key)]])) ;=> [2 1 3 0]

(find-index-route :my-key '(1 2 [3 [[]]])) ;=> nil or []
like image 639
Ertuğrul Çetin Avatar asked Aug 18 '17 20:08

Ertuğrul Çetin


3 Answers

(defn find-index-route [x coll]
  (letfn [(path-in [y]
            (cond
              (= y x) '()
              (coll? y) (let [[failures [success & _]]
                              (->> y
                                   (map path-in)
                                   (split-with not))]
                          (when success (cons (count failures) success)))))]
    (path-in coll)))

Examples:

(find-index-route :my-key '(1 2 :my-key)) ;=> [2]
=> (2)

(find-index-route :my-key '(1 2 "a" :my-key "b")) ;=> [3]
=> (3)

(find-index-route :my-key '(1 2 [:my-key] "c")) ;=> [2 0]
=> (2 0)

(find-index-route :my-key '(1 2 [3 [:my-key]])) ;=> [2 1 0]
=> (2 1 0)

(find-index-route :my-key '(1 2 [3 [[] :my-key]])) ;=> [2 1 1]
=> (2 1 1)

(find-index-route :my-key '(1 2 [3 [4 5 6 (:my-key)]])) ;=> [2 1 3 0]
=> (2 1 3 0)

(find-index-route :my-key '(1 2 [3 [[]]])) ;=> nil or []
=> nil

Thinking about Performance

split-with scans the sequence twice, with take-while and drop-while, to produce two lazy sequences. If performance is tight, you could write a version of drop-while that also tells you how many items it has dropped:

(defn counted-drop-while [pred coll]
  (loop [takes 0, tail coll]
    (if (and (seq tail) (pred (first tail)))
      (recur (inc takes) (rest tail))
      [takes tail])))

It just does one scan. We can easily adapt find-index-route to use it:

(defn find-index-route [x coll]
  (letfn [(path-in [y]
            (cond
              (= y x) '()
              (coll? y) (let [[fail-count [success & _]]
                              (->> y
                                   (map path-in)
                                   (counted-drop-while not))]
                          (when success (cons fail-count success)))))]
    (path-in coll)))

... with identical results.

like image 86
Thumbnail Avatar answered Nov 19 '22 12:11

Thumbnail


This question seemed perfect for the Specter library, which is all about navigating and transforming nested data structures. I asked Nathan Marz about a possible solution to this problem on Clojurians yesterday - see his original answer here:

(defn find-index-route [v data]
  (let [walker (recursive-path [] p
                               (if-path sequential?
                                        [INDEXED-VALS
                                         (if-path [LAST (pred= v)]
                                                  FIRST
                                                  [(collect-one FIRST) LAST p])]))
        ret    (select-first walker data)]
    (if (vector? ret) ret [ret])))

Note that this returns [nil] if :my-key is not present. You could easily modify it to return nil by changing the last "if" section to this:

(if (or (vector? ret) (nil? ret)) ret [ret])
like image 29
mwfogleman Avatar answered Nov 19 '22 11:11

mwfogleman


I made a version that found all the route's.

(defn find-index-route
  "find all routes to some value"
  ([k x] (find-index-route k x []))
  ([k x p]
   (cond
     (= k x) [p]
     (coll? x) (->> (if (map? x) x (vec x))
                    (reduce-kv (fn [acc i v]
                                 (into (vec (find-index-route k v (conj p i))) acc)) []))
     :else nil)))

EDIT: also works on maps

(find-index-route :my-key '{:bar 33
                            :foo [{:my-key ("0" :my-key)}]
                            "bar" ["key" {:foo ([:my-key])}]})
;=> [["bar" 1 :foo 0 0] [:foo 0 :my-key 1]]
like image 1
souenzzo Avatar answered Nov 19 '22 10:11

souenzzo