Daniel Azuma
Ruby Version Numbers Done Right?
Posted on Mon, Nov 30, 2009, at 11:53 PM
Tags: ruby

Summary: Introducing versionomy, a new rubygem offering robust parsing, interpretation, manipulation, and comparison of version numbers. It features an adaptive parser with enough flexibility to handle a variety of syntaxes, including prerelease (e.g. alpha, beta) syntax, patchlevels, and nonstandard delimiters. Try it out and feel free to leave feedback.

The Challenge of Version Numbers

How do you compare version numbers? Suppose you have some ruby code that requires Ruby 1.8.7 or later, and won’t work under 1.8.6. Well, I’ve seen a lot of code that does something like this:

if RUBY_VERSION >= '1.8.7'
  do_something
end

Treating the version number as a string works well enough… that is, until it doesn’t. If RUBY_VERSION is “1.8.7”, everything’s okay. Same for “1.8.6”, “1.8.8”, and even “1.9.1”. But wait… what if the RUBY_VERSION was “1.8.10”? Then a simple string comparison would fail. Not that we expect that many point releases of Ruby 1.8, but the example should make us pause and think. Version numbers aren’t strings, and in the general case it isn’t sufficient to treat them as such. They need to be parsed.

Here’s another example. A package management system such as rubygems needs to track what version of each package is installed so that it knows when to perform upgrades. For a long time, rubygems saw a version number as a period-delimited series of numbers, e.g. “1.2.3”. It provided a class that parsed a version string into a list of integers, so it didn’t fail on string comparisons.

However, some gem authors wanted to be able to release “prerelease” versions of their gems, i.e. versions before the “.0” release. This wasn’t technically possible, and so some gem authors resorted to schemes such as “1.2.99” to denote a pre-“1.3” version, a system that had some obvious problems. More recently, rubygems added limited support for prerelease versions by allowing a “string” field in a version number that is always sorted before “0”. So, a pre-“1.3” beta release could be versioned “1.3.beta.1”. That was an improvement, but still nonoptimal: it forces the use of a syntax that we don’t normally expect for a version number. Furthermore, it doesn’t properly support “patchlevel” versions such as we find used for the Ruby virtual machine itself; versions like “1.9.1-p243” could get misinterpreted as a prerelease.

The trouble with version numbers is that there are many different cases we want to express: major and minor versions, prereleases, patches, and so forth; and there are many different syntaxes used to express them. Working with version numbers is a challenge.

Introducing Versionomy

The Versionomy library is my attempt to make sense of version numbers. It represents version numbers as ruby objects, and knows how to parse, interpret, compare, and manipulate all the syntax forms I discuss above, plus many, many more. It interprets prerelease versions and patchlevels, and knows how to handle them when doing manipulation and comparison. It also provides facilities for creating new parsers to recognize different syntax forms, or even different version number structures. In short, Versionomy is trying to solve the version number challenge once and for all.

Here are some examples, straight from the README file:

require 'versionomy'

# Create version numbers that understand their own semantics
v1 = Versionomy.create(:major => 1, :minor => 3, :tiny => 2)
v1.major                                 # => 1
v1.minor                                 # => 3
v1.tiny                                  # => 2
v1.release_type                          # => :final
v1.patchlevel                            # => 0

# Parse version numbers, including common prerelease syntax
v2 = Versionomy.parse('1.4a3')
v2.major                                 # => 1
v2.minor                                 # => 4
v2.tiny                                  # => 0
v2.release_type                          # => :alpha
v2.alpha_version                         # => 3
v2 > v1                                  # => true
v2.to_s                                  # => '1.4a3'

# Version numbers are semantically self-adjusting.
v3 = Versionomy.parse('1.4.0b2')
v3.major                                 # => 1
v3.minor                                 # => 4
v3.tiny                                  # => 0
v3.release_type                          # => :beta
v3.alpha_version                         # raises NoMethodError
v3.beta_version                          # => 2
v3 > v2                                  # => true
v3.to_s                                  # => '1.4.0b2'

# You can bump any field
v4 = Versionomy.parse('1.4.0b2').bump(:beta_version)
v4.to_s                                  # => '1.4.0b3'
v5 = v4.bump(:tiny)
v5.to_s                                  # => '1.4.1'

# Bumping the release type works as you would expect
v6 = Versionomy.parse('1.4.0b2').bump(:release_type)
v6.release_type                          # => :release_candidate
v6.to_s                                  # => '1.4.0rc1'
v7 = v6.bump(:release_type)
v7.release_type                          # => :final
v7.to_s                                  # => '1.4.0'

# If a version has trailing zeros, it remembers how many fields to
# unparse; however, you can also change this.
v8 = Versionomy.parse('1.4.0b2').bump(:major)
v8.to_s                                  # => '2.0.0'
v8.unparse(:optional_fields => [:tiny])  # => '2.0'
v8.unparse(:required_fields => [:tiny2]) # => '2.0.0.0'

# Comparisons are semantic, so will behave as expected even if the
# formatting is set up differently.
v9 = Versionomy.parse('2.0.0.0')
v9.to_s                                  # => '2.0.0.0'
v9 == Versionomy.parse('2')              # => true

# Patchlevels are supported when the release type is :final
v10 = Versionomy.parse('2.0.0').bump(:patchlevel)
v10.patchlevel                           # => 1
v10.to_s                                 # => '2.0.0-1'
v11 = Versionomy.parse('2.0p1')
v11.patchlevel                           # => 1
v11.to_s                                 # => '2.0p1'
v11 == v10                               # => true

# You can create your own format from scratch or by modifying an
# existing format
microsoft_format = Versionomy.default_format.modified_copy do
  field(:minor) do
    recognize_number(:default_value_optional => true,
                     :delimiter_regexp => '\s?sp',
                     :default_delimiter => ' SP')
  end
end
v12 = microsoft_format.parse('2008 SP2')
v12.major                                # => 2008
v12.minor                                # => 2
v12.tiny                                 # => 0
v12.to_s                                 # => '2008 SP2'
v12 == Versionomy.parse('2008.2')        # => true

You can install Versionomy from rubygems/gemcutter:

gem install versionomy

Documentation is available at http://virtuoso.rubyforge.org/versionomy/Versionomy_rdoc.html

Source code is hosted on Github at http://github.com/dazuma/versionomy

Report bugs on Github issues at http://github.org/dazuma/versionomy/issues

Versionomy is released under a BSD-style license.

How Are You Handling Version Numbers?

I’m releasing Versionomy to the community as a request-for-comments. How do you think version numbers should be handled? Is this library on the right track, or is there a better strategy? Do you find it useful? What major syntaxes are missing support right now? Leave me a note.

3 Comments

If you have rubygems already loaded:

Gem::Version.new(RUBY_VERSION) <=> Gem::Version.new('1.8.6')

Yeah, Gem::Version works for the simple cases. But if you need a particular patchlevel, it gets challenging.

Gem::Version.new('1.8.7-p174')  # raises ArgumentError
Versionomy.parse('1.8.7-p174')  # works fine

I agree with Daniel

Version parsing and comparison is challenging. There are so many version schemas in the wild.

Take for example OpenSSL: 0.9.8k, 0.9.8l

Other packages uses different version schema.

RubyGems is not smart enough to even deal with patchlevels (take for example 1.9.2dev which is patchelvel -1)

I think for the purpose of RubyGems, version comparison is OK, but there is need for a specialized versioning parsing and processing library.

Comments are disabled for this article.
Recent
Tags
Random blogs