Natural Sorting of SemVer Strings in Hugo

 ·   ·  ☕ 3 min read
🏷️
This page looks best with JavaScript enabled

Earlier today, I saw a post on Hugo’s Discourse site where someone was asking for a way to sort version numbers with a natural sort order where multi-digit numbers are treated atomically.

The accepted solution seemed somewhat complicated with the way the versions were split into separate version components (i.e., Major, Minor, Patch, PreRelease), sorted into nested maps, and then merged together again afterward.

I believe the solution I came up with is a bit more straightforward, and it should be able to sort all SemVer strings. Also, since it doesn’t assume the MAJOR.MINOR.PATCH format, it can also handle version strings with pre-release suffixes, such as “beta” and “rc”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{{- $versions := slice "17.9.200" "17.1.52" "16.8.100" "3.2.3" "3.1.2" "3.10.0" "17.8.20" "3.9.10"
        "16.8.93" "16.8.201" "17.8.10" "16.8.25-rc.0" "2.8.2" "16.8.21" "16.8.25-rc.2" "16.8.2"
        "17.21.46" "17.11.42" "20.8.2" "17.9.26" }}

{{- $padded := apply $versions "partial" "padZeroPrefix" "." }}
{{- $sorted := sort $padded "value" "desc" }}
{{- $results := apply $sorted "partial" "trimZeroPrefix" "." }}

{{- range $results }}
    <li>{{- . }}</li>
{{- end }}

The original slice has the padZeroPrefix partial function applied over it, which pads all numbers in the string with zeros to the max length of six digits (the max length can be adjusted in the partial). The resulting slice is then sorted in descending order before the trimZeroPrefix is applied to remove the padding. It’s simple and clean.

Results

20.8.2
17.21.46
17.11.42
17.9.200
17.9.26
17.8.20
17.8.10
17.1.52
16.8.201
16.8.100
16.8.93
16.8.25-rc.2
16.8.25-rc.0
16.8.21
16.8.2
3.10.0
3.9.10
3.2.3
3.1.2
2.8.2

padZeroPrefix.html

1
2
3
4
{{- $padSize := 6 }}
{{- $paddedString := replaceRE "(\\d+)" (print (strings.Repeat (sub $padSize 1) "0") "$1") . }}
{{- $trimmedString := replaceRE (print "0+(\\d{" $padSize "})") "$1" $paddedString }}
{{- return $trimmedString }}

The function receives a string as input, prepends 5 zeros in front of every digit sequence it finds within the string, and then trims those numbers down to a maximum of 6 digits before returning the results.

As long as the padding length is greater than or equal to the length of the largest number, everything should sort correctly. The following blocks show a few lines of input and how those lines are transformed so that they can be sorted:

Input:
...
16.8.201
17.8.10
16.8.25-rc.0
2.8.2
16.8.21
...

Output:

...
000016.000008.000201
000017.000008.000010
000016.000008.000025-rc.000000
000002.000008.000002
000016.000008.000021
...

an AI-generated illustration of a Cthulhu comic panel

trimZeroPrefix.html

1
{{- return replaceRE "0+(\\d+)" "$1" . }}

This function receives a string as input, identifies all digit sequences, and discards unnecessary zeros at the beginning of each sequence before returning the results.

End of Line.