mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
Improve tokenization of version strings
Tokens like "b4", "beta1", "p195", &c. are now treated as atoms rather than being broken down even further. Additionally, we enable support for padding in the middle of versions strings, so we can successfully compare something like "2.1-p195" with "2.1.0-p194" by inferring that "2.1" is really "2.1.0". This fixes the comparison "9.9.3-P1" > "9.9.3" which previously has not been handled correctly.
This commit is contained in:
parent
3e5ac7e55c
commit
28acfbba51
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user