22

How do I sort a list of versions in Ruby? I've seen stuff about natural sort, but this is a step beyond that.

Input is a bunch of strings like this:

input = ['10.0.0b12', '10.0.0b3', '10.0.0a2', '9.0.10', '9.0.3']

I can almost do it with the naturally gem:

require 'naturally'
Naturally.sort(input)
=> ["9.0.3", "9.0.10", "10.0.0a2", "10.0.0b12", "10.0.0b3"]    

Problem: 10.0.0b3 is sorted after 10.0.0b12; 10.0.0b3 should be first.

Anyone have a way that works? Other languages are helpful too!

Chaim Leib Halbert
  • 2,194
  • 20
  • 23

3 Answers3

39

Ruby ships with the Gem class, which knows about versions:

ar = ['10.0.0b12', '10.0.0b3', '10.0.0a2', '9.0.10', '9.0.3']

p ar.sort_by { |v| Gem::Version.new(v) }
# => ["9.0.3", "9.0.10", "10.0.0a2", "10.0.0b3", "10.0.0b12"]
steenslag
  • 79,051
  • 16
  • 138
  • 171
  • 2
    Nice. For what it's worth -- looks like this handles "alpha" and "beta" alphabetically only. That is, `['9.0.10rc2', '9.0.10', '9.0.10rc1', '9.0.10a', '9.0.10test']` yields `["9.0.10a", "9.0.10rc1", "9.0.10rc2", "9.0.10test", "9.0.10"]`. Should be sufficient, since "alpha / beta / pre-release / rc / release" happen to flow alphabetically anyway, but could be quirky if your data deviates too far from that. – DreadPirateShawn Oct 22 '15 at 20:50
  • 1
    pretty interesting how it works: https://github.com/rubygems/rubygems/blob/1aa8033952d4eda5ca131039822f9548166ab507/lib/rubygems/version.rb#L336-L361 – Anthony Feb 01 '18 at 18:22
5

If you interpret this as "sort by each segment of digits", then the following will handle your example input above:

input.map{ |ver| ver.split(%r{[^\d]+}).map(&:to_i) }.zip(input).sort.map(&:last)
=> ["9_0", "9_1", "10_0b3", "10_0b12"]

That is,

  • for each value, eg 10_0b3
  • split on any length of non-digit characters, eg ["10","0","3"]
  • cast each digit segment to integer, eg [10,0,3]
  • zip with original input, yields [[[10, 0, 12], "10_0b12"], [[10, 0, 3], "10_0b3"], [[9, 0], "9_0"], [[9, 1], "9_1"]]
  • sort, by virtue of [10,0,3] < [10,0,12]
  • get last value of each element, which is the original input value which corresponds to each processed sortable value

Now granted, this is still quite custom -- version numbers as simple as "9_0a" vs "9_0b" won't be handled, both will appear to be [9,0] -- so you may need to tweak it further, but hopefully this starts you down a viable path.

EDIT: Example input above changed, so I changed the regex to make sure the digit-matching is greedy, and with that it still holds up:

irb(main):018:0> input = ['10.0.0b12', '10.0.0b3', '9.0.10', '9.0.3']
=> ["10.0.0b12", "10.0.0b3", "9.0.10", "9.0.3"]
irb(main):025:0> input.map{ |ver| ver.split(%r{[^\d]+}).map(&:to_i) }.zip(input).sort.map(&:last)
=> ["9.0.3", "9.0.10", "10.0.0b3", "10.0.0b12"]
Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
DreadPirateShawn
  • 8,164
  • 4
  • 49
  • 71
  • This does help, but betas aren't the only possible suffix. We could also have alphas like '10.0.0a2' or release candidates like '10.0.0rc1'. If these are side-by-side with the others, the sorting breaks. – Chaim Leib Halbert Oct 22 '15 at 20:39
1

In the specific case that you are working with NuGet and want to parse, compare or sort by NuGet's peculiar own versioning scheme from Ruby code, there is now this:

https://rubygems.org/gems/nuget_versions

I created it specifically to solve this problem. NuGet's version numbers are a bit weird, they are a superset of SemVer that also permits the use of 4 components instead of 3.

Jonathan Gilbert
  • 3,526
  • 20
  • 28