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 []
(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.
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])
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]]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With