diff --git a/Library/Homebrew/test/test_versions.rb b/Library/Homebrew/test/test_versions.rb index e414054efb..cd88bd37cb 100644 --- a/Library/Homebrew/test/test_versions.rb +++ b/Library/Homebrew/test/test_versions.rb @@ -8,10 +8,20 @@ class VersionComparisonTests < Test::Unit::TestCase assert_operator version('0.1'), :==, version('0.1.0') assert_operator version('0.1'), :<, version('0.2') assert_operator version('1.2.3'), :>, version('1.2.2') - assert_operator version('1.2.3-p34'), :>, version('1.2.3-p33') assert_operator version('1.2.4'), :<, version('1.2.4.1') + end + + def test_patchlevel + assert_operator version('1.2.3-p34'), :>, version('1.2.3-p33') + assert_operator version('1.2.3-p33'), :<, version('1.2.3-p34') + end + + def test_HEAD assert_operator version('HEAD'), :>, version('1.2.3') assert_operator version('1.2.3'), :<, version('HEAD') + end + + def test_alpha_beta_rc assert_operator version('3.2.0b4'), :<, version('3.2.0') assert_operator version('1.0beta6'), :<, version('1.0b7') assert_operator version('1.0b6'), :<, version('1.0beta7') @@ -19,13 +29,18 @@ class VersionComparisonTests < Test::Unit::TestCase assert_operator version('1.1beta2'), :<, version('1.1rc1') assert_operator version('1.0.0beta7'), :<, version('1.0.0') assert_operator version('3.2.1'), :>, version('3.2beta4') - assert_nil version('1.0') <=> 'foo' end - def test_version_queries - assert Version.new("1.1alpha1").alpha? - assert Version.new("1.0beta2").beta? - assert Version.new("1.0rc-1").rc? + def test_comparing_unevenly_padded_versions + assert_operator version('2.1.0-p194'), :<, version('2.1-p195') + assert_operator version('2.1-p195'), :>, version('2.1.0-p194') + assert_operator version('2.1-p194'), :<, version('2.1.0-p195') + assert_operator version('2.1.0-p195'), :>, version('2.1-p194') + assert_operator version('2-p194'), :<, version('2.1-p195') + end + + def test_comparison_returns_nil_for_non_version + assert_nil version('1.0') <=> 'foo' end def test_compare_patchlevel_to_non_patchlevel diff --git a/Library/Homebrew/version.rb b/Library/Homebrew/version.rb index 4a165d72b8..e2b7524962 100644 --- a/Library/Homebrew/version.rb +++ b/Library/Homebrew/version.rb @@ -1,46 +1,148 @@ -class VersionElement - include Comparable - - def initialize elem - elem = elem.to_s.downcase - @elem = case elem - when /\d+/ then elem.to_i - when 'a', 'alpha' then 'alpha' - when 'b', 'beta' then 'beta' - else elem - end - end - - ZERO = VersionElement.new(0) - - def <=>(other) - return unless other.is_a? VersionElement - return -1 if string? and other.numeric? - return 1 if numeric? and other.string? - return elem <=> other.elem - end - - def to_s - @elem.to_s - end - - protected - - attr_reader :elem - - def string? - elem.is_a? String - end - - def numeric? - elem.is_a? Numeric - end -end - class Version include Comparable - def initialize val, detected=false + class Token + include Comparable + + attr_reader :value + + def initialize(value) + @value = value + end + + def inspect + "#<#{self.class} #{value.inspect}>" + end + end + + class NullToken < Token + def initialize(value=nil) + super + end + + def <=>(other) + case other + when NumericToken + other.value == 0 ? 0 : -1 + when AlphaToken, BetaToken, RCToken + 1 + else + -1 + end + end + + def inspect + "#<#{self.class}>" + end + end + + NULL_TOKEN = NullToken.new + + class StringToken < Token + PATTERN = /[a-z]+[0-9]+/i + + def initialize(value) + @value = value.to_s + end + + def <=>(other) + case other + when StringToken + value <=> other.value + when NumericToken, NullToken + -Integer(other <=> self) + end + end + end + + class NumericToken < Token + PATTERN = /[0-9]+/i + + def initialize(value) + @value = value.to_i + end + + def <=>(other) + case other + when NumericToken + value <=> other.value + when StringToken + 1 + when NullToken + -Integer(other <=> self) + end + end + end + + class CompositeToken < StringToken + def rev + value[/([0-9]+)/, 1] + end + end + + class AlphaToken < CompositeToken + PATTERN = /a(?:lpha)?[0-9]+/i + + def <=>(other) + case other + when AlphaToken + rev <=> other.rev + else + super + end + end + end + + class BetaToken < CompositeToken + PATTERN = /b(?:eta)?[0-9]+/i + + def <=>(other) + case other + when BetaToken + rev <=> other.rev + when AlphaToken + 1 + when RCToken, PatchToken + -1 + else + super + end + end + end + + class RCToken < CompositeToken + PATTERN = /rc[0-9]+/i + + def <=>(other) + case other + when RCToken + rev <=> other.rev + when AlphaToken, BetaToken + 1 + when PatchToken + -1 + else + super + end + end + end + + class PatchToken < CompositeToken + PATTERN = /p[0-9]+/i + + def <=>(other) + case other + when PatchToken + rev <=> other.rev + when AlphaToken, BetaToken, RCToken + 1 + else + super + end + end + end + + def initialize(val, detected=false) @version = val.to_s @detected_from_url = detected end @@ -53,40 +155,14 @@ class Version @version == 'HEAD' end - def devel? - alpha? or beta? or rc? - end - - def alpha? - to_a.any? { |e| e.to_s == 'alpha' } - end - - def beta? - to_a.any? { |e| e.to_s == 'beta' } - end - - def rc? - to_a.any? { |e| e.to_s == 'rc' } - end - def <=>(other) - # Return nil if objects aren't comparable - return unless other.is_a? Version - # Versions are equal if both are HEAD - return 0 if head? and other.head? - # HEAD is greater than any numerical version - return 1 if head? and not other.head? - return -1 if not head? and other.head? + return unless Version === other + return 0 if head? && other.head? + return 1 if head? && !other.head? + return -1 if !head? && other.head? - stuple, otuple = to_a, other.to_a - slen, olen = stuple.length, otuple.length - - max = [slen, olen].max - - stuple.fill(VersionElement::ZERO, slen, max - slen) - otuple.fill(VersionElement::ZERO, olen, max - olen) - - stuple <=> otuple + max = [tokens.length, other.tokens.length].max + pad_to(max) <=> other.pad_to(max) end def to_s @@ -96,8 +172,36 @@ class Version protected - def to_a - @array ||= @version.scan(/\d+|[a-zA-Z]+/).map! { |e| VersionElement.new(e) } + def pad_to(length) + nums, rest = tokens.partition { |t| NumericToken === t } + nums.concat([NULL_TOKEN]*(length - tokens.length)).concat(rest) + end + + def tokens + @tokens ||= tokenize + end + alias_method :to_a, :tokens + + def tokenize + @version.scan( + Regexp.union( + AlphaToken::PATTERN, + BetaToken::PATTERN, + RCToken::PATTERN, + PatchToken::PATTERN, + NumericToken::PATTERN, + StringToken::PATTERN + ) + ).map! do |token| + case token + when /\A#{AlphaToken::PATTERN}\z/o then AlphaToken + when /\A#{BetaToken::PATTERN}\z/o then BetaToken + when /\A#{RCToken::PATTERN}\z/o then RCToken + when /\A#{PatchToken::PATTERN}\z/o then PatchToken + when /\A#{NumericToken::PATTERN}\z/o then NumericToken + when /\A#{StringToken::PATTERN}\z/o then StringToken + end.new(token) + end end def self.parse spec