David Furnam
David Furnam

Reputation: 55

Clojure getting a coordinate from arrays

I'm having trouble figuring out how I can find a coordinate of a character within an array.

I have small ASCII maps that look like this:

+-----+
|XPX X|
|X   X|
|X  XX|
|X   X|
|XX DX|
+-----+

This map is split up by lines into an array so the array looks like:

["+-----+" "|XPX X|" "|X   X|" "|X  XX|" "|X   X|" "|XX DX|" "+-----+"]

What I am trying to do is pass a character such as 'P' and find out the coordinate of the character in the map. In this instance 'P' is located at the coordinate (2,3).

As a starting point, I tried to figure out the column of the character using

(doseq [m map] (println (clojure.string/index-of m "P")))

This returned the output of

nil
2
nil
nil
nil
nil
nil
nil

At this point, I am now confused as to how I can just return '2' and the array index that this row is in.

Upvotes: 1

Views: 312

Answers (5)

leetwinski
leetwinski

Reputation: 17859

i would go with something like this (a bit more generalized)

(def field ["+-----+"
            "|XPX X|"
            "|X  ZX|"
            "|X  XX|"
            "|X   X|"
            "|XX DX|"
            "+-----+"])

(defn find-2d [pred field]
  (for [[i line] (map-indexed vector field)
        [j ch] (map-indexed vector line)
        :when (pred ch)]
    [i j ch]))

user> (find-2d #{\P} field)
;;=> ([1 2 \P])

user> (find-2d #{\P \Z} field)
;;=> ([1 2 \P] [2 4 \Z])

user> (find-2d #{\D \P \Z} field)
;;=> ([1 2 \P] [2 4 \Z] [5 4 \D])

user> (find-2d #(Character/isUpperCase %) field)
;;=> ([1 1 \X] [1 2 \P] [1 3 \X] [1 5 \X] 
;;    [2 1 \X] [2 4 \Z] [2 5 \X] [3 1 \X] 
;;    [3 4 \X] [3 5 \X] [4 1 \X] [4 5 \X] 
;;    [5 1 \X] [5 2 \X] [5 4 \D] [5 5 \X])

another one is more functional (although a less readable)

(defn find-2d [pred field]
  (filter (comp pred last)
          (mapcat #(map vector (repeat %1) (range) %2)
                  (range)
                  field)))

works exactly the same as the first one

Upvotes: 1

Solaxun
Solaxun

Reputation: 2792

One way to do it - assuming your input is a vector of strings as you have presented, is like so:

(def the-map ["+-----+" "|XPX X|" "|X   X|" "|X  XX|" "|X   X|" "|XX DX|" "+-----+"])

(defn get-index [item coll]
  (some #(when ((complement nil?) %) %)
         (map-indexed (fn [ix x] (let [i (clojure.string/index-of x item)]
                                  (when i [ix i])))
                      coll)))

This will return the first instance of whatever item you are looking for, e.g. for "+" it would be [0,0]. Also (2,3) is actually (1,2) as clojure uses zero-based indexing.

You can shorten this a little by using keep with identity

(defn get-index [item coll]
  (first (keep identity
               (map-indexed (fn [ix x] (let [i (clojure.string/index-of x item)]
                                        (when i [ix i])))
                            coll))))

EDIT:

I realized it's redundant to have (first (keep identity....), instead just use some with identity:

(defn get-index [item coll]
  (some identity 
        (map-indexed (fn [ix x] (let [i (clojure.string/index-of x item)]
                                     (when i [ix i])))
                                   coll)))

Some answers below gave you a way to get every matching coordinate, rather than the first. All you have to do is change some identity to remove nil? in my above version to accomplish this.

For example:

(defn get-index [item coll]
  (remove nil? 
        (map-indexed (fn [ix x] (let [i (clojure.string/index-of x item)]
                                     (when i [ix i])))
                                   coll)))

Finally, if it is really your intention to have indexes be one-based, you can just increment each found index:

(defn get-index [item coll]
  (some identity 
        (map-indexed (fn [ix x] (let [i (clojure.string/index-of x item)]
                                     (when i [(inc ix) (inc i)])))
                                   coll)))

Upvotes: 2

Alan Thompson
Alan Thompson

Reputation: 29958

I added another P char to show how to find all of them. Here is a simpler solution using for:

(defn strs->array [strs] (mapv vec strs))

(def data ["+-----+"
           "|XPX X|"
           "|X   X|"
           "|X  XX|"
           "|X P X|"
           "|XX DX|"
           "+-----+"])
(def data-array (strs->array data))

(defn find-chars-2 [ch-array tgt-char]
  (let [num-rows (count ch-array)
        num-cols (count (first ch-array))]
    (for [ii (range num-rows)
          jj (range num-cols)
          :let [curr-char (get-in ch-array [ii jj])]
          :when (= tgt-char curr-char)]
      [ii jj])))

with result:

(find-chars-2 data-array \P))  => ([1 2] [4 3])

Using get-in assumes you have nested vectors, hence the need for strs->array. We also assume the data is rectangular (not ragged) and haven't put in any error-checking that a real solution would need.

Upvotes: 0

akond
akond

Reputation: 16035

I went a little bit further and wrote a function that find coordinates of all the occurrences of a char.

(let [a ["+-----+" "|XPX X|" "|X   X|" "|X  XX|" "|X   X|" "|XX DX|" "+-----+"]]
        (letfn [(find-coords [a ch]
                    (let [op (fn [f] (comp inc #(f % (count (first a)))))]
                        (->> a
                             (clojure.string/join "")
                             (map-indexed vector)
                             (filter (comp (partial = ch) second))
                             (map first)
                             (map (juxt (op quot) (op rem))))))]
            (find-coords a \P)))

=> ([2 3])

(find-coords a \X)
=> ([2 2] [2 4] [2 6] [3 2] [3 6] [4 2] [4 5] [4 6] [5 2] [5 6] [6 2] [6 3] [6 6])

Upvotes: 1

cjg
cjg

Reputation: 2684

(def strmap ["+-----+" "|XPX X|" "|X   X|" "|X  XX|" "|X   X|" "|XX DX|" "+-----+"])

(defn find-char-2d
  [arr char-to-find]
  (some->> (map-indexed (fn [i row]
                          [i (clojure.string/index-of row char-to-find)])
                        arr)
           (filter second)
           first
           (map inc)))

(println (find-char-2d strmap "Q")) ; prints "nil"
(println (find-char-2d strmap "P")) ; prints "(2, 3)"

The map-indexed loops through the row strings, searching each one for your substring while keeping track of the row index i. The some->> threading macro passes the result (which is a LazySeq) to the filter, which removes all elements with nil columns. Since we only want the first coordinates (assuming the character you're searching for can only exist once in the map), we select the first element. If the substring doesn't exist in the map, we will get nil, and the the some->> will short-circuit so that nil is returned. Otherwise, the indices will both be incremented (since your coordinates are 1-indexed.

Alternatively, you could linearize the your map, find the index in the linear map, and then calculate the 2d coordinates from the linear index:

(defn find-char-2d
  [arr char-to-find]
  (some-> (clojure.string/join "" arr)
          (clojure.string/index-of char-to-find)
          ((juxt #(-> (/ % (count arr)) int inc)
                 #(inc (mod % (count (first arr))))))))

Upvotes: 1

Related Questions