# Sorting semantic version numbers

**URL:** <https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838>\
**Category:** support\
**Created:** [October 6, 2022, 3:08pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838 "2022-10-06T15:08:02Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![IanMadd](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/ianmadd/32/9452_2.png) [@IanMadd](https://discourse.gohugo.io/u/IanMadd)\
**Post date:** [October 6, 2022, 3:08pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/1 "2022-10-06T15:08:02Z")

</div>

Has anyone come up with a clever way to sort version numbers?

I’m trying something like this:

```auto
{{ $numbers := slice "3.2.3" "3.1.2" "3.10.0" }}
{{ $sortNumbers := sort $numbers "value" "desc" }}

```

Ideally $sortNumbers would return `["3.10.0", "3.2.3", "3.1.2"]`, but each item is a string so it returns `["3.2.3", "3.10.0", "3.1.2"]`.

---

<div class="post-metadata">

**Author:** ![zwbetz](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/zwbetz/32/16088_2.png) [@zwbetz](https://discourse.gohugo.io/u/zwbetz)\
**Post date:** [October 7, 2022, 2:13am UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/2 "2022-10-07T02:13:01Z")

</div>

There’s an open GitHub issue about this, if you’d like to follow it.

> <https://github.com/gohugoio/hugo/issues/9402>
>
> We use Hugo to manage our documentation site. On this site, we provide documenta…tion for various versions of software packages that are released according to \[semantic versioning\](https://semver.org/) rules. We list versions of the packages in descending order according to their version number for easy navigation. Today, Hugo has no direct support for sorting by semver, so we settle for sorting by the date the version was released. This is less than desirable in the situation where we are actively maintaining more than one major or minor version at a time, resulting in seemingly unsorted lists of versions for our users. 
> 
> On a stale PR attempting to implement sorting of semver numbers, @jmooring \[suggested\](https://github.com/gohugoio/hugo/pull/8010#issuecomment-1014711269) extending the sort function to support this functionality. Implementing sorting in this way would avoid breaking existing sites and allow sorting any resource by any front matter field according to semver conventions:
> 
> \`\`\`
> {{ range (sort .Pages "Params.version" "semVerAsc" ) }}
> {{ range (sort .Pages "Params.version" "semVerDesc" ) }}
> \`\`\`
> 
> I'm unfamiliar with the Hugo codebase, but would be more than happy to perform the implementation once the design proposal is finalized and accepted by the Hugo maintainers. My initial survey of the source suggests most changes would center around \[tpl/collections/sort.go\](https://github.com/gohugoio/hugo/blob/master/tpl/collections/sort.go), but a nudge in the right direction from more experienced contributors would be appreciated.

---

<div class="post-metadata">

**Author:** ![zwbetz](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/zwbetz/32/16088_2.png) [@zwbetz](https://discourse.gohugo.io/u/zwbetz)\
**Post date:** [October 7, 2022, 2:44am UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/3 "2022-10-07T02:44:09Z")

</div>

Took a stab at this.

Assuming your input strings are always in the format of `MAJOR.MINOR.PATCH`, then you can do this:

```nohighlight
{{ $semVers := slice
  "3.2.3"
  "3.1.2"
  "3.10.0"
}}

{{ $arrayOfMaps := slice }}

{{ range $semVers }}
  {{ $separated := split . "." }}
  {{ $major := index $separated 0 }}
  {{ $minor := index $separated 1 }}
  {{ $patch := index $separated 2 }}
  {{ $semVerAsInt := printf "%s%s%s" $major $minor $patch | int }}
  {{ $map := dict
    "semVer" .
    "semVerAsInt" $semVerAsInt
  }}
  {{ $arrayOfMaps = $arrayOfMaps | append $map }}
{{ end }}

{{ $arrayOfMaps = sort $arrayOfMaps "semVerAsInt" "desc" }}

{{ $sortedSemVers := slice }}

{{ range $arrayOfMaps }}
  {{ $sortedSemVers = $sortedSemVers | append .semVer }}
{{ end }}

{{ $sortedSemVers }}

```

Which will output:

```nohighlight
[3.10.0 3.2.3 3.1.2]

```

---

<div class="post-metadata">

**Author:** ![IanMadd](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/ianmadd/32/9452_2.png) [@IanMadd](https://discourse.gohugo.io/u/IanMadd)\
**Post date:** [October 11, 2022, 7:36pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/4 "2022-10-11T19:36:37Z")

</div>

This is the solution I came up with. I’m all ears if someone can come up with a simpler solution or ways to simplify this.

I should add that I had a few more interesting cases than I initially mentioned, like a few pre-release version numbers.

Also, the solution above from @zwbetz can’t handle a number like “3.9.10” because 3910 is greater than 3100, even though the semver number is lower than “3.10.0”. Thanks for the suggestion though.

Basically my solution is to create a set of nested maps with each major, minor, and patch numbers, and prerelease version numbers are a slice in the value of the patch map.

Each new version number is converted to a map and then merged in. Then I range through each level of each map creating sorted slices of integers and then index the map of version numbers by each item of the slice. Once it’s ranged all the way up to the patch or pre-release version numbers, it appends the whole semver version number to a slice of version numbers.

One big caveat, it doesn’t sort pre-release version numbers correctly, but I have so few that it wasn’t worth my time to address this. If anyone is planning to use this and has a lot of pre-release version numbers, you’re going to want to spend some time figuring out a way to sort those.

I’ve also left a bunch of headings and text in the page output so it’s easier to make sense of for anyone who wants ~~is crazy enough~~ to try this.

```auto

<!-- Create a bunch of numbers for testing -->
{{ $map1 := dict "17" (dict "1" (dict "52" slice )) }}
{{ $map2 := dict "17" (dict "21" (dict "46" slice)) }}
{{ $map3 := dict "17" (dict "11" (dict "42" slice)) }}
{{ $map4 := dict "17" (dict "9" (dict "26" slice)) }}
{{ $map5 := dict "17" (dict "9" (dict "200" slice)) }}

{{ $map6 := dict "17" (dict "8" (dict "20" slice)) }}
{{ $map7 := dict "17" (dict "8" (dict "10" slice)) }}

{{ $map8 := dict "16" (dict "8" (dict "21" slice )) }}
{{ $map9 := dict "16" (dict "8" (dict "25" (slice "-rc.2") )) }}
{{ $map10 := dict "16" (dict "8" (dict "93" slice)) }}
{{ $map11 := dict "16" (dict "8" (dict "201" slice)) }}
{{ $map12 := dict "16" (dict "8" (dict "2" slice)) }}
{{ $map15 := dict "16" (dict "8" (dict "100" slice)) }}

{{ $map13 := dict "20" (dict "8" (dict "2" slice)) }}
{{ $map14 := dict "2" (dict "8" (dict "2" slice)) }}

{{ $versionsMap := merge $map2 $map1 $map3 $map4 $map5 $map6 $map7 $map8 $map9 $map10 $map11 $map12 $map13 $map14 $map15 }}

<h2>Existing Map of Versions</h2>
<p>VersionsMap:</p>
{{ range $k, $v := $versionsMap }}
  <p>Major Version:{{ $k }} Major Version Map:{{ $v }}</p>
{{ end }}

<h2>Create new version number</h2>
<!-- A bunch of other numbers for testing how they will be added/sorted -->
{{/* $newNumber := "17.9.21" */}}
{{ $newNumber := "16.8.25-rc.0" }}
{{/* $newNumber := "1.2.3" */}}
{{/* $newNumber := "20.100.3" */}}
<p>Add new number: {{ $newNumber }}</p>

{{ $preReleaseVersion := index (findRE `\-\w+\.{0,1}\d{0,}$` $newNumber 1) 0 }}
{{ $versionCore := strings.TrimSuffix $preReleaseVersion $newNumber }}
{{ $versionNumberSlice := split $versionCore "." }}
{{ $majorVersion := index $versionNumberSlice 0 }}
{{ $minorVersion := index $versionNumberSlice 1 }}
{{ $patchVersion := index $versionNumberSlice 2 }}

<p>Version Core: {{ $versionCore }}</p>
<p>Major Version: {{ $majorVersion }}</p>
<p>Minor Version: {{ $minorVersion }}</p>
<p>Patch Version: {{ $patchVersion }}</p>
<p>Pre Release Version: {{ $preReleaseVersion }}</p>

{{ $newMap := dict }}
{{ if ne $preReleaseVersion nil }}
  {{ $newMap = dict $majorVersion (dict $minorVersion (dict $patchVersion (slice $preReleaseVersion))) }}
{{ else }}
  {{ $newMap = dict $majorVersion (dict $minorVersion (dict $patchVersion slice )) }}
{{ end }}

<p>New Version Number Map: {{ $newMap }}</p>

<h2>Merge Version Number Map into existing dict of version numbers</h2>

{{ if isset $versionsMap $majorVersion }}
  {{ if isset (index $versionsMap $majorVersion ) $minorVersion }}
    {{ if isset (index $versionsMap $majorVersion $minorVersion) $patchVersion }}
      {{ if ne (index $versionsMap $majorVersion $minorVersion $patchVersion) nil }}
        {{ $preReleaseVersions := index $versionsMap $majorVersion $minorVersion $patchVersion }}
        {{ $preReleaseVersions = sort ($preReleaseVersions | append $preReleaseVersion) "value" "desc" }}
        {{ $newMap = dict $majorVersion (dict $minorVersion (dict $patchVersion $preReleaseVersions )) }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

{{ $versionsMap = merge $versionsMap $newMap }}

<p>VersionsMap:</p>
{{ range $k, $v := $versionsMap }}
  <p>{{ $k }} {{ $v }}</p>
{{ end }}

<h2>Convert Map to Slice</h2>

{{ $versionsSlice := slice }}

{{$majVersions := slice }}
{{ range $maj, $minPatchPre := $versionsMap }}
  {{ $majVersions = sort ($majVersions | append (int $maj)) "value" "desc" }}
{{ end }}

<p>Sorted Major Versions: {{ $majVersions }}</p>

{{ range $maj := $majVersions }}
  <p>Major version: {{ $maj }}</p>
  <p>{{ index $versionsMap (string $maj) }}</p>
  {{ $minVersions := slice }}
  {{ range $min, $patchPre := index $versionsMap (string $maj) }}
    {{ $minVersions = sort ($minVersions | append (int $min)) "value" "desc" }}
    <p>Min version: {{ $min }}</p>
  {{ end }}
  <p>Minor Versions: {{ $minVersions }}</p>

  {{ range $min := $minVersions }}
    <p>{{ index $versionsMap (string $maj) (string $min) }}</p>
    {{ $patchVersions := slice }}
    {{ range $patch, $pre := index $versionsMap (string $maj) (string $min) }}
      <p>Patch version: {{ $patch }}</p>
      {{ $patchVersions = sort ($patchVersions | append $patch ) "value" "desc" }}
    {{ end }}
    <p>Patch Versions: {{ $patchVersions }}</p>
    {{ range $patch := $patchVersions }}
      <p>Index of maj, min, patch: {{ index $versionsMap (string $maj) (string $min) (string $patch) }}</p>
      {{ range $pre := sort (index $versionsMap (string $maj) (string $min) (string $patch)) "value" "desc" }}
        <p>Pre release version: {{ $pre }}</p>
        <p>Full version: {{ (printf "%v%s%v%s%v%s" $maj "." $min "." $patch $pre ) }}</p>
        {{ $versionsSlice = $versionsSlice | append (printf "%v%s%v%s%v%s" $maj "." $min "." $patch $pre )}}
      {{ else }}
        <p>Full version: {{ (printf "%v%s%v%s%v" $maj "." $min "." $patch ) }}</p>
        {{ $versionsSlice = $versionsSlice | append (printf "%v%s%v%s%v" $maj "." $min "." $patch)}}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

<h2>Final Slice of sorted versions</h2>
<p>Versions Slice: {{ $versionsSlice }}</p>

<ul>
  {{ range $version := $versionsSlice }}
    <li>{{ $version }}</li>
  {{ end }}
</ul>

```

The final UL list output is this:

```
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
2.8.2

```

---

<div class="post-metadata">

**Author:** ![gaetawoo](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/gaetawoo/32/16789_2.png) [@gaetawoo](https://discourse.gohugo.io/u/gaetawoo)\
**Post date:** [October 12, 2022, 3:44pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/5 "2022-10-12T15:44:33Z")

</div>

I think @jmooring had a solution for this somewhere in this forum, but I can’t find it.

---

<div class="post-metadata">

**Author:** ![solus](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/solus/32/15500_2.png) [@solus](https://discourse.gohugo.io/u/solus)\
**Post date:** [October 13, 2022, 7:51pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/6 "2022-10-13T19:51:57Z")

</div>

The following solution should be able to sort all semver strings, although it doesn’t assume `MAJOR.MINOR.PATCH` format and can handle any strings with numbers, including those with “beta” or “rc” suffixes.

```auto
{{- $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" }}

{{- $paddedVersions := apply $versions "partial" "padZeroPrefix" "." }}
{{- $sortedVersions := (sort $paddedVersions "value" "desc") }}
{{- $sortedVersions = apply $sortedVersions "partial" "trimZeroPrefix" "." }}

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

```

The slice has the `padZeroPrefix` partial function applied over it, which pads all numbers in the string with 0 to the max length of 6 digits (this length can be adjusted in the partial). The slice is then sorted in descending order before the `trimZeroPrefix` is applied to remove the padding.

As long as the padding length is greater than the length of the largest number, everything should sort correctly. The strings in `$paddedVersions` look something like this before they’re sorted:

```auto
...
000016.000008.000201
000017.000008.000010
000016.000008.000025-rc.000000
...

```

The resulting output is this:

```auto
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** :

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

```

**trimZeroPrefix.html** :

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

```

---

<div class="post-metadata">

**Author:** ![zwbetz](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/zwbetz/32/16088_2.png) [@zwbetz](https://discourse.gohugo.io/u/zwbetz)\
**Post date:** [October 14, 2022, 1:05pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/7 "2022-10-14T13:05:46Z")

</div>

It’s true what they say. _Cunningham’s Law_ is real.

It’s fun to see yall come up with creative solutions for this problem.

---

<div class="post-metadata">

**Author:** ![ju52](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/ju52/32/6749_2.png) [@ju52](https://discourse.gohugo.io/u/ju52)\
**Post date:** [October 15, 2022, 6:12pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/8 "2022-10-15T18:12:53Z")

</div>

golang has a version module. Can hugo bring it to the surface? a variable type version?  
Or sort “asVersion” …

only my 2 cents here

---

<div class="post-metadata">

**Author:** ![system](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/system/32/1_2.png) [@system](https://discourse.gohugo.io/u/system)\
**Post date:** [October 17, 2022, 8:21pm UTC](https://discourse.gohugo.io/t/sorting-semantic-version-numbers/40838/10 "2022-10-17T20:21:34Z")

</div>

Closed on October 17, 2022, 8:21pm UTC

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.
