Skip to content

Commit

Permalink
Ruby 2.4: compat with new Array#sum
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
jeremy committed Apr 19, 2016
1 parent f2f2d64 commit 7ad4690
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 3 deletions.
23 changes: 23 additions & 0 deletions 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')
Expand Down
14 changes: 14 additions & 0 deletions activesupport/lib/active_support/core_ext/enumerable.rb
Expand Up @@ -104,3 +104,17 @@ def sum(identity = 0)
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
24 changes: 21 additions & 3 deletions activesupport/test/core_ext/enumerable_test.rb
Expand Up @@ -10,22 +10,22 @@ def +(p) self.class.new(price + p.price) end
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
Expand Down Expand Up @@ -70,6 +70,24 @@ def test_range_sums
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) },
Expand Down

0 comments on commit 7ad4690

Please sign in to comment.