aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/array
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support/core_ext/array')
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb92
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb213
-rw-r--r--activesupport/lib/active_support/core_ext/array/extract.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/array/extract_options.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb109
-rw-r--r--activesupport/lib/active_support/core_ext/array/inquiry.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/array/prepend_and_append.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/array/wrap.rb48
8 files changed, 538 insertions, 0 deletions
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
new file mode 100644
index 0000000000..b7ff7a3907
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+class Array
+ # Returns the tail of the array from +position+.
+ #
+ # %w( a b c d ).from(0) # => ["a", "b", "c", "d"]
+ # %w( a b c d ).from(2) # => ["c", "d"]
+ # %w( a b c d ).from(10) # => []
+ # %w().from(0) # => []
+ # %w( a b c d ).from(-2) # => ["c", "d"]
+ # %w( a b c ).from(-10) # => []
+ def from(position)
+ self[position, length] || []
+ end
+
+ # Returns the beginning of the array up to +position+.
+ #
+ # %w( a b c d ).to(0) # => ["a"]
+ # %w( a b c d ).to(2) # => ["a", "b", "c"]
+ # %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
+ # %w().to(0) # => []
+ # %w( a b c d ).to(-2) # => ["a", "b", "c"]
+ # %w( a b c ).to(-10) # => []
+ def to(position)
+ if position >= 0
+ take position + 1
+ else
+ self[0..position]
+ end
+ end
+
+ # Returns a copy of the Array without the specified elements.
+ #
+ # people = ["David", "Rafael", "Aaron", "Todd"]
+ # people.without "Aaron", "Todd"
+ # # => ["David", "Rafael"]
+ #
+ # Note: This is an optimization of <tt>Enumerable#without</tt> that uses <tt>Array#-</tt>
+ # instead of <tt>Array#reject</tt> for performance reasons.
+ def without(*elements)
+ self - elements
+ end
+
+ # Equal to <tt>self[1]</tt>.
+ #
+ # %w( a b c d e ).second # => "b"
+ def second
+ self[1]
+ end
+
+ # Equal to <tt>self[2]</tt>.
+ #
+ # %w( a b c d e ).third # => "c"
+ def third
+ self[2]
+ end
+
+ # Equal to <tt>self[3]</tt>.
+ #
+ # %w( a b c d e ).fourth # => "d"
+ def fourth
+ self[3]
+ end
+
+ # Equal to <tt>self[4]</tt>.
+ #
+ # %w( a b c d e ).fifth # => "e"
+ def fifth
+ self[4]
+ end
+
+ # Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
+ #
+ # (1..42).to_a.forty_two # => 42
+ def forty_two
+ self[41]
+ end
+
+ # Equal to <tt>self[-3]</tt>.
+ #
+ # %w( a b c d e ).third_to_last # => "c"
+ def third_to_last
+ self[-3]
+ end
+
+ # Equal to <tt>self[-2]</tt>.
+ #
+ # %w( a b c d e ).second_to_last # => "d"
+ def second_to_last
+ self[-2]
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
new file mode 100644
index 0000000000..ea688ed2ea
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -0,0 +1,213 @@
+# frozen_string_literal: true
+
+require "active_support/xml_mini"
+require "active_support/core_ext/hash/keys"
+require "active_support/core_ext/string/inflections"
+require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/to_query"
+
+class Array
+ # Converts the array to a comma-separated sentence where the last element is
+ # joined by the connector word.
+ #
+ # You can pass the following options to change the default behavior. If you
+ # pass an option key that doesn't exist in the list below, it will raise an
+ # <tt>ArgumentError</tt>.
+ #
+ # ==== Options
+ #
+ # * <tt>:words_connector</tt> - The sign or word used to join the elements
+ # in arrays with two or more elements (default: ", ").
+ # * <tt>:two_words_connector</tt> - The sign or word used to join the elements
+ # in arrays with two elements (default: " and ").
+ # * <tt>:last_word_connector</tt> - The sign or word used to join the last element
+ # in arrays with three or more elements (default: ", and ").
+ # * <tt>:locale</tt> - If +i18n+ is available, you can set a locale and use
+ # the connector options defined on the 'support.array' namespace in the
+ # corresponding dictionary file.
+ #
+ # ==== Examples
+ #
+ # [].to_sentence # => ""
+ # ['one'].to_sentence # => "one"
+ # ['one', 'two'].to_sentence # => "one and two"
+ # ['one', 'two', 'three'].to_sentence # => "one, two, and three"
+ #
+ # ['one', 'two'].to_sentence(passing: 'invalid option')
+ # # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale
+ #
+ # ['one', 'two'].to_sentence(two_words_connector: '-')
+ # # => "one-two"
+ #
+ # ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ')
+ # # => "one or two or at least three"
+ #
+ # Using <tt>:locale</tt> option:
+ #
+ # # Given this locale dictionary:
+ # #
+ # # es:
+ # # support:
+ # # array:
+ # # words_connector: " o "
+ # # two_words_connector: " y "
+ # # last_word_connector: " o al menos "
+ #
+ # ['uno', 'dos'].to_sentence(locale: :es)
+ # # => "uno y dos"
+ #
+ # ['uno', 'dos', 'tres'].to_sentence(locale: :es)
+ # # => "uno o dos o al menos tres"
+ def to_sentence(options = {})
+ options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
+
+ default_connectors = {
+ words_connector: ", ",
+ two_words_connector: " and ",
+ last_word_connector: ", and "
+ }
+ if defined?(I18n)
+ i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
+ default_connectors.merge!(i18n_connectors)
+ end
+ options = default_connectors.merge!(options)
+
+ case length
+ when 0
+ ""
+ when 1
+ "#{self[0]}"
+ when 2
+ "#{self[0]}#{options[:two_words_connector]}#{self[1]}"
+ else
+ "#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
+ end
+ end
+
+ # Extends <tt>Array#to_s</tt> to convert a collection of elements into a
+ # comma separated id list if <tt>:db</tt> argument is given as the format.
+ #
+ # Blog.all.to_formatted_s(:db) # => "1,2,3"
+ # Blog.none.to_formatted_s(:db) # => "null"
+ # [1,2].to_formatted_s # => "[1, 2]"
+ def to_formatted_s(format = :default)
+ case format
+ when :db
+ if empty?
+ "null"
+ else
+ collect(&:id).join(",")
+ end
+ else
+ to_default_s
+ end
+ end
+ alias_method :to_default_s, :to_s
+ alias_method :to_s, :to_formatted_s
+
+ # Returns a string that represents the array in XML by invoking +to_xml+
+ # on each element. Active Record collections delegate their representation
+ # in XML to this method.
+ #
+ # All elements are expected to respond to +to_xml+, if any of them does
+ # not then an exception is raised.
+ #
+ # The root node reflects the class name of the first element in plural
+ # if all elements belong to the same type and that's not Hash:
+ #
+ # customer.projects.to_xml
+ #
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <projects type="array">
+ # <project>
+ # <amount type="decimal">20000.0</amount>
+ # <customer-id type="integer">1567</customer-id>
+ # <deal-date type="date">2008-04-09</deal-date>
+ # ...
+ # </project>
+ # <project>
+ # <amount type="decimal">57230.0</amount>
+ # <customer-id type="integer">1567</customer-id>
+ # <deal-date type="date">2008-04-15</deal-date>
+ # ...
+ # </project>
+ # </projects>
+ #
+ # Otherwise the root element is "objects":
+ #
+ # [{ foo: 1, bar: 2}, { baz: 3}].to_xml
+ #
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <objects type="array">
+ # <object>
+ # <bar type="integer">2</bar>
+ # <foo type="integer">1</foo>
+ # </object>
+ # <object>
+ # <baz type="integer">3</baz>
+ # </object>
+ # </objects>
+ #
+ # If the collection is empty the root element is "nil-classes" by default:
+ #
+ # [].to_xml
+ #
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <nil-classes type="array"/>
+ #
+ # To ensure a meaningful root element use the <tt>:root</tt> option:
+ #
+ # customer_with_no_projects.projects.to_xml(root: 'projects')
+ #
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <projects type="array"/>
+ #
+ # By default name of the node for the children of root is <tt>root.singularize</tt>.
+ # You can change it with the <tt>:children</tt> option.
+ #
+ # The +options+ hash is passed downwards:
+ #
+ # Message.all.to_xml(skip_types: true)
+ #
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <messages>
+ # <message>
+ # <created-at>2008-03-07T09:58:18+01:00</created-at>
+ # <id>1</id>
+ # <name>1</name>
+ # <updated-at>2008-03-07T09:58:18+01:00</updated-at>
+ # <user-id>1</user-id>
+ # </message>
+ # </messages>
+ #
+ def to_xml(options = {})
+ require "active_support/builder" unless defined?(Builder)
+
+ options = options.dup
+ options[:indent] ||= 2
+ options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
+ options[:root] ||= \
+ if first.class != Hash && all? { |e| e.is_a?(first.class) }
+ underscored = ActiveSupport::Inflector.underscore(first.class.name)
+ ActiveSupport::Inflector.pluralize(underscored).tr("/", "_")
+ else
+ "objects"
+ end
+
+ builder = options[:builder]
+ builder.instruct! unless options.delete(:skip_instruct)
+
+ root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
+ children = options.delete(:children) || root.singularize
+ attributes = options[:skip_types] ? {} : { type: "array" }
+
+ if empty?
+ builder.tag!(root, attributes)
+ else
+ builder.tag!(root, attributes) do
+ each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
+ yield builder if block_given?
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/extract.rb b/activesupport/lib/active_support/core_ext/array/extract.rb
new file mode 100644
index 0000000000..cc5a8a3f88
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/extract.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Array
+ # Removes and returns the elements for which the block returns a true value.
+ # If no block is given, an Enumerator is returned instead.
+ #
+ # numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+ # odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
+ # numbers # => [0, 2, 4, 6, 8]
+ def extract!
+ return to_enum(:extract!) { size } unless block_given?
+
+ extracted_elements = []
+
+ reject! do |element|
+ extracted_elements << element if yield(element)
+ end
+
+ extracted_elements
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/extract_options.rb b/activesupport/lib/active_support/core_ext/array/extract_options.rb
new file mode 100644
index 0000000000..8c7cb2e780
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/extract_options.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Hash
+ # By default, only instances of Hash itself are extractable.
+ # Subclasses of Hash may implement this method and return
+ # true to declare themselves as extractable. If a Hash
+ # is extractable, Array#extract_options! pops it from
+ # the Array when it is the last element of the Array.
+ def extractable_options?
+ instance_of?(Hash)
+ end
+end
+
+class Array
+ # Extracts options from a set of arguments. Removes and returns the last
+ # element in the array if it's a hash, otherwise returns a blank hash.
+ #
+ # def options(*args)
+ # args.extract_options!
+ # end
+ #
+ # options(1, 2) # => {}
+ # options(1, 2, a: :b) # => {:a=>:b}
+ def extract_options!
+ if last.is_a?(Hash) && last.extractable_options?
+ pop
+ else
+ {}
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
new file mode 100644
index 0000000000..67e760bc4b
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+class Array
+ # Splits or iterates over the array in groups of size +number+,
+ # padding any remaining slots with +fill_with+ unless it is +false+.
+ #
+ # %w(1 2 3 4 5 6 7 8 9 10).in_groups_of(3) {|group| p group}
+ # ["1", "2", "3"]
+ # ["4", "5", "6"]
+ # ["7", "8", "9"]
+ # ["10", nil, nil]
+ #
+ # %w(1 2 3 4 5).in_groups_of(2, '&nbsp;') {|group| p group}
+ # ["1", "2"]
+ # ["3", "4"]
+ # ["5", "&nbsp;"]
+ #
+ # %w(1 2 3 4 5).in_groups_of(2, false) {|group| p group}
+ # ["1", "2"]
+ # ["3", "4"]
+ # ["5"]
+ def in_groups_of(number, fill_with = nil)
+ if number.to_i <= 0
+ raise ArgumentError,
+ "Group size must be a positive integer, was #{number.inspect}"
+ end
+
+ if fill_with == false
+ collection = self
+ else
+ # size % number gives how many extra we have;
+ # subtracting from number gives how many to add;
+ # modulo number ensures we don't add group of just fill.
+ padding = (number - size % number) % number
+ collection = dup.concat(Array.new(padding, fill_with))
+ end
+
+ if block_given?
+ collection.each_slice(number) { |slice| yield(slice) }
+ else
+ collection.each_slice(number).to_a
+ end
+ end
+
+ # Splits or iterates over the array in +number+ of groups, padding any
+ # remaining slots with +fill_with+ unless it is +false+.
+ #
+ # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
+ # ["1", "2", "3", "4"]
+ # ["5", "6", "7", nil]
+ # ["8", "9", "10", nil]
+ #
+ # %w(1 2 3 4 5 6 7 8 9 10).in_groups(3, '&nbsp;') {|group| p group}
+ # ["1", "2", "3", "4"]
+ # ["5", "6", "7", "&nbsp;"]
+ # ["8", "9", "10", "&nbsp;"]
+ #
+ # %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
+ # ["1", "2", "3"]
+ # ["4", "5"]
+ # ["6", "7"]
+ def in_groups(number, fill_with = nil)
+ # size.div number gives minor group size;
+ # size % number gives how many objects need extra accommodation;
+ # each group hold either division or division + 1 items.
+ division = size.div number
+ modulo = size % number
+
+ # create a new array avoiding dup
+ groups = []
+ start = 0
+
+ number.times do |index|
+ length = division + (modulo > 0 && modulo > index ? 1 : 0)
+ groups << last_group = slice(start, length)
+ last_group << fill_with if fill_with != false &&
+ modulo > 0 && length == division
+ start += length
+ end
+
+ if block_given?
+ groups.each { |g| yield(g) }
+ else
+ groups
+ end
+ end
+
+ # Divides the array into one or more subarrays based on a delimiting +value+
+ # or the result of an optional block.
+ #
+ # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
+ # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
+ def split(value = nil)
+ arr = dup
+ result = []
+ if block_given?
+ while (idx = arr.index { |i| yield i })
+ result << arr.shift(idx)
+ arr.shift
+ end
+ else
+ while (idx = arr.index(value))
+ result << arr.shift(idx)
+ arr.shift
+ end
+ end
+ result << arr
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/inquiry.rb b/activesupport/lib/active_support/core_ext/array/inquiry.rb
new file mode 100644
index 0000000000..92c61bf201
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/inquiry.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "active_support/array_inquirer"
+
+class Array
+ # Wraps the array in an +ArrayInquirer+ object, which gives a friendlier way
+ # to check its string-like contents.
+ #
+ # pets = [:cat, :dog].inquiry
+ #
+ # pets.cat? # => true
+ # pets.ferret? # => false
+ #
+ # pets.any?(:cat, :ferret) # => true
+ # pets.any?(:ferret, :alligator) # => false
+ def inquiry
+ ActiveSupport::ArrayInquirer.new(self)
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
new file mode 100644
index 0000000000..ba3739f640
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+
+ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Array#append and Array#prepend natively, so requiring active_support/core_ext/array/prepend_and_append is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb
new file mode 100644
index 0000000000..d62f97edbf
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/array/wrap.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class Array
+ # Wraps its argument in an array unless it is already an array (or array-like).
+ #
+ # Specifically:
+ #
+ # * If the argument is +nil+ an empty array is returned.
+ # * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned.
+ # * Otherwise, returns an array with the argument as its single element.
+ #
+ # Array.wrap(nil) # => []
+ # Array.wrap([1, 2, 3]) # => [1, 2, 3]
+ # Array.wrap(0) # => [0]
+ #
+ # This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences:
+ #
+ # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
+ # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
+ # an array with the argument as its single element right away.
+ # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
+ # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
+ # * It does not call +to_a+ on the argument, if the argument does not respond to +to_ary+
+ # it returns an array with the argument as its single element.
+ #
+ # The last point is easily explained with some enumerables:
+ #
+ # Array(foo: :bar) # => [[:foo, :bar]]
+ # Array.wrap(foo: :bar) # => [{:foo=>:bar}]
+ #
+ # There's also a related idiom that uses the splat operator:
+ #
+ # [*object]
+ #
+ # which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise.
+ #
+ # The differences with <tt>Kernel#Array</tt> explained above
+ # apply to the rest of <tt>object</tt>s.
+ def self.wrap(object)
+ if object.nil?
+ []
+ elsif object.respond_to?(:to_ary)
+ object.to_ary || [object]
+ else
+ [object]
+ end
+ end
+end