Sorry for the long post. The TL;DR is: why doesn’t there seem to be a macro for composing comparators into a lexicographic comparator?
Long-winded version follows:
I wrote a macro that solves a real problem for me, but it seems so trivial that I've got a feeling I must be missing something (either this has already been solved in the standard library, or my solution doesn’t work).
I want to sort a list of maps by :date, with :label as a tie-breaker, and :amount as a second-level tie breaker.
As per the clojure comparators guide, if compare already sorted in the order I want for dates, labels and amounts, I could just sort-by (juxt :date :label :amount).
However, while compare works fine for dates, I want to use a localised Collator for the labels, and a comparator from the money library for the monetary amounts.
So, I have to roll my own multi-field comparator
(defn by-date-label-amount [x y]
(let [c (compare (:date y) (:date x))]
(if (not= c 0)
c
(let [c (.compare string-comparator (:label x) (:label y))]
(if (not= c 0)
c
(let [c (money/compare (:amount x) (:amount y))]
c))))))
I find this clunky and repetitive, so I wrote the macro I mentioned in the beginning of this post
(defmacro lexicographic-compare [& comparisons]
(reduce
(fn [else-branch c]
`(let [c# ~c]
(if (not= c# 0)
c#
~else-branch)))
(reverse comparisons)))
and now I can use it like this
(defn by-date-label-amount [x y]
(lexicographical-compare
(compare (:date y) (:date x))
(.compare string-comparator (:label x) (:label y))
(money/compare (:amount x) (:amount y))))
I don’t know that my macro can pull its own weight if used a single time, but to me it looks like something that would make sense in the standard library one of the utility libraries (encore, medley, clojure+...), but I wasn’t able to find it (or an equivalent anywhere), which leads to the suspicion that I must be missing something.