better way to group consecutive numbers in a vector?

632 views
Skip to first unread message

John Gabriele

unread,
Nov 6, 2014, 5:22:14 PM11/6/14
to clo...@googlegroups.com
Hi all,

I've got this: `[1 3 4 5 7 9 10 11 12]`
and I'd like to turn it into this: `[[1] [3 4 5] [7] [9 10 11 12]]`.

That is, I'd like to group consecutive numbers together (the final goal being to produce something like `["1" "3-5" "7" "9-12"]`, but that's the easy part).

I haven't found an easy way to do this. Here's what I've come up with in Clojure:

~~~clojure
#!/usr/bin/env lein-exec

(def v [1 3 4 5 7 9 10 11 12])

(defn congeal-consecutives
  [coll]
  (reduce (fn [accum x]
            (if (= x (inc (last (last accum))))
              (concat (butlast accum)
                      [(conj (last accum) x)])
              (concat accum
                      [[x]])))
          [[(first coll)]]
          (rest coll)))

(prn (congeal-consecutives v))
~~~

and here's it done in Python:

~~~python
#!/usr/bin/python3

v = [1, 3, 4, 5, 7, 9, 10, 11, 12]

def congeal_consecutives(coll):
    accum = [[ coll[0] ]]
    more  = coll[1:]
    for x in more:
        if x == accum[-1][-1] + 1:
            accum[-1].append(x)
        else:
            accum.append([x])
    return accum

print(congeal_consecutives(v))
~~~

Can anyone suggest a better / simpler / more easily-understandable way to do this in Clojure?

Thanks,
-- John

Ashton Kemerling

unread,
Nov 6, 2014, 5:51:05 PM11/6/14
to clo...@googlegroups.com
Consider using partition-by. I can't type clojure on my phone, but the following link might help. https://clojuredocs.org/clojure.core/partition-by

--Ashton

Sent from my iPhone
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Steve Miner

unread,
Nov 6, 2014, 7:13:57 PM11/6/14
to clo...@googlegroups.com
I would try to avoid last and but-last as they work in linear time. How about something like this?

(defn congeal-consecutives [coll]
(when (seq coll)
(let [[_ group res] (reduce (fn [[succ group res] n]
(if (== succ n)
[(inc n) (conj group n) res]
[(inc n) [n] (conj res group)]))
[(nth coll 0) [] []]
coll)]
(conj res group))))

john walker

unread,
Nov 6, 2014, 7:27:31 PM11/6/14
to clo...@googlegroups.com
Hi John,

As miner mentioned, peek is preferable to last for vectors. Here is an implementation based on the observation that consecutive increasing numbers are equivalent when you subtract their indices. Maybe it is more clever than simple.

(ns experimental-clojure.congeal-consecutives)

(def v [1 3 4 5 7 9 10 11 12])
(defn congeal-consecutives [coll]
 (->> coll
      (map-indexed (fn [i x] [(- x i) x]))
      (partition-by first)
      (mapv (fn [pairs]
              (mapv second pairs)))))
(defn rangify [coll]
 (mapv (fn [r]
         (assert (vector? coll))
         (let [f    (first r)
               top  (peek  r)]
           (if (= f top)
             (str f)
             (str f "-" top)))) coll))
(-> v
   congeal-consecutives
   rangify)

https://gist.github.com/johnwalker/8e7e6a8bcbeff03d4e80

Gordon Stratton

unread,
Nov 6, 2014, 8:11:31 PM11/6/14
to clo...@googlegroups.com
On Thu, Nov 6, 2014 at 10:22 PM, John Gabriele <jmg...@gmail.com> wrote:
> Hi all,
>
> I've got this: `[1 3 4 5 7 9 10 11 12]`
> and I'd like to turn it into this: `[[1] [3 4 5] [7] [9 10 11 12]]`.
>
> That is, I'd like to group consecutive numbers together (the final goal
> being to produce something like `["1" "3-5" "7" "9-12"]`, but that's the
> easy part).
>
> I haven't found an easy way to do this. Here's what I've come up with in
> Clojure:
>
> ~~~clojure
> #!/usr/bin/env lein-exec
>
> (def v [1 3 4 5 7 9 10 11 12])
>
> (defn congeal-consecutives
> [coll]
> (reduce (fn [accum x]
> (if (= x (inc (last (last accum))))
> (concat (butlast accum)
> [(conj (last accum) x)])
> (concat accum
> [[x]])))
> [[(first coll)]]
> (rest coll)))
>
> (prn (congeal-consecutives v))
> ~~~
>
> Can anyone suggest a better / simpler / more easily-understandable way to do
> this in Clojure?

Hey John,

I think your approach is pretty good. I made some small modifications:

(defn congeal-consecutives
[xs]
(if (empty? xs)
[]
(reduce (fn [acc x]
(let [l (last acc)]
(if (= (inc (last l)) x)
(conj (pop acc) (conj l x))
(conj acc [x]))))
[[(first xs)]]
(rest xs))))

I didn't see how to use `partition-by` in a way that made the function
any clearer than the `reduce` version, but I'd be interested if you or
others do.

blake

unread,
Nov 6, 2014, 8:48:07 PM11/6/14
to clo...@googlegroups.com
I wanted to put the delimiters in one step and then split in a different one, so I did this:

(defn delimit[v]
  (reduce #(if (= (last %) (dec %2))
            (conj % %2)
            (conj % :split %2))
        [(first v)] (rest v)))

(delimit [1 3 4 5 7 9 10 11 12])
=> [1 :split 3 4 5 :split 7 :split 9 10 11 12]

But that was before I realized there was no equivalent to clojure.string/split that works on lists.

John Gabriele

unread,
Nov 6, 2014, 10:38:41 PM11/6/14
to clo...@googlegroups.com

Oooh, that's a good idea. Thanks. It's easy enough to split after you've added those markers/gaps in:

~~~clojure
#!/usr/bin/env lein-exec

(def v [1 3 4 5 7 9 10 11 12])

(defn insert-gaps

  [coll]
  (reduce (fn [accum x]
            (if (= x (inc (last accum)))
              (conj accum x)
              (conj accum :gap x)))
          [(first coll)]
          (rest coll)))

(defn congeal-consecutives
  "Assumes coll has :gaps."
  [coll]
  (let [res (partition-by (fn [x]
                            (= x :gap))
                          coll)]
    (remove (fn [x]
              (= x '(:gap)))
            res)))

(prn (congeal-consecutives (insert-gaps v)))
~~~
 

Ben Wolfson

unread,
Nov 6, 2014, 10:53:59 PM11/6/14
to clo...@googlegroups.com
I don't know if this is *clearer* than the reduce version ...

user> (def xs [1 3 4 5 7 9 10 11 12 ])
#'user/xs
user> (defn consecutivizer []
        (let [prev (atom nil)
              last-returned (atom (gensym))]
          (fn [cur]
            (when-not (or (nil? @prev)
                          (= (inc @prev) cur))
              (reset! last-returned (gensym)))
            (reset! prev cur)
            @last-returned)))
#'user/consecutivizer
user> (partition-by (consecutivizer) xs)
((1) (3 4 5) (7) (9 10 11 12))




--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
Ben Wolfson
"Human kind has used its intelligence to vary the flavour of drinks, which may be sweet, aromatic, fermented or spirit-based. ... Family and social life also offer numerous other occasions to consume drinks for pleasure." [Larousse, "Drink" entry]

vve...@gmail.com

unread,
Nov 7, 2014, 3:46:14 AM11/7/14
to clo...@googlegroups.com
(let [data [1 3 4 5 7 9 10 11 12]
      seen (atom (first data))]
  (partition-by #(if (< (- % @seen) 2)
                   (do (reset! seen %) true) 
                   (do (reset! seen %) false))
                data))
((1) (3 4 5) (7) (9 10 11 12))

Paweł Rozynek

unread,
Nov 7, 2014, 7:09:21 AM11/7/14
to clo...@googlegroups.com

(def data '(1 3 4 5 7 9 10 11 12))
(map #(map last %) (partition-by #(apply - %) (map-indexed vector data))) => ((1) (3 4 5) (7) (9 10 11 12))

regards
PR

Alex Hammel

unread,
Nov 7, 2014, 12:57:25 PM11/7/14
to clo...@googlegroups.com
Here's my take on the 'add delimiters and split' approach. Bonus `congeal function, which chunks collections on any condition you like:

(defn- insert-gaps
  [coll pred gap-symbol]
  (reduce (fn [acc x]
            (if (pred (peek acc) x)
                (conj acc x)
                (conj acc gap-symbol x)))
          [(first coll)]
          (rest coll)))

(defn split-coll
  [coll delimiter]
  (->> coll
       (partition-by (partial = delimiter))
       (remove (partial = (list delimiter)))))

(defn congeal
  [pred coll]
  (let [gap-symbol (gensym)]
    (-> coll
        (insert-gaps pred gap-symbol)
        (split-coll gap-symbol))))

(defn consecutive? [p q]
  (= (inc p) q))

(println (congeal consecutive? [1 3 4 5 7 9 10 11 12]))
#=> ((1) (3 4 5) (7) (9 10 11 12))
(println (congeal < [1 2 3 1 2 3 1 2 3]))
#=> ((1 2 3) (1 2 3) (1 2 3))
(println (congeal not= [:foo :foo :bar :bar :foo :bar]))
#=> ((:foo) (:foo :bar) (:bar :foo :bar))




--

Jan-Paul Bultmann

unread,
Nov 7, 2014, 1:21:39 PM11/7/14
to clo...@googlegroups.com
I think what you want is `partition-between` as implemented by amalloys useful lib (https://github.com/amalloy/useful/blob/develop/src/flatland/useful/seq.clj#L224).

`(partition-between (fn [x y]
                                    (not= (inc x) y))
                               coll)`

Why this isn’t in the standard library is beyond me tbh,
as `partition-by` is a special case for `partition-between`.

`(def partition-by (partial partition-between not=))`

Maybe adding it to clojure.core should be considered?
Also I’d be intrigued to know the rationale behind `partition-by`.

Cheers, Jan

Rob Jentzema

unread,
Oct 1, 2016, 3:51:54 AM10/1/16
to Clojure
Nice, I like that solution. Clean, logical and semantical :) 

(defn consecutive? [[x y]] (= (inc x) y))

(def nonconsecutive? (complement consecutive?))

(partition-between nonconsecutive? [1 2 3 4 6 7 9 11 14 15])
;=> ([1 2 3 4] [6 7] [9] [11] [14 15])

(partition-between consecutive? [1 2 3 4 6 7 9 11 14 15])
;=> ([1] [2] [3] [4 6] [7 9 11 14] [15])

Reply all
Reply to author
Forward
0 new messages