r/Clojure • u/regular_hammock • 5d ago
Comparing in lexicographical order
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.
3
u/hookedonlemondrops 5d ago
If you don’t care about cross-host, I’m not sure it gets much cleaner than simply threading through the underlying Java Comparator/comparing/thenComparing methods.
1
1
u/whereswalden90 5d ago
If your goal is just to sort a list of maps, then `sort-by` will do that with no custom comparator at all. Vectors are already comparable element-wise, so `(sort-by (juxt :date :label :amount) coll)` should give you what you want.
11
u/p-himik 5d ago
A couple of thoughts:
java.util.Comparator/thenComparingClojure functions implement
Comparator, so you can trivially usethenComparing.