diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 091858b7570e..b7966c3973f7 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -222,6 +222,7 @@ def initialize(version) end @version = -@version @segments = nil + @sort_key = compute_sort_key end ## @@ -350,6 +351,12 @@ def <=>(other) end return unless Gem::Version === other + + # Fast path for comparison when available. + if @sort_key && other.sort_key + return @sort_key <=> other.sort_key + end + return 0 if @version == other.version || canonical_segments == other.canonical_segments lhsegments = canonical_segments @@ -415,6 +422,21 @@ def freeze protected + attr_reader :sort_key # :nodoc: + + def compute_sort_key + segments = canonical_segments + return if segments.size > 4 || prerelease? || segments.any? {|segment| segment > 65_000 } + + base = 1_000_000_000_000 + + segments.sum do |segment| + result = segment * base + base /= 10_000 + result + end + end + def _segments # segments is lazy so it can pick up version values that come from # old marshaled versions, which don't go through marshal_load. diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index 3987c620d0a7..7ad085d48018 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -200,6 +200,37 @@ def test_semver assert_less_than "1.0.0-1", "1" end + def test_sort_key_is_computed_on_regular_release + refute_nil v("9.8.7").send(:sort_key) + end + + def test_sort_key_is_computed_on_security_release + refute_nil v("9.8.7.1").send(:sort_key) + end + + def test_sort_key_is_not_computed_on_prerelease + assert_nil v("9.8.7.pre1").send(:sort_key) + end + + def test_sort_key_is_not_computed_on_version_with_more_segments + assert_nil v("1.1.1.1.1.1.1").send(:sort_key) + end + + def test_sort_key_is_not_computed_on_huge_numbers + assert_nil v("2.30.1.250000").send(:sort_key) + end + + def test_sort_key_is_used_for_comparison + a = v("18.0.1") + b = v("18.0.2") + + # Ensure the slow path isn't getting hit + a.instance_variable_set(:@version, nil) + a.instance_variable_set(:@canonical_segments, nil) + + assert_operator(a, :<, b) + end + # modifying the segments of a version should not affect the segments of the cached version object def test_segments v("9.8.7").segments[2] += 1