diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 8f4b1a2f01b387d1b1caa52ed1394abd786913da..829b387fd59e74eea32bd163fa58a7436c869907 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,8 @@ +* Fix `Range#===`, `Range#include?`, and `Range#cover?` to work with beginless (startless) + and endless range targets. Builds on work by Allen Hsu. + + *Andrew Hodgkinson* + * Fix `Range#include?` to work with beginless and endless ranges. *Allen Hsu* diff --git a/activesupport/lib/active_support/core_ext/range/compare_range.rb b/activesupport/lib/active_support/core_ext/range/compare_range.rb index ea1dc29a763186fd229c2347c87631eb018d0f78..d2c85d767913b0a4062cde2b1746411e37a323aa 100644 --- a/activesupport/lib/active_support/core_ext/range/compare_range.rb +++ b/activesupport/lib/active_support/core_ext/range/compare_range.rb @@ -11,13 +11,15 @@ module CompareWithRange # The native Range#=== behavior is untouched. # ('a'..'f') === ('c') # => true # (5..9) === (11) # => false + # + # The given range must be fully bounded, with both start and end. def ===(value) if value.is_a?(::Range) # 1...10 includes 1..9 but it does not include 1..10. # 1..10 includes 1...11 but it does not include 1...12. operator = exclude_end? && !value.exclude_end? ? :< : :<= value_max = !exclude_end? && value.exclude_end? ? value.max : value.last - super(value.first) && value_max.send(operator, last) + super(value.first) && (self.end.nil? || value_max.send(operator, last)) else super end @@ -32,13 +34,15 @@ def ===(value) # The native Range#include? behavior is untouched. # ('a'..'f').include?('c') # => true # (5..9).include?(11) # => false + # + # The given range must be fully bounded, with both start and end. def include?(value) if value.is_a?(::Range) # 1...10 includes 1..9 but it does not include 1..10. # 1..10 includes 1...11 but it does not include 1...12. operator = exclude_end? && !value.exclude_end? ? :< : :<= value_max = !exclude_end? && value.exclude_end? ? value.max : value.last - super(value.first) && value_max.send(operator, last) + super(value.first) && (self.end.nil? || value_max.send(operator, last)) else super end @@ -53,13 +57,15 @@ def include?(value) # The native Range#cover? behavior is untouched. # ('a'..'f').cover?('c') # => true # (5..9).cover?(11) # => false + # + # The given range must be fully bounded, with both start and end. def cover?(value) if value.is_a?(::Range) # 1...10 covers 1..9 but it does not cover 1..10. # 1..10 covers 1...11 but it does not cover 1...12. operator = exclude_end? && !value.exclude_end? ? :< : :<= value_max = !exclude_end? && value.exclude_end? ? value.max : value.last - super(value.first) && value_max.send(operator, last) + super(value.first) && (self.end.nil? || value_max.send(operator, last)) else super end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 29a727ed60ab8255dbb3d56f8aa3f3bc3a4439bd..428644339e43a691cf0aca2fcb2bebe41a2abecc 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -64,12 +64,28 @@ def test_should_include_other_with_exclusive_end def test_include_with_endless_range assert(eval("1..").include?(2)) end + + def test_should_include_range_with_endless_range + assert(eval("1..").include?(2..4)) + end + + def test_should_not_include_range_with_endless_range + assert_not(eval("1..").include?(0..4)) + end end if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0") def test_include_with_beginless_range assert(eval("..2").include?(1)) end + + def test_should_include_range_with_beginless_range + assert(eval("..2").include?(-1..1)) + end + + def test_should_not_include_range_with_beginless_range + assert_not(eval("..2").include?(-1..3)) + end end def test_should_compare_identical_inclusive @@ -84,6 +100,26 @@ def test_should_compare_other_with_exclusive_end assert((1..10) === (1...11)) end + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0") + def test_should_compare_range_with_endless_range + assert(eval("1..") === (2..4)) + end + + def test_should_not_compare_range_with_endless_range + assert_not(eval("1..") === (0..4)) + end + end + + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0") + def test_should_compare_range_with_beginless_range + assert(eval("..2") === (-1..1)) + end + + def test_should_not_compare_range_with_beginless_range + assert_not(eval("..2") === (-1..3)) + end + end + def test_exclusive_end_should_not_include_identical_with_inclusive_end assert_not_includes (1...10), 1..10 end @@ -109,6 +145,26 @@ def test_should_cover_other_with_exclusive_end assert((1..10).cover?(1...11)) end + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0") + def test_should_cover_range_with_endless_range + assert(eval("1..").cover?(2..4)) + end + + def test_should_not_cover_range_with_endless_range + assert_not(eval("1..").cover?(0..4)) + end + end + + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0") + def test_should_cover_range_with_beginless_range + assert(eval("..2").cover?(-1..1)) + end + + def test_should_not_cover_range_with_beginless_range + assert_not(eval("..2").cover?(-1..3)) + end + end + def test_overlaps_on_time time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) time_range_2 = Time.utc(2005, 12, 10, 17, 00)..Time.utc(2005, 12, 10, 18, 00)