Here's an approach to convert arbitrary expressions into a form that is sorted "in reverse" (full code at the end):
(* a list of arbitary expressions *)
list = {"a", "b", f[1, 2], f[1]};
Sort@list
(* {"a", "b", f[1], f[1, 2]} *)
(* create a list of wrapped items that will be sorted in reverse *)
list2 = ReverseSorted /@ list
(* {ReverseSorted["a"], ReverseSorted["b"], ReverseSorted[f[1, 2]], ReverseSorted[f[1]]} *)
Sort@list2
(* {ReverseSorted[f[1, 2]], ReverseSorted[f[1]], ReverseSorted["b"], ReverseSorted["a"]} *)
(* convert back *)
Normal[Sort@list2, ReverseSorted]
(* {f[1, 2], f[1], "b", "a"} *)
(* use SortBy to avoid the need for back-conversion *)
SortBy[list, ReverseSorted]
(* {f[1, 2], f[1], "b", "a"} *)
(* example from the question *)
dataset[SortBy[{#b, ReverseSorted@#a} &]]
General strategy
This method works by converting any expression into a canonical form that allows us to control its sorting behavior.
- We wrap all expressions in a
ReverseSorted
wrapper
- The wrapper remembers the original expression in the second argument (for back-conversion)
- The first argument contains a processed form of the wrapped expression that ensures reverse-sorting
- To actually get the reverse-sorting, we look at the canonical ordering for Mathematica, and figure out a way to reverse each rule:
- The general order is numbers, strings, symbols, expressions
- -> We create lists of the form
{4, number}
, {3, string}
, etc. This way, the order is controlled by the type of the expression, but since e.g. 4>3, strings will come before numbers, etc,
- Numbers are sorted by their real part, then imaginary part
- -> Simply take the negative of the number
- Strings are sorted lexicographically, with special rules for character ordering
- -> We generate a list of all characters, sort it, and then replace the first character with the last, the second with the second-to-last, etc. To avoid the length of the string from mattering, we create a nested list from the characters of the string, so
{"a", "b", "c"}
becomes {"a", {"b", {"c", M[]}}}
. The M[]
ensures that shorter strings are sorted after longer ones (since M[]
comes after List[]
)
- Symbols are sorted by their names, then contexts
- -> We take the name of the symbols and apply the same procedure as for strings (contexts are currently ignored, see also notes in next section)
- Expressions are sorted by their head, then length, and then arguments
- -> We create a list
{reverseHead, -length, {reverseArgs}}
, where the reversing strategy is recursively applied to the head and each argument. (The nesting of the lists ensures that the ordering does not depend on the number of arguments in an uncontrolled way)
Advantages & Limitations
- Compared to the approach by @kglr, this approach does not need "global" knowledge, i.e. you don't need to know the other expressions to be sorted
- Use of this method is very easy, simply wrap any expression (or part of an expression) in
ReverseSorted
if you want to flip the ordering
- The current version will not work properly for symbols with different contexts, this would have to be implemented properly
- The current version will probably not work properly for some atomic expressions, e.g.
Image
or Graph
, since they are not handled explicitly by the code
- The current version might not work for arbitrary strings if the characters are not in the list
CharacterRange[0, 1114111]
(which should include any character represented by a single character code, but I'm not sure whether there could be any other characters)
Code
Attributes[ReverseSorted] = {HoldAll};
ReverseSorted[expr_] :=
ReverseSorted[reverseExpr@expr // Evaluate, expr]
MakeBoxes[full : ReverseSorted[_, expr_], frm : StandardForm] :=
With[
{boxes = MakeBoxes[ReverseSorted@expr, frm]},
InterpretationBox[boxes, full]
]
Normal[expr_, ReverseSorted] ^:=
expr /. ReverseSorted[_, e_] :> e
reverseCharacters =
ReplaceAll@Dispatch@Thread[# -> SortBy[#, FromCharacterCode]] &@Range[0, 1114111];
toNestedList[{a_, rest___}] :=
{a, toNestedList@{rest}}
toNestedList[{}] :=
M[]
reverseString[str_] :=
toNestedList@reverseCharacters@ToCharacterCode@str
Attributes[reverseExpr] = {HoldFirst};
reverseExpr[n_?NumberQ] :=
{4, -n}
reverseExpr[s_String] :=
{3, reverseString@s}
reverseExpr[s : HoldPattern@Except[Symbol[___], _Symbol]] :=
{2, reverseString@SymbolName@Unevaluated@s}
reverseExpr[h_[args___]] :=
{1, {reverseExpr[h], -Length@Unevaluated@{args}, reverseExpr /@ Unevaluated@{args}}}
dataset[SortBy[{#b &, -#a &}]]
? $\endgroup$