Welcome! Please see the About page for a little more info on how this works.

0 votes
in Clojure by

I am writing a slack bot that can take a group of people and the office that they work for and then break them into n random groups, making sure to mix the two offices together.

I have a few functions that do this just fine, but I am wondering if there is a more "Clojure" way to do this? I am still trying to break out of my object-oriented habits so any suggestions are very welcome.

My thought for this approach was to treat this like a deck of cards and deal the people out into each resulting group until I run out of people in an office, then move to the next one until I run out of offices then return my result.

(defn split 
  "split by office then shuffle
   result is sorted by size"
  [coll]
  (->> (sort-by :office coll)
       (partition-by :office)
       (sort-by count)
       (map shuffle)))

(defn conj-in 
  "inserts an item into a collection's child"
  [coll idx itm]
    (assoc coll idx (conj (nth coll idx) itm)))

(defn split-into-groups
  [coll]
  (let [sorted (split coll)]
    (loop [pool    (rest sorted)
           hand    (first sorted)
           result  (into [] (take @ngroups (repeat [])))
           idx     0]
      
      (if (not-empty hand)
        (let [next (first hand)]
          (recur pool
                 (rest hand)
                 (conj-in result idx next)
                 (mod (inc idx) @ngroups)))
        
        (if (not-empty pool)
          (recur (rest pool)
                 (first pool)
                 result
                 idx)
          
          result)))))

1 Answer

+1 vote
by
edited by

Perhaps interleave:

> (interleave [1 2 3] [:a :b :c] ["X" "Y"])
(1 :a "X" 2 :b "Y")

So, something like:

(defn people->groups
  [people n-groups]
  (let [group-size (quot (count people) n-groups)]
    (->> (group-by :office people)
         (map (comp shuffle second))
         (apply interleave)
         (partition group-size group-size []))))

Used like:

> (def ppl (for [i (range 100)] (let [office (rand-nth [:a :b :c])] {:name (str office "-" i) :office office})))
#'ppl
> (rand-nth ppl)
{:name ":a-19", :office :a}
> (people->groups ppl 4)
...

A few thoughts:

  • There are some edge cases to work out, depending on what you believe should happen when the number of people doesn't evenly divide the number of groups. What does the current implementation do? Why? Getting the 'right' thing to happen is left as a fun exercise - maybe go test-first to calcify the behavior to your liking.

  • Starting with a sequence of maps, as you have, is good - that's what I would do

  • Your code implies ngroups is some non-local derefable, a function parameter may make more sense

  • I find I can almost always rewrite code that uses loop/first/rest with things in clojure.core. Learn as much clojure.core as you can.


Hope that helps. Have fun! You're well on your way.

...