4
votes

I'm learning clojure (coming from ruby) and having some trouble wrapping my head around the best way to generate collections.

I'd like to write a function that takes two arguments -- the vector ary and integer sum -- and generates a new 2D vector in which each row's sum is <= the input sum (ignore input validation). The conceptual point I'm having trouble with is how to keep the state of "sum of current row" while also building up a new collection.

Here's what I've got:

(defn split-after-sum [ary sum]
  (reduce (fn [acc i]
            (let [inner-sum (+ (last acc) i)]
              (if (< inner-sum sum)
                [(conj (first acc) i) (+ i (last acc))]
                [(conj (first acc) i "X") 0])))
          [[] 0] ary))

I'm passing reduce a 2 element vector in order to keep track of both the collection I'm building and the total count of this row.

It sort of works. I haven't figured out how to actually make the result a 2D array so it's just sticking "X"s where the splits should be:

(first (split-after-sum [1 1 1 1 1 1 1 1 1] 2)) => [1 1 "X" 1 1 "X" 1 1 "X" 1 1 "X" 1]

Ideal output would be:

(split-after-sum [1 1 1 1 1 1 1 1 1] 2) => [[1 1] [1 1] [1 1] [1 1] [1]]

I know there are a few things jumbled up in here, but I think an idiomatic answer to this problem would be enlightening.

2

2 Answers

7
votes
(defn split-after-sum [ary sum]
  (let [[acc v] (reduce (fn [[acc v s] x]
                          (let [new-s (+ s x)]
                            (if (<= new-s sum)
                              [acc (conj v x) new-s]
                              [(conj acc v) [x] x])))
                        [[] [] 0]
                        ary)]
    (conj acc v)))

(split-after-sum [1 1 3 2 1 1 1 1 1] 3)
;= [[1 1] [3] [2 1] [1 1 1] [1]]
(split-after-sum [1 1 3 2 1 1 1 1 1] 4)
;= [[1 1] [3] [2 1 1] [1 1 1]]
(split-after-sum [1 1 3 2 1 1 1 1 1] 5)
;= [[1 1 3] [2 1 1 1] [1 1]]
(split-after-sum [1 1 3 2 1 1 1 1 1] 6)
;= [[1 1 3] [2 1 1 1 1] [1]]
2
votes

As Michał shows, the cumulative state a can be joined in a tuple with the associated element.

Below is an imperative approach where the state is held in a loop binding.

(defn split-after-sum [ary sum]
  (when (seq ary)
    (loop [ary ary
           cur 0
           row []
           rows []]
      (if-let [[x & xs] ary]
        (let [nxt (+ x cur)]
          (if (<= nxt sum)
            (recur xs nxt (conj row x) rows)
            (recur xs x [x] (conj rows row))))
        (conj rows row)))))

Aside: the argument order should probably be reversed. Functions that work over elements of sequences tend to take the sequence as the last argument.