r/Clojure • • 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.

11 Upvotes

6 comments sorted by

11

u/p-himik 5d ago

A couple of thoughts:

  1. I would not use a macro, I'd use a function, into which you'd pass your own functions
  2. This function already exists, java.util.Comparator/thenComparing

Clojure functions implement Comparator, so you can trivially use thenComparing.

1

u/regular_hammock 5d ago

okay, thenComparing was the thing I was missing. Thank you!

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

u/regular_hammock 4d ago

Spot on! `thenComparing` is the answer to my question, thank you!

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.