jirutka/corefines

View on GitHub
lib/corefines/hash.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'corefines/support/alias_submodules'

module Corefines
  module Hash

    ##
    # @!method compact
    #   @example
    #     hash = { a: true, b: false, c: nil }
    #     hash.compact # => { a: true, b: false }
    #     hash # => { a: true, b: false, c: nil }
    #     { c: nil }.compact # => {}
    #
    #   @return [Hash] a new hash with no key-value pairs which value is +nil+.
    #
    # @!method compact!
    #   Removes all key-value pairs from the hash which value is +nil+.
    #   Same as {#compact}, but modifies +self+.
    #
    #   @return [Hash] self
    #
    module Compact
      refine ::Hash do
        def compact
          reject { |_, value| value.nil? }
        end

        def compact!
          delete_if { |_, value| value.nil? }
        end
      end
    end

    ##
    # @!method except(*keys)
    #   @example
    #     hash = { a: 1, b: 2, c: 3, d: 4 }
    #     hash.except(:a, :d)  # => { b: 2, c: 3 }
    #     hash  # => { a: 1, b: 2, c: 3, d: 4 }
    #
    #   @param *keys the keys to exclude from the hash.
    #   @return [Hash] a new hash without the specified key/value pairs.
    #
    # @!method except!(*keys)
    #   Removes the specified keys/value pairs in-place.
    #
    #   @example
    #     hash = { a: 1, b: 2, c: 3, d: 4 }
    #     hash.except(:a, :d)  # => { b: 2, c: 3 }
    #     hash  # => { b: 2, c: 3 }
    #
    #   @see #except
    #   @param *keys (see #except)
    #   @return [Hash] a hash containing the removed key/value pairs.
    #
    module Except
      refine ::Hash do
        def except(*keys)
          keys.each_with_object(dup) do |k, hash|
            hash.delete(k)
          end
        end

        def except!(*keys)
          keys.each_with_object(dup.clear) do |k, deleted|
            deleted[k] = delete(k) if has_key? k
          end
        end
      end
    end

    ##
    # @!method flat_map(&block)
    #   Returns a new hash with the merged results of running the _block_ once
    #   for every entry in +self+.
    #
    #   @example
    #     hash = { a: 1, b: 2, c: 3 }
    #     hash.flat_map { |k, v| {k => v * 2, k.upcase => v} if v % 2 == 1 }
    #     # => { a: 2, A: 1, c: 6, C: 3 }
    #     hash.flat_map { |k, v| [[k, v * 2], [k.upcase => v]] if v % 2 == 1 }
    #     # => { a: 2, A: 1, c: 6, C: 3 }
    #
    #   @yield [key, value] gives every key-value pair to the block.
    #   @yieldreturn [#to_h, Array, nil] an object that will be interpreted as
    #     a Hash and merged into the result, or nil to omit this key-value.
    #   @return [Hash] a new hash.
    module FlatMap
      refine ::Hash do
        def flat_map
          each_with_object({}) do |(key, value), hash|
            yielded = yield(key, value)

            if yielded.is_a? ::Hash
              hash.merge!(yielded)
            elsif yielded.is_a? ::Array
              # Array#to_h exists since Ruby 2.1.
              yielded.each do |pair|
                hash.store(*pair)
              end
            elsif yielded
              hash.merge!(yielded.to_h)
            end
          end
        end
      end
    end

    ##
    # @!method only(*keys)
    #   @example
    #     hash = { a: 1, b: 2, c: 3, d: 4 }
    #     hash.only(:a, :d)  # => { a: 1, d: 4 }
    #     hash  # => { a: 1, b: 2, c: 3, d: 4 }
    #
    #   @param *keys the keys to include in the hash.
    #   @return [Hash] a new hash with only the specified key/value pairs.
    #
    # @!method only!(*keys)
    #   Removes all key/value pairs except the ones specified by _keys_.
    #
    #   @example
    #     hash = { a: 1, b: 2, c: 3, d: 4 }
    #     hash.only(:a, :d)  # => { a: 1, d: 4 }
    #     hash  # => { a: 1, d: 4 }
    #
    #   @see #only
    #   @param *keys (see #only)
    #   @return [Hash] a hash containing the removed key/value pairs.
    #
    module Only
      refine ::Hash do
        def only(*keys)
          # Note: self.dup is used to preserve the default_proc.
          keys.each_with_object(dup.clear) do |k, hash|
            hash[k] = self[k] if has_key? k
          end
        end

        def only!(*keys)
          deleted = keys.each_with_object(dup) do |k, hash|
            hash.delete(k)
          end
          replace only(*keys)

          deleted
        end
      end
    end

    ##
    # @!method +(other_hash)
    #   Alias for +#merge+.
    #
    #   @example
    #     {a: 1, b: 2} + {b: 3, c: 4}
    #      => {a: 1, b: 3, c: 4}
    #
    #   @param other_hash [Hash]
    #   @return [Hash] a new hash containing the contents of _other_hash_ and
    #     this hash. The value for entries with duplicate keys will be that of
    #     _other_hash_.
    #
    module OpAdd
      refine ::Hash do
        def +(other_hash)
          merge other_hash
        end
      end
    end

    ##
    # @!method recurse(&block)
    #   Transforms this hash and each of its sub-hashes recursively using the
    #   given _block_.
    #
    #   It does not mutate the hash if the given _block_ is pure (i.e. does not
    #   modify given hashes, but returns new ones).
    #
    #   @example
    #     hash = {"a" => 1, "b" => {"c" => {"e" => 5}, "d" => 4}}
    #
    #     hash.recurse { |h| h.symbolize_keys }  # => {a: 1, b: {c: {e: 5}, d: 4}}
    #     hash  # => {"a" => 1, "b" => {"c" => {"e" => 5}, "d" => 4}}
    #
    #     hash.recurse { |h| h.symbolize_keys! }  # => {a: 1, b: {c: {e: 5}, d: 4}}
    #     hash  # => {a: 1, b: {c: {e: 5}, d: 4}}
    #
    #   @yield [Hash] gives this hash and every sub-hash (recursively).
    #     The return value replaces the old value.
    #   @return [Hash] a result of applying _block_ to this hash and each of
    #     its sub-hashes (recursively).
    #
    module Recurse
      refine ::Hash do
        def recurse(&block)
          h = yield(self)
          h.each do |key, value|
            h[key] = value.recurse(&block) if value.is_a? ::Hash
          end
        end
      end
    end

    ##
    # @!method rekey(key_map = nil, &block)
    #   Returns a new hash with keys transformed according to the given
    #   _key_map_ or the _block_.
    #
    #   If no _key_map_ or _block_ is given, then all keys are converted
    #   to +Symbols+, as long as they respond to +to_sym+.
    #
    #   @example
    #     hash = { :a => 1, 'b' => 2 }
    #     hash.rekey(:a => :x, :c => :y) # => { :x => 1, 'b' => 2 }
    #     hash.rekey(&:to_s) # => { 'a' => 1, 'b' => 2 }
    #     hash.rekey { |k| k.to_s.upcase } # => { 'A' => 1, 'B' => 2 }
    #     hash.rekey # => { :a => 1, :b => 2 }
    #
    #   @overload rekey(key_map)
    #     @param key_map [Hash] a translation map from the old keys to the new
    #       keys; <tt>{from_key => to_key, ...}</tt>.
    #
    #   @overload rekey
    #     @yield [key, value] gives every key-value pair to the block.
    #       The return value becomes a new key.
    #
    #   @return [Hash] a new hash.
    #   @raise ArgumentError if both _key_map_ and the _block_ are given.
    #
    # @!method rekey!(key_map = nil, &block)
    #   Transforms keys according to the given _key_map_ or the _block_.
    #   Same as {#rekey}, but modifies +self+.
    #
    #   @overload rekey!(key_map)
    #     @param key_map [Hash] a translation map from the old keys to the new
    #       keys; <tt>{from_key => to_key, ...}</tt>.
    #
    #   @overload rekey!
    #     @yield [key, value] gives every key-value pair to the block.
    #       The return value becomes a new key.
    #
    #   @return [Hash] self
    #   @raise (see #rekey)
    #
    module Rekey
      refine ::Hash do
        def rekey(key_map = nil, &block)
          fail ArgumentError, "provide key_map, or block, not both" if key_map && block

          # Note: self.dup is used to preserve the default_proc.
          if key_map
            key_map.each_with_object(dup) do |(from, to), hash|
              hash[to] = hash.delete(from) if hash.key? from
            end
          else
            transform_key = if !block
              ->(k, _) { k.to_sym rescue k }
            elsif block.arity.abs == 1 || block.lambda? && block.arity != 2
              # This is needed for "symbol procs" (e.g. &:to_s). It behaves
              # differently since Ruby 3.0!
              # Ruby <3.0: block.arity => -1, block.lambda? => false
              # Ruby 3.0: block.arity => -2, block.lambda? => true
              ->(k, _) { block[k] }
            else
              block
            end

            each_with_object(dup.clear) do |(key, value), hash|
              hash[ transform_key[key, value] ] = value
            end
          end
        end

        def rekey!(key_map = nil, &block)
          replace rekey(key_map, &block)
        end
      end
    end

    ##
    # @!method symbolize_keys
    #   @example
    #     hash = { 'name' => 'Lindsey', :born => 1986 }
    #     hash.symbolize_keys # => { :name => 'Lindsey', :born => 1986 }
    #     hash # => { 'name' => 'Lindsey', :born => 1986 }
    #
    #   @return [Hash] a new hash with all keys converted to symbols, as long
    #     as they respond to +to_sym+.
    #
    # @!method symbolize_keys!
    #   Converts all keys to symbols, as long as they respond to +to_sym+.
    #   Same as {#symbolize_keys}, but modifies +self+.
    #
    #   @return [Hash] self
    #
    module SymbolizeKeys
      refine ::Hash do
        def symbolize_keys
          each_with_object(dup.clear) do |(key, value), hash|
            hash[(key.to_sym rescue key)] = value
          end
        end

        def symbolize_keys!
          keys.each do |key|
            self[(key.to_sym rescue key)] = delete(key)
          end
          self
        end
      end
    end

    include Support::AliasSubmodules

    class << self
      alias_method :compact!, :compact
      alias_method :except!, :except
      alias_method :only!, :only
      alias_method :rekey!, :rekey
      alias_method :symbolize_keys!, :symbolize_keys
    end
  end
end