aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/enumerable.rb
blob: 728d3323065f56c197dda4e668d768003f3af5f0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# frozen_string_literal: true

module Enumerable
  INDEX_WITH_DEFAULT = Object.new
  private_constant :INDEX_WITH_DEFAULT

  # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
  # when we omit an identity.

  # :stopdoc:

  # We can't use Refinements here because Refinements with Module which will be prepended
  # doesn't work well https://bugs.ruby-lang.org/issues/13446
  alias :_original_sum_with_required_identity :sum
  private :_original_sum_with_required_identity

  # :startdoc:

  # Calculates a sum from the elements.
  #
  #  payments.sum { |p| p.price * p.tax_rate }
  #  payments.sum(&:price)
  #
  # The latter is a shortcut for:
  #
  #  payments.inject(0) { |sum, p| sum + p.price }
  #
  # It can also calculate the sum without the use of a block.
  #
  #  [5, 15, 10].sum # => 30
  #  ['foo', 'bar'].sum # => "foobar"
  #  [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
  #
  # The default sum of an empty list is zero. You can override this default:
  #
  #  [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
  def sum(identity = nil, &block)
    if identity
      _original_sum_with_required_identity(identity, &block)
    elsif block_given?
      map(&block).sum(identity)
    else
      inject(:+) || 0
    end
  end

  # Convert an enumerable to a hash keying it by the block return value.
  #
  #   people.index_by(&:login)
  #   # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
  #
  #   people.index_by { |person| "#{person.first_name} #{person.last_name}" }
  #   # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
  def index_by
    if block_given?
      result = {}
      each { |elem| result[yield(elem)] = elem }
      result
    else
      to_enum(:index_by) { size if respond_to?(:size) }
    end
  end

  # Convert an enumerable to a hash keying it with the enumerable items and with the values returned in the block.
  #
  #   post = Post.new(title: "hey there", body: "what's up?")
  #
  #   %i( title body ).index_with { |attr_name| post.public_send(attr_name) }
  #   # => { title: "hey there", body: "what's up?" }
  def index_with(default = INDEX_WITH_DEFAULT)
    if block_given?
      result = {}
      each { |elem| result[elem] = yield(elem) }
      result
    elsif default != INDEX_WITH_DEFAULT
      result = {}
      each { |elem| result[elem] = default }
      result
    else
      to_enum(:index_with) { size if respond_to?(:size) }
    end
  end

  # Returns +true+ if the enumerable has more than 1 element. Functionally
  # equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too,
  # much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+
  # if more than one person is over 26.
  def many?
    cnt = 0
    if block_given?
      any? do |element|
        cnt += 1 if yield element
        cnt > 1
      end
    else
      any? { (cnt += 1) > 1 }
    end
  end

  # Returns a new array that includes the passed elements.
  #
  #   [ 1, 2, 3 ].including(4, 5)
  #   # => [ 1, 2, 3, 4, 5 ]
  #
  #   ["David", "Rafael"].including %w[ Aaron Todd ]
  #   # => ["David", "Rafael", "Aaron", "Todd"]
  def including(*elements)
    to_a.including(*elements)
  end

  # The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the
  # collection does not include the object.
  def exclude?(object)
    !include?(object)
  end

  # Returns a copy of the enumerable excluding the specified elements.
  #
  #   ["David", "Rafael", "Aaron", "Todd"].excluding "Aaron", "Todd"
  #   # => ["David", "Rafael"]
  #
  #   ["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ]
  #   # => ["David", "Rafael"]
  #
  #   {foo: 1, bar: 2, baz: 3}.excluding :bar
  #   # => {foo: 1, baz: 3}
  def excluding(*elements)
    elements.flatten!(1)
    reject { |element| elements.include?(element) }
  end

  # Alias for #excluding.
  def without(*elements)
    excluding(*elements)
  end

  # Convert an enumerable to an array based on the given key.
  #
  #   [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name)
  #   # => ["David", "Rafael", "Aaron"]
  #
  #   [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name)
  #   # => [[1, "David"], [2, "Rafael"]]
  def pluck(*keys)
    if keys.many?
      map { |element| keys.map { |key| element[key] } }
    else
      map { |element| element[keys.first] }
    end
  end

  # Returns a new +Array+ without the blank items.
  # Uses Object#blank? for determining if an item is blank.
  #
  #    [1, "", nil, 2, " ", [], {}, false, true].compact_blank
  #    # =>  [1, 2, true]
  #
  #    Set.new([nil, "", 1, 2])
  #    # => [2, 1] (or [1, 2])
  #
  # When called on a +Hash+, returns a new +Hash+ without the blank values.
  #
  #    { a: "", b: 1, c: nil, d: [], e: false, f: true }.compact_blank
  #    #=> { b: 1, f: true }
  def compact_blank
    reject(&:blank?)
  end
end

class Hash
  # Hash#reject has its own definition, so this needs one too.
  def compact_blank #:nodoc:
    reject { |_k, v| v.blank? }
  end

  # Removes all blank values from the +Hash+ in place and returns self.
  # Uses Object#blank? for determining if a value is blank.
  #
  #    h = { a: "", b: 1, c: nil, d: [], e: false, f: true }
  #    h.compact_blank!
  #    # => { b: 1, f: true }
  def compact_blank!
    # use delete_if rather than reject! because it always returns self even if nothing changed
    delete_if { |_k, v| v.blank? }
  end
end

class Range #:nodoc:
  # Optimize range sum to use arithmetic progression if a block is not given and
  # we have a range of numeric values.
  def sum(identity = nil)
    if block_given? || !(first.is_a?(Integer) && last.is_a?(Integer))
      super
    else
      actual_last = exclude_end? ? (last - 1) : last
      if actual_last >= first
        sum = identity || 0
        sum + (actual_last - first + 1) * (actual_last + first) / 2
      else
        identity || 0
      end
    end
  end
end

# Using Refinements here in order not to expose our internal method
using Module.new {
  refine Array do
    alias :orig_sum :sum
  end
}

class Array #:nodoc:
  # Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
  def sum(init = nil, &block)
    if init.is_a?(Numeric) || first.is_a?(Numeric)
      init ||= 0
      orig_sum(init, &block)
    else
      super
    end
  end

  # Removes all blank elements from the +Array+ in place and returns self.
  # Uses Object#blank? for determining if an item is blank.
  #
  #    a = [1, "", nil, 2, " ", [], {}, false, true]
  #    a.compact_blank!
  #    # =>  [1, 2, true]
  def compact_blank!
    # use delete_if rather than reject! because it always returns self even if nothing changed
    delete_if(&:blank?)
  end
end