lib/versioncmp.rb
# versioncmp.rb
#
# Natural order comparison of two version strings
# e.g. "1.10.beta" < "1.10" > "1.9"
# which does not follow alphabetically
#
#
# This implementation is Copyright (C) 2012 by Robert Reiz
#
# This software is provided 'as-is', without any express or implied
# warranty. In no event will the authors be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
# claim that you wrote the original software. If you use this software
# in a product, an acknowledgment in the product documentation would be
# appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
# misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.
class Versioncmp
# 'Natural version order' comparison of two version strings
#
def self.compare(a_val, b_val)
a_empty = a_val.to_s.empty?
b_empty = b_val.to_s.empty?
return 0 if a_empty && b_empty
return 0 if a_val.to_s.eql?( b_val.to_s )
return 1 if (a_empty == false) && (b_empty == true )
return -1 if (b_empty == false) && (a_empty == true )
return 1 if b_val.length > a_val.length && b_val.to_s.match(/\A#{a_val}-SNAPSHOT/i)
return -1 if a_val.length > b_val.length && a_val.to_s.match(/\A#{a_val}-SNAPSHOT/i)
return 1 if b_val.length > a_val.length && b_val.to_s.match(/\A#{a_val}-BETA.*/i)
return -1 if a_val.length > b_val.length && a_val.to_s.match(/\A#{a_val}-BETA.*/i)
return 1 if b_val.length > a_val.length && b_val.to_s.match(/\A#{a_val}-alpha.*/i)
return -1 if a_val.length > b_val.length && a_val.to_s.match(/\A#{a_val}-alpha.*/i)
return 1 if b_val.length > a_val.length && b_val.to_s.match(/\A#{a_val}-rc.*/i)
return -1 if a_val.length > b_val.length && a_val.to_s.match(/\A#{a_val}-rc.*/i)
return -1 if b_val.length > a_val.length && b_val.to_s.match(/\A#{a_val}u\d/i)
return 1 if a_val.length > b_val.length && a_val.to_s.match(/\A#{b_val}u\d/i)
a = pre_process a_val
b = pre_process b_val
am = a.to_s.match(/\A(\d+\.\d+)\.\d+\z/i)
bm = b.to_s.match(/\A(\d+\.\d+)-\w+\z/i)
return 1 if am && bm && am[1].eql?(bm[1])
am = a.to_s.match(/\A(\d+\.\d+)-\w+\z/i)
bm = b.to_s.match(/\A(\d+\.\d+)\.\d+\z/i)
return -1 if am && bm && am[1].eql?(bm[1])
ab = [a, b]
offsets = [0, 0]
max_length = a_val.length
max_length = b_val.length if b_val.length > max_length
for i in 0..max_length
result = self.check_the_slice ab, offsets
next if result.nil?
return result if result == 1 || result == -1
end
result = Versioncmp.check_for_tags(a, b)
return result
end
def self.check_the_slice ab, offsets
ab[0] += ".0" if offsets[0] >= ab[0].length
ab[1] += ".0" if offsets[0] >= ab[1].length
part1 = Versioncmp.get_a_piece_of_the_cake offsets[0], ab[0]
part2 = Versioncmp.get_a_piece_of_the_cake offsets[0], ab[1]
part1 = '' if part1.nil?
part2 = '' if part2.nil?
return -1 if Versioncmp.timestamp?(part1) && part2.length() < 8
return 1 if Versioncmp.timestamp?(part2) && part1.length() < 8
offsets[0] += part1.length() + 1;
offsets[1] += part2.length() + 1;
if ( part1.to_s.match(/^[0-9]+$/) && part2.to_s.match(/^[0-9]+$/) )
return self.compare_numbers part1, part2
elsif ( !part1.to_s.match(/^[0-9]+$/) && !part2.to_s.match(/^[0-9]+$/) )
return self.compare_strings ab[0], ab[1], part1, part2
else
return self.compare_special_cases part1, part2
end
end
def self.compare_numbers part1, part2
ai = part1.to_i;
bi = part2.to_i;
result = Versioncmp.compare_int(ai, bi);
return result if result == 1 || result == -1
return nil
end
def self.compare_strings a, b, part1, part2
result = double_scope_checker(a, b)
return result if result == 1 || result == -1
result = Versioncmp.compare_string(part1, part2)
return result if result == 1 || result == -1
return nil
end
def self.compare_special_cases part1, part2
result = Versioncmp.check_jquery_versioning(part1, part2)
return result if !result.to_s.strip.empty?
digit = part1 if part1.to_s.match(/\d/)
return -1 if ( part1.to_s.match(/\d/) && part2.to_s.match(/#{digit}\S*patch\S*/) )
digit = part2 if part2.to_s.match(/\d/)
return 1 if ( part2.to_s.match(/\d/) && part1.to_s.match(/#{digit}\S*patch\S*/) )
if ( part1.to_s.match(/#\S*patch\S*/) && part2.to_s.match(/\S*patch\S*/) )
return compare_string(part1, part2)
end
return 1 if ( part1.eql?("0") && part2.to_s.match(/^[a-zA-Z]+/) )
return -1 if ( part2.eql?("0") && part1.to_s.match(/^[a-zA-Z]+/) )
return -1 if ( part1.eql?("0") && part2.to_s.match(/^[1-9]+[-_a-zA-Z]+/) )
return 1 if ( part2.eql?("0") && part1.to_s.match(/^[1-9]+[-_a-zA-Z]+/) )
pm1 = part1.to_s.match(/\A[0-9]+\z/)
pm2 = part2.to_s.match(/\A(\d+)(\w+)\z/i)
return 1 if ( pm1 && pm2 && pm2[1].eql?(part1) && VersionTagRecognizer.stable?(pm2[2]) )
return -1 if ( pm1 && pm2 && pm2[1].eql?(part1) && !VersionTagRecognizer.stable?(pm2[2]) )
pm1 = part1.to_s.match(/\A(\d+)-(\w+)\z/i)
pm2 = part2.to_s.match(/\A\d+\z/i)
return 1 if try_to_i_bigger( pm1, pm2, part2 )
return 1 if pm2 && pm1 && pm1[1].eql?(part2) && VersionTagRecognizer.stable?(pm1[2])
pm1 = part1.to_s.match(/\A\d+\z/i)
pm2 = part2.to_s.match(/\A(\d+)-(\w+)\z/i)
return 1 if try_to_i_bigger( pm1, pm2, part2 )
return -1 if pm1 && pm2 && pm2[1].eql?(part1) && VersionTagRecognizer.stable?(pm2[2])
return 1 if ( part1.match(/\A[0-9]+\z/) && !part2.match(/\A[0-9]+\z/) )
return -1;
rescue => e
p e.message
p e.backtrace.join("\n")
return -1
end
def self.try_to_i_bigger pm1, pm2, part2
pm2 && pm1 && pm1[1].to_i > part2.to_i
rescue => e
false
end
# Tags are RC, alpha, beta, dev and so on.
#
def self.check_for_tags(a, b)
big = String.new(a)
small = String.new(b)
if (a.length() < b.length())
big = String.new(b)
small = String.new(a)
end
if (VersionTagRecognizer.tagged?(big))
big_without_scope = VersionTagRecognizer.remove_tag big
if (Versioncmp.compare_string(big_without_scope, small) == 0)
return Versioncmp.compare_string_length(a, b)
end
end
0
end
def self.double_scope_checker(a, b)
if VersionTagRecognizer.tagged?(a) && VersionTagRecognizer.tagged?(b)
a_without_scope = VersionTagRecognizer.remove_tag a
b_without_scope = VersionTagRecognizer.remove_tag b
if a_without_scope.eql? b_without_scope
return VersionTagRecognizer.compare_tags(a, b)
end
end
0
end
def self.get_a_piece_of_the_cake(offset, cake)
for z in 0..100
offsetz = offset + z
break if offsetz > cake.length()
p = cake[ offset..offset + z ]
break if ( p.to_s.match(/^\w+\.$/) != nil )
end
z = z - 1 if z > 0
piece = cake[offset..offset + z ]
return piece
end
def self.timestamp? part
return false if part.to_s.empty?
return part.length() == 9 && part.to_s.match(/^[0-9]+$/) != nil
end
def self.pre_process val
cleaned_version = replace_x_dev val
cleaned_version = replace_wildcards cleaned_version
replace_snapshot cleaned_version
replace_leading_v cleaned_version
replace_99_does_not_exist cleaned_version
replace_timestamps cleaned_version
replace_groovy cleaned_version
replace_redhatx cleaned_version
VersionTagRecognizer.remove_minimum_stability cleaned_version
cleaned_version
end
def self.replace_99_does_not_exist val
if val.eql?("99.0-does-not-exist")
val.gsub!("99.0-does-not-exist", "0.0.0")
end
end
def self.replace_groovy val
if val.to_s.match(/\-groovy\-/)
val.gsub!("-groovy-", ".")
end
end
def self.replace_snapshot val
if val.to_s.match(/\-SNAPSHOT/)
val.gsub!("-SNAPSHOT", "")
end
end
def self.replace_redhatx val
if val.to_s.match(/\-redhat\-[0-9]+$/i)
val.gsub!(/\-redhat\-[0-9]+$/i, "")
end
end
# Some glory Java Devs used the timestamp as version string
# http://www.versioneye.com/package/commons-beanutils--commons-beanutils
# Ganz grosses Kino !
#
def self.replace_timestamps val
if val.to_s.match(/^[0-9]{8}$/)
val.gsub!(/^[0-9]{8}$/, "0.0.0")
elsif val.to_s.match(/^[0-9]{8}.[0-9]{6}$/)
val.gsub!(/^[0-9]{8}.[0-9]{6}$/, "0.0.0")
end
end
def self.replace_wildcards val
new_val = String.new(val)
new_val = "9999999" if val.to_s.match(/\.\*$/)
new_val
end
def self.replace_x_dev val
new_val = String.new(val)
if ['dev-master', 'master', 'trunk'].include?( val )
new_val = "99999999999"
elsif val.eql?("dev-develop")
new_val = "9999999999"
elsif val.to_s.match(/\Adev-/i)
new_val = "9999999"
elsif val.to_s.match(/\.x-dev$/i)
new_val = val.gsub("x-dev", "9999999")
elsif val.to_s.match(/-dev$/)
new_val = val.gsub("-dev", ".9999999")
end
new_val
end
def self.replace_leading_v val
val.gsub!(/^v/, "") if val.to_s.match(/^v[0-9]+/)
end
def self.replace_leading_vs a, b
self.replace_leading_v a
self.replace_leading_v b
end
def self.check_jquery_versioning(part1, part2)
# --- START ---- special case for awesome jquery shitty verison numbers
if ( part1.to_s.match(/^[0-9]+[a-zA-Z]+[0-9]+$/) != nil && part2.to_s.match(/^[0-9]+$/) != nil )
part1_1 = part1.match(/^[0-9]+/)
result = Versioncmp.compare_int(part1_1[0], part2)
if result != 0
return result
end
return -1
end
if ( part2.to_s.match(/^[0-9]+[a-zA-Z]+[0-9]+$/) != nil && part1.to_s.match(/^[0-9]+$/) != nil )
part2_1 = part2.to_s.match(/^[0-9]+/)
result = Versioncmp.compare_int(part1, part2_1[0])
if result != 0
return result
end
return 1
end
return nil
# --- END ---- special case for awesome jquery shitty verison numbers
end
def self.compare_int(ai, bi)
return -1 if (ai < bi)
return 0 if (ai == bi)
return 1
end
def self.compare_string(a, b)
return 0 if a.eql? b
return Natcmp.natcmp(a, b)
end
def self.compare_string_length(a, b)
return 0 if a.length() == b.length()
return 1 if a.length() < b.length()
return -1
end
end