From 7ad4690b2149fbb23faa179c21698b92ff383c73 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 18 Apr 2016 22:46:29 -0700 Subject: Ruby 2.4: compat with new Array#sum Ruby 2.4 introduces `Array#sum`, but it only supports numeric elements, breaking our `Enumerable#sum` which supports arbitrary `Object#+`. To fix, override `Array#sum` with our compatible implementation. Native Ruby 2.4: %w[ a b ].sum # => TypeError: String can't be coerced into Fixnum With `Enumerable#sum` shim: %w[ a b ].sum # => 'ab' We tried shimming the fast path and falling back to the compatible path if it fails, but that ends up slower even in simple causes due to the cost of exception handling. Our only choice is to override the native `Array#sum` with our `Enumerable#sum`. --- activesupport/CHANGELOG.md | 23 +++++++++++++++++++++ .../lib/active_support/core_ext/enumerable.rb | 14 +++++++++++++ activesupport/test/core_ext/enumerable_test.rb | 24 +++++++++++++++++++--- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 16b0a06d44..58ec6fb4fe 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,26 @@ +* `Array#sum` compat with Ruby 2.4's native method. + + Ruby 2.4 introduces `Array#sum`, but it only supports numeric elements, + breaking our `Enumerable#sum` which supports arbitrary `Object#+`. + To fix, override `Array#sum` with our compatible implementation. + + Native Ruby 2.4: + + %w[ a b ].sum + # => TypeError: String can't be coerced into Fixnum + + With `Enumerable#sum` shim: + + %w[ a b ].sum + # => 'ab' + + We tried shimming the fast path and falling back to the compatible path + if it fails, but that ends up slower even in simple causes due to the cost + of exception handling. Our only choice is to override the native `Array#sum` + with our `Enumerable#sum`. + + *Jeremy Daer* + * `ActiveSupport::Duration` supports ISO8601 formatting and parsing. ActiveSupport::Duration.parse('P3Y6M4DT12H30M5S') diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 8a74ad4d66..f297214d0f 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -104,3 +104,17 @@ class Range #:nodoc: end end end + +# Array#sum was added in Ruby 2.4 but it only works with Numeric elements. +# +# We tried shimming it to attempt the fast native method, rescue TypeError, +# and fall back to the compatible implementation, but that's much slower than +# just calling the compat method in the first place. +if Array.instance_methods(false).include?(:sum) && (%w[a].sum rescue true) + class Array + def sum(*args) #:nodoc: + # Use Enumerable#sum instead. + super + end + end +end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index f09b7d8850..976c8b2b81 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -10,22 +10,22 @@ class SummablePayment < Payment end class EnumerableTests < ActiveSupport::TestCase - class GenericEnumerable include Enumerable + def initialize(values = [1, 2, 3]) @values = values end def each - @values.each{|v| yield v} + @values.each { |v| yield v } end end def test_sums enum = GenericEnumerable.new([5, 15, 10]) assert_equal 30, enum.sum - assert_equal 60, enum.sum { |i| i * 2} + assert_equal 60, enum.sum { |i| i * 2 } enum = GenericEnumerable.new(%w(a b c)) assert_equal 'abc', enum.sum @@ -70,6 +70,24 @@ class EnumerableTests < ActiveSupport::TestCase assert_equal 42, (10...10).sum(42) end + def test_array_sums + enum = [5, 15, 10] + assert_equal 30, enum.sum + assert_equal 60, enum.sum { |i| i * 2 } + + enum = %w(a b c) + assert_equal 'abc', enum.sum + assert_equal 'aabbcc', enum.sum { |i| i * 2 } + + payments = [ Payment.new(5), Payment.new(15), Payment.new(10) ] + assert_equal 30, payments.sum(&:price) + assert_equal 60, payments.sum { |p| p.price * 2 } + + payments = [ SummablePayment.new(5), SummablePayment.new(15) ] + assert_equal SummablePayment.new(20), payments.sum + assert_equal SummablePayment.new(20), payments.sum { |p| p } + end + def test_index_by payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) }, -- cgit v1.2.3