Sebastian_學生
Sebastian_學生

Reputation: 345

Clojure iterate a vector and look ahead/look behing

I do have to iterate over a vector, which in turn has maps as its items. I need to compare which map comes next, and sometimes I need to look what was in the map we looked at before. So it is necessary to have some kind of look ahead/look behind functionality. My current approach works, but I guess it is ugly, unidiomatic Clojure and I assume that there must be a better (more canonical) way to achieve this.

(let [result (apply str (map (fn [{position-before :position compound-before :compund } ; previous term (unfortunately we need to compare all three)
                                        {:keys [word position line tag stem compound grammarpos] :or {grammarpos "0" stem "" } } ; this maps to the current word
                                        {position-ahead :position compound-ahead :compound line-ahead :line}] ; this is for lookahead
                (do some stuff)) ;; now we have position, which is our current position, position-before and position-after to compare with each other
                ;; this is how we map:
                (into  '[{}] (conj grammar '[{}]))
                (next (into  '[{}] (conj grammar '[{}])))
                (next (next (into  '[{}] (conj grammar '[{}]))))))])

As for the request of the data-example, this is a part of the vector:

[{:tag "0", :position "0", :line "0", :stem "dev", :grammarpos "2625", :word "deva"} {:tag "0", :position "0", :line "0", :stem "deva", :grammarpos "4", :word "deva"}]

The job is to compare values for position, compound etc., sometimes look ahead, sometimes look behind.

Upvotes: 2

Views: 476

Answers (3)

DanLebrero
DanLebrero

Reputation: 8593

If you want to do really complex things, perhaps zippers will be a better solution.

For example, lets say that you start with:

(def x
  [{:tag "0" :dups 0}
   {:tag "1" :dups 0}
   {:tag "1" :dups 0}
   {:tag "3" :dups 0}])

And your requirements are to increment the dups counter of all consecutive tags with the same name and add a "---" tag between them.

With zippers the solution will look like:

(require '[clojure.zip :as zip :refer [root node]])

(defn complex-thing [zip]
  (if (zip/end? zip) ;; are we done?
    (root zip) ;; return the "updated" vector
    (let [current-node (node zip)
          before-node (node (zip/prev zip))] ;; you can access any map in the vector, both before or after
      (if (= (:tag current-node) (:tag before-node))
        (recur (-> zip
                   zip/prev ;; move to the previous map
                   (zip/edit update :dups inc) ;; increment it
                   zip/next ;; move back to the current map
                   (zip/edit update :dups inc)
                   (zip/insert-left {:tag "----"}) ;; insert "---" before the current tag
                   zip/next)) ;; move to next map to process
        (recur (zip/next zip))))))

(complex-thing (zip/next (zip/next (zip/vector-zip x)))) ;; start from the second element of the vector

[{:tag "0", :dups 0} 
 {:tag "1", :dups 1} 
 {:tag "----"} 
 {:tag "1", :dups 1} 
 {:tag "3", :dups 0}]

Upvotes: 1

leetwinski
leetwinski

Reputation: 17859

also if you need all the preceding and following items for every item, you can combine for list comprehension, with destructuring.

for example:

user> (def items [:a :b :c :d :e :f :g])
#'user/items

user> (for [index (range (count items))
            :let [[before [current & after]] (split-at index items)]]
        {:before before :current current :after after})

({:before (), :current :a, :after (:b :c :d :e :f :g)} 
 {:before (:a), :current :b, :after (:c :d :e :f :g)} 
 {:before (:a :b), :current :c, :after (:d :e :f :g)} 
 {:before (:a :b :c), :current :d, :after (:e :f :g)} 
 {:before (:a :b :c :d), :current :e, :after (:f :g)} 
 {:before (:a :b :c :d :e), :current :f, :after (:g)} 
 {:before (:a :b :c :d :e :f), :current :g, :after nil})

you just split collection at every item's index one by one, and from the result take first item (before), first of second item (current), rest of second item (after)

also a bit less readable way (but probably more productive for big collection, since it doesn't do take/drop on every step, but adds/removes a single item to coll)

user> (take (count items)
            (iterate
             (fn [[before current after]]
               [(conj before current) (first after) (rest after)])
             [[] (first items) (rest items)]))

([[] :a (:b :c :d :e :f :g)] 
 [[:a] :b (:c :d :e :f :g)] 
 [[:a :b] :c (:d :e :f :g)] 
 [[:a :b :c] :d (:e :f :g)] 
 [[:a :b :c :d] :e (:f :g)] 
 [[:a :b :c :d :e] :f (:g)] 
 [[:a :b :c :d :e :f] :g ()])

Upvotes: 3

johnbakers
johnbakers

Reputation: 24771

You could iterate over a partition of your vector, with a size of 3 and step of 1. Then for each element in the vector, you also get the before and after that you can study as you iterate with a for or reduce.

Some examples: https://clojuredocs.org/clojure.core/partition

Upvotes: 13

Related Questions