bubblebop
bubblebop

Reputation: 37

Clojure nested for loop with index

I've been trying to idiomatically loop through a nested vector like below:

[[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]]

I also need to return the coordinates once I've found a value. eg The call (find-key-value 3) should return [1 2]

This is what I have so far but its not giving me the output that I need it would return ([] [] [] [] [] [1 2] [] [] []) where as i only need [1 2]

(defn find-key-value
  [array value]
  (for [x (range 0 (count array))]
    (loop [y   0
           ret []]
      (cond
        (= y (count (nth array x))) [x y]
        :else (if (= value (get-in array [x y]))
                (recur (+ 1 y) (conj ret [x y]))
                (recur (+ 1 y) ret))))))

Anyone have any ideas on how I can fix my code to get to my desired solution or have a better approach in mind!

Upvotes: 2

Views: 1348

Answers (6)

leetwinski
leetwinski

Reputation: 17859

there is a function in clojure core, which exactly suites the task: keep-indexed. Which is exactly indexed map + filter:

(defn find-val-idx [v data]
  (ffirst (keep-indexed
           (fn [i row]
             (seq (keep-indexed
                   (fn [j [_ x]] (when (= v x) [i j]))
                   (partition 2 row))))
           data)))

user> (find-val-idx 3 [[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]])
;;=> [1 2]

user> (find-val-idx 10 [[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]])
;;=> nil

user> (find-val-idx 1 [[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]])
;;=> [0 0]

Upvotes: 1

Steffan Westcott
Steffan Westcott

Reputation: 2201

A list comprehension can be used to find coordinates of all values satisfying a predicate:

(defn find-locs [pred coll]
  (for [[i vals] (map-indexed vector coll)
        [j val] (map-indexed vector vals)
        :when (pred val)]
    [i j]))

(find-locs #(= 3 %) [[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]])
=> ([1 5])

(find-locs zero? [[0 1 1] [1 1 1] [1 0 1]])
=> ([0 0] [2 1])

The posed question seems to imply that the keywords in the inputs should be ignored, in which case the answer becomes:

(defn find-locs-ignore-keyword [pred coll]
  (for [[i vals] (map-indexed vector coll)
        [j val] (map-indexed vector (remove keyword? vals))
        :when (pred val)]
    [i j]))

(find-locs-ignore-keyword #(= 3 %) [[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]])
=> ([1 2])

Upvotes: 4

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 9910

A simpler solution, assuming 2D array where the inner vectors are key value vectors, uses flattening of the 2D array and .indexOf.

(defn find-coord [arr val]
  (let [m (count (first arr))
        idx (.indexOf (flatten arr) val)]
    [(quot idx m) (quot (dec (mod idx m)) 2)]))
(find-coord arr 3) ;;=> [1 2]

Upvotes: 0

Rulle
Rulle

Reputation: 4901

I might be over-engineering this answer slightly, but here is a non-recursive and non-lazy approach based on a single loop that will work for arbitrary and mixed levels of nesting and won't suffer from stack overflow due to recursion:

(defn find-key-value [array value]
  (loop [remain [[[] array]]]
    (if (empty? remain)
      nil
      (let [[[path x] & remain] remain]
        (cond (= x value) path
              (sequential? x)
              (recur (into remain
                           (comp (remove keyword?)
                                 (map-indexed (fn [i x] [(conj path i) x])))
                           x))
              :default (recur remain))))))

(find-key-value [[:a 1 :b 1 :c 1] [:a 1 :b 1 :c 3] [:a 1 :b 1 :c 1]] 3)
;; => [1 2]

(find-key-value [[:a 1 [[[[[:c]]]] [[[9 [[[3]] :k]] 119]]]] [:a [[[1]]] :b 1]] 3)
;; => [0 1 1 0 0 1 0 0 0]

(find-key-value (last (take 20000 (iterate vector 3))) 3)
;; => [0 0 0 0 0 0 0 0 0 0 0 0 0 ...]

Upvotes: 0

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 9910

(defn vec-to-map [v] (into {} (into [] (map vec (partition 2 v)))))
(defn vec-vals [v] (vals (vec-to-map v)))
(defn map-vec-index [v el] (.indexOf (vec-vals v) el))

(defn find-val-coord
  ([arr val] (find-val-coord arr val 0))
  ([arr val counter]
    (let [row (first arr)
          idx (map-vec-index row val)]
       (cond (< 0 idx) [counter idx]
             :else (recur (rest arr) val (inc counter)))))) 

(find-val-coord arr 3)    ;; => [1 2]

We can also write functions to pick value or corresponding key from array when coordinate is given:

(defn vec-keys [v] (keys (vec-to-map v)))

(defn get-val-coord [arr coord]
  (nth (vec-vals (nth arr (first coord))) (second coord)))

(defn get-key-coord [arr coord]
  (nth (vec-keys (nth arr (first coord))) (second coord)))


(get-val-coord arr [1 2]) ;; => 3
(get-key-coord arr [1 2]) ;; => :c

Upvotes: 0

Alan Thompson
Alan Thompson

Reputation: 29966

There is a map-indexed that is sometimes helpful. See the Clojure Cheatsheet and other docs listed here.

==> Could you please edit the question to clarify the search conditions?


Here is an outline of what you could do to search for the desired answer:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(defn coords
  [data pred]
  (let [result (atom [])]
    (doseq [row (range (count data))
            col (range (count (first data)))]
      (let [elem    (get-in data [row col])
            keeper? (pred elem)]
        (when keeper?
          (swap! result conj [row col]))))
    (deref result)))

(dotest
  (let [data [[11 12 13]
              [21 22 23]
              [31 32 33]]
        ends-in-2? (fn [x] (zero? (mod x 2)))]
    (is= (coords data ends-in-2?)
      [[0 1]
       [1 1]
       [2 1]])))

It is based on the same template project as the docs. There are many variations (for example, you could use reduce instead of an atom).

Please review the docs listed above.

Upvotes: 0

Related Questions