6
$\begingroup$

The goal is to sort long lists of versions. For example:

Input

versions={"openmpi@1.8.4", "openmpi@1.10", "openmpi@1.10.6", "openmpi@2.0.2",
"openmpi@1.6", "openmpi@1.6.2", "openmpi@2.0"}

Desired Output

SortedVersions={"openmpi@2.0.2", "openmpi@2.0","openmpi@1.10.6", "openmpi@1.10", "openmpi@1.8.4", "openmpi@1.6.2", 
    "openmpi@1.6"}

Assume the package (here openmpi) does not change, and the format is package@version with version being n.n.n or n.n

My method is very cumbersome. Surely some Mathematica wizardry can bring relief.

Bonus: allow the option to sort descending (shown here) or ascending.

$\endgroup$
  • $\begingroup$ Related. $\endgroup$ – corey979 May 2 '18 at 21:54
  • $\begingroup$ @corey979: Yes, this is related. I'm struggling with the n.n.n or n.n format. $\endgroup$ – dantopa May 2 '18 at 21:56
3
$\begingroup$
versionList = ToExpression@StringSplit[Last@StringSplit[#, "@"], "."] &;

Reverse@SortBy[versions, versionList]
(* {"openmpi@2.0.2", "openmpi@1.10.6", "openmpi@1.8.4", "openmpi@1.6.2", "openmpi@2.0", "openmpi@1.10", "openmpi@1.6"} *)
$\endgroup$
8
$\begingroup$
SortBy[ 
 PadRight[#, 3] & @* StringCases[n : DigitCharacter .. :> ToExpression[n]]
] @ versions // Reverse
$\endgroup$
  • 2
    $\begingroup$ Probably the safest method, as opposed to using ToExpression on random strings (like Quit). $\endgroup$ – Carl Woll May 2 '18 at 23:15
  • $\begingroup$ @CarlWoll yep, though I did assume there are no numbers except in the version part. $\endgroup$ – Kuba May 3 '18 at 7:32
  • 1
    $\begingroup$ @CarlWoll The Paclet Manager uses ToExpression for version parsing. Imagine what you could do with a simple PacletInfo file ... $\endgroup$ – Szabolcs May 3 '18 at 17:32
  • $\begingroup$ @Szabolcs mathematica.stackexchange.com/q/172553/5478 $\endgroup$ – Kuba May 4 '18 at 12:37
6
$\begingroup$
versions[[Ordering @ - PadRight@ToExpression[Rest /@ StringSplit[versions, "@" | "."]]]]

{"openmpi@2.0.2", "openmpi@2.0", "openmpi@1.10.6", "openmpi@1.10", "openmpi@1.8.4", "openmpi@1.6.2", "openmpi@1.6"}

$\endgroup$
5
$\begingroup$
Reverse@SortBy[
 versions, 
 PadRight[ToExpression[
  StringSplit[StringDrop[#, StringPosition[#, "@"][[-1, 1]]], "."]],
  3] &
]

{"openmpi@2.0.2", "openmpi@2.0", "openmpi@1.10.6", "openmpi@1.10", "openmpi@1.8.4", "openmpi@1.6.2", "openmpi@1.6"}

$\endgroup$
  • $\begingroup$ Ah, my approach was very similar but SortBy is definitely better here. $\endgroup$ – AccidentalFourierTransform May 2 '18 at 22:04
3
$\begingroup$

I tried to create a 'complete' function with input checking, options and basic error messaging; I was going for a more contained experience but I think it got a bit out of hand; anyway, here's my take:

(* try and localize symbols *)
Block[{m, n, checkVersions, checkQ, ascend},

  (* 'pattn' *should* match the 'package@version' strings *)
  With[{pattn = StringExpression[LetterCharacter .., "@", m : StringExpression[DigitCharacter, "."] .., n : DigitCharacter ..]},

    (* 'checkVersions' works with 'checkQ' (see below) to validate input *)
    SetAttributes[checkVersions, Listable];

    checkVersions = Function[{version}, StringMatchQ[version, pattn], Listable];

    checkQ = Function[{versions}, And @@ checkVersions[versions]] /. OwnValues[checkVersions];

    (* 'rhs' extracts the version digits from an input string *)
    With[{rhs = Thread[ToExpression[PadRight[Join[StringSplit[#1, "."], {#2}], 3]]] &},

      (* 'ascend' sorts the input according to the version digits *)
      ascend = SortBy[#, (StringCases[#, pattn :> rhs[m, n]] &)] &;

      With[{checkVersionsQ = checkQ, sortVersionsAscending = ascend},

        (* 'sortVersions' is the callable function; option 'Ascending' controls how sorting is done *)
        Options[sortVersions] = {Ascending -> Automatic};

        sortVersions::srtvopterr = "Option value '`1`' is not recognised. Possible values for option 'Ascending' include 'True', 'False' and 'Automatic'.";

        sortVersions[versions_?checkVersionsQ, OptionsPattern[]] :=  With[{optval = OptionValue[Ascending]},

          If[

            MemberQ[{True, False, Automatic}, optval],

            Which[

              optval === Automatic || optval, sortVersionsAscending[versions],        

              Not[optval], Reverse[sortVersionsAscending[versions]]

             ],

            Message[sortVersions::srtvopterr, optval]

           ]


         ]

       ]

    ]

  ]

 ]

Evaluating the following lines of code

sortVersions[versions]
sortVersions[versions, Ascending -> Automatic]
sortVersions[versions, Ascending -> True]
sortVersions[versions, Ascending -> False]
sortVersions[versions, Ascending -> WrongOptionValue]

produces the following output

{"openmpi@1.6", "openmpi@1.6.2", "openmpi@1.8.4", 
     "openmpi@1.10", "openmpi@1.10.6", "openmpi@2.0", "openmpi@2.0.2"}
{"openmpi@1.6", "openmpi@1.6.2", "openmpi@1.8.4", 
     "openmpi@1.10", "openmpi@1.10.6", "openmpi@2.0", "openmpi@2.0.2"}
{"openmpi@1.6", "openmpi@1.6.2", "openmpi@1.8.4", 
     "openmpi@1.10", "openmpi@1.10.6", "openmpi@2.0", "openmpi@2.0.2"}
{"openmpi@2.0.2", "openmpi@2.0", "openmpi@1.10.6", 
    "openmpi@1.10", "openmpi@1.8.4", "openmpi@1.6.2", "openmpi@1.6"}

enter image description here

A last minute though was to provide an extra 'Option' that deals with the possible issue of a variable length for the version digits (eg more than three groups) but I didn't want to make it more complicated than it already is...

hope it's helpfull

$\endgroup$
2
$\begingroup$

It seems like all you need is a general-purpose number-aware sorting function. Here is my implementation of it:

NaturalSort::usage = "Sorting function for number-aware sorting. Use with Sort.";
NaturalSort[a_String, b_String] := OrderedQ[
  StringSplit[#, d : DigitCharacter .. :> ToExpression@d, All] & /@ {a, b},
  ListOrder]

NaturalSort splits the string into a list of DigitCharacters and non-DigitCharacters. Then it checks whether the two lists are in order.

The helper function ListOrder determines whether two lists of unequal length are in order. I needed this because with Order[list1, list2] the shorter list always comes first. ListOrder pads the shorter list with Null, maps Order to all pairs, and reverses the order if one element is Null (I have defined that Null always comes first but Order disagrees, hence the workaround).

ListOrder[a_List, b_List] := SelectFirst[
  MapThread[
   Order[#1, #2] * If[FreeQ[{#1, #2}, Null], 1, -1] &,

   PadRight[#, Max[Length /@ {a, b}], Null] & /@ {a, b}
   ],
  # != 0 &,
  0
]

NaturalSort can then be used as the sorting function for Sort:

In[]:= Sort[versions, NaturalSort]

(* {"openmpi@1.6", "openmpi@1.6.2", "openmpi@1.8.4", "openmpi@1.10", "openmpi@1.10.6", "openmpi@2.0", "openmpi@2.0.2"} *)
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.