6
$\begingroup$

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

Input

versions={"[email protected]", "[email protected]", "[email protected]", "[email protected]",
"[email protected]", "[email protected]", "[email protected]"}

Desired Output

SortedVersions={"[email protected]", "[email protected]","[email protected]", "[email protected]", "[email protected]", "[email protected]", 
    "[email protected]"}

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$
2
  • $\begingroup$ Related. $\endgroup$
    – corey979
    May 2, 2018 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, 2018 at 21:56

6 Answers 6

3
$\begingroup$
versionList = ToExpression@StringSplit[Last@StringSplit[#, "@"], "."] &;

Reverse@SortBy[versions, versionList]
(* {"[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"} *)
$\endgroup$
9
$\begingroup$
SortBy[ 
 PadRight[#, 3] & @* StringCases[n : DigitCharacter .. :> ToExpression[n]]
] @ versions // Reverse
$\endgroup$
4
  • 2
    $\begingroup$ Probably the safest method, as opposed to using ToExpression on random strings (like Quit). $\endgroup$
    – Carl Woll
    May 2, 2018 at 23:15
  • $\begingroup$ @CarlWoll yep, though I did assume there are no numbers except in the version part. $\endgroup$
    – Kuba
    May 3, 2018 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, 2018 at 17:32
  • $\begingroup$ @Szabolcs mathematica.stackexchange.com/q/172553/5478 $\endgroup$
    – Kuba
    May 4, 2018 at 12:37
6
$\begingroup$
versions[[Ordering @ - PadRight@ToExpression[Rest /@ StringSplit[versions, "@" | "."]]]]

{"[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"}

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

{"[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"}

$\endgroup$
1
  • $\begingroup$ Ah, my approach was very similar but SortBy is definitely better here. $\endgroup$ May 2, 2018 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

{"[email protected]", "[email protected]", "[email protected]", 
     "[email protected]", "[email protected]", "[email protected]", "[email protected]"}
{"[email protected]", "[email protected]", "[email protected]", 
     "[email protected]", "[email protected]", "[email protected]", "[email protected]"}
{"[email protected]", "[email protected]", "[email protected]", 
     "[email protected]", "[email protected]", "[email protected]", "[email protected]"}
{"[email protected]", "[email protected]", "[email protected]", 
    "[email protected]", "[email protected]", "[email protected]", "[email protected]"}

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]

(* {"[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"} *)
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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