diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 25c412be4b6b..7cd2001223a0 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -336,6 +336,65 @@ def segments # :nodoc: _segments.dup end + ## + # Deconstructs the version into an array for pattern matching. + # Returns the version segments as an array. + # + # Gem::Version.new("3.2.1").deconstruct #=> [3, 2, 1] + # Gem::Version.new("3.2.0.rc.2").deconstruct #=> [3, 2, 0, "rc", 2] + # + # This enables array pattern matching on version segments: + # + # case Gem::Version.new("3.2.1") + # in [major, minor, build] + # # major => 3, minor => 2, build => 1 + # end + # + # case Gem::Version.new("3.2.0.rc.2") + # in [major, minor, build, pre, *] + # # Matches prerelease versions + # end + alias_method :deconstruct, :segments + + ## + # Deconstructs the version into a hash for pattern matching. + # Returns a hash with keys +:major+, +:minor+, and +:build+. + # + # Note: RubyGems does not enforce a specific versioning scheme, and the + # names "major", "minor", and "build" are conventional. + # + # Gem::Version.new("3.2.1").deconstruct_keys(nil) #=> { major: 3, minor: 2, build: 1 } + # Gem::Version.new("3.2").deconstruct_keys(nil) #=> { major: 3, minor: 2, build: nil } + # Gem::Version.new("3").deconstruct_keys(nil) #=> { major: 3, minor: nil, build: nil } + # + # This enables hash pattern matching: + # + # case Gem::Version.new("3.2.1") + # in major: 3, minor: 2, build: 1 + # # Matches exactly 3.2.1 + # end + # + # Important: Hash pattern matching checks each segment independently, which + # differs from version comparison semantics: + # + # # This matches "3.2" but NOT "4.0" (since 0 < 2) + # case Gem::Version.new("4.0") + # in major: (3..), minor: (2..) + # "matches" + # else + # "no match" # => "no match" + # end + # + # # However, version comparison shows 4.0 > 3.2 + # Gem::Version.new("4.0") >= Gem::Version.new("3.2") #=> true + # + # For version comparison logic, use standard comparison operators or array + # patterns instead of hash patterns with ranges. + def deconstruct_keys(keys) + major, minor, build = segments + { major:, minor:, build: } + end + ## # A recommended version for use with a ~> Requirement. diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index cf771bc5a14a..d39f46bb22e6 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -226,6 +226,65 @@ def test_frozen_version assert_version_equal v("2"), v.bump end + def test_deconstruct + version = v("3.2.1") + major, minor, patch = version.deconstruct + assert_equal 3, major + assert_equal 2, minor + assert_equal 1, patch + end + + def test_deconstruct_keys + version = v("3.2.1") + assert_equal({ major: 3, minor: 2, build: 1 }, version.deconstruct_keys(nil)) + end + + def test_deconstruct_keys_two_segments + version = v("3.2") + assert_equal({ major: 3, minor: 2, build: nil }, version.deconstruct_keys(nil)) + end + + def test_deconstruct_keys_one_segment + version = v("3") + assert_equal({ major: 3, minor: nil, build: nil }, version.deconstruct_keys(nil)) + end + + def test_pattern_matching_array + case v("3.2.1") + in [major, minor, build] + assert_equal 3, major + assert_equal 2, minor + assert_equal 1, build + else + flunk "Array pattern did not match" + end + end + + def test_pattern_matching_hash + result = + case v("3.2.1") + in major: 3.., minor: 2.. then "matched" + else "no match" + end + assert_equal "matched", result + end + + def test_pattern_matching_hash_vs_comparison + # Hash pattern checks each segment independently + version = v("4.0") + result = + case version + in major: (3..), minor: (2..) + "matched" + else + "no match" + end + assert_equal "no match", result + + # But version comparison shows 4.0 > 3.2 + assert_operator v("4.0"), :>=, v("3.2") + end + # Asserts that +version+ is a prerelease. def assert_prerelease(version)