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"]