14

I have a sequence s and a list of indexes into this sequence indexes. How do I retain only the items given via the indexes?

Simple example:

(filter-by-index '(a b c d e f g) '(0 2 3 4)) ; => (a c d e)

My usecase:

(filter-by-index '(c c# d d# e f f# g g# a a# b) '(0 2 4 5 7 9 11)) ; => (c d e f g a b)
qollin
  • 1,271
  • 1
  • 14
  • 15

8 Answers8

28

You can use keep-indexed:

(defn filter-by-index [coll idxs]
  (keep-indexed #(when ((set idxs) %1) %2) 
                coll))  

Another version using explicit recur and lazy-seq:

(defn filter-by-index [coll idxs]
  (lazy-seq
   (when-let [idx (first idxs)]
     (if (zero? idx)
       (cons (first coll)
             (filter-by-index (rest coll) (rest (map dec idxs))))
       (filter-by-index (drop idx coll)
                        (map #(- % idx) idxs))))))
Eric
  • 2,300
  • 3
  • 22
  • 29
Jonas
  • 19,422
  • 10
  • 54
  • 67
13

make a list of vectors containing the items combined with the indexes,

(def with-indexes (map #(vector %1 %2 ) ['a 'b 'c 'd 'e 'f] (range)))
#'clojure.core/with-indexes
 with-indexes
([a 0] [b 1] [c 2] [d 3] [e 4] [f 5])

filter this list

lojure.core=> (def filtered (filter #(#{1 3 5 7} (second % )) with-indexes))
#'clojure.core/filtered
clojure.core=> filtered
([b 1] [d 3] [f 5])

then remove the indexes.

clojure.core=> (map first filtered)                                          
(b d f)

then we thread it together with the "thread last" macro

(defn filter-by-index [coll idxs] 
    (->> coll
        (map #(vector %1 %2)(range)) 
        (filter #(idxs (first %)))
        (map second)))
clojure.core=> (filter-by-index ['a 'b 'c 'd 'e 'f 'g] #{2 3 1 6}) 
(b c d g)

The moral of the story is, break it into small independent parts, test them, then compose them into a working function.

Arthur Ulfeldt
  • 90,827
  • 27
  • 201
  • 284
  • Great, thx! I had something like that, but I couldn't figure out to use "range" properly. – qollin Oct 12 '11 at 19:07
  • ohh and note that I switched the list of indexes for a vector of indexes. This is because sets can be used as a filter function. – Arthur Ulfeldt Oct 12 '11 at 19:41
10

The easiest solution is to use map:

(defn filter-by-index [coll idx]
  (map (partial nth coll) idx))
Leonid Beschastny
  • 50,364
  • 10
  • 118
  • 122
7

I like Jonas's answer, but neither version will work well for an infinite sequence of indices: the first tries to create an infinite set, and the latter runs into a stack overflow by layering too many unrealized lazy sequences on top of each other. To avoid both problems you have to do slightly more manual work:

(defn filter-by-index [coll idxs]
  ((fn helper [coll idxs offset]
     (lazy-seq
      (when-let [idx (first idxs)]
        (if (= idx offset)
          (cons (first coll)
                (helper (rest coll) (rest idxs) (inc offset)))
          (helper (rest coll) idxs (inc offset))))))
   coll idxs 0))

With this version, both coll and idxs can be infinite and you will still have no problems:

user> (nth (filter-by-index (range) (iterate #(+ 2 %) 0)) 1e6)
2000000

Edit: not trying to single out Jonas's answer: none of the other solutions work for infinite index sequences, which is why I felt a solution that does is needed.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • I wonder which is a more remote edge-case, an infinite sequence of indices to keep, or indices not in monotonically increasing order. – Alex Taggart Oct 13 '11 at 07:17
  • @AlexTaggart good point, since it's impossible to cater for both I guess you have to make a decision. Laziness seems more important to me, as part of the general Clojure philosophy, but for a given instance of the problem it could easily be wrong. – amalloy Oct 13 '11 at 08:17
1

I had a similar use case and came up with another easy solution. This one expects vectors.

I've changed the function name to match other similar clojure functions.

(defn select-indices [coll indices]
   (reverse (vals (select-keys coll indices))))
Paul English
  • 937
  • 11
  • 18
0
=> (defn filter-by-index [src indexes]
     (reduce (fn [a i] (conj a (nth src i))) [] indexes))

=> (filter-by-index '(a b c d e f g) '(0 2 3 4))
[a c d e]
Jarppe
  • 173
  • 4
  • 7
0

I know this is not what was asked, but after reading these answers, I realized in my own personal use case, what I actually wanted was basically filtering by a mask.

So here was my take. Hopefully this will help someone else.

(defn filter-by-mask [coll mask]
  (filter some? (map #(if %1 %2) mask coll)))

(defn make-errors-mask [coll]
  (map #(nil? (:error %)) coll))

Usage

(let [v [{} {:error 3} {:ok 2} {:error 4 :yea 7}]
    data ["one" "two" "three" "four"]
    mask (make-errors-mask v)]
    (filter-by-mask data mask))

; ==> ("one" "three")
HeyWatchThis
  • 21,241
  • 6
  • 33
  • 41
0
(defn filter-by-index [seq idxs]
  (let [idxs (into #{} idxs)]
    (reduce (fn [h [char idx]]
              (if (contains? idxs idx)
                (conj h char) h))
            [] (partition 2 (interleave seq (iterate inc 0))))))

(filter-by-index [\a \b \c \d \e \f \g] [0 2 3 4])
=>[\a \c \d \e]
Hamza Yerlikaya
  • 49,047
  • 44
  • 147
  • 241