Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Version sort (with alphas, betas, etc.) in ruby

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!

like image 838
Chaim Leib Halbert Avatar asked Oct 22 '15 20:10

Chaim Leib Halbert


People also ask

How do you sort a method in Ruby?

The Ruby sort method works by comparing elements of a collection using their <=> operator (more about that in a second), using the quicksort algorithm. You can also pass it an optional block if you want to do some custom sorting. The block receives two parameters for you to specify how they should be compared.

How do you sort an array of numbers in Ruby?

You can use the sort method on an array, hash, or another Enumerable object & you'll get the default sorting behavior (sort based on <=> operator) You can use sort with a block, and two block arguments, to define how one object is different than another (block should return 1, 0, or -1)


2 Answers

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"]
like image 117
steenslag Avatar answered Oct 19 '22 12:10

steenslag


If you interpret this as "sort by each segment of digits", then you 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"]
like image 36
DreadPirateShawn Avatar answered Oct 19 '22 12:10

DreadPirateShawn