Removing all empty elements from a hash / YAML?

Ruby on-RailsRubyHashYaml

Ruby on-Rails Problem Overview


How would I go about removing all empty elements (empty list items) from a nested Hash or YAML file?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Rails 4.1 added Hash#compact and Hash#compact! as a core extensions to Ruby's Hash class. You can use them like this:

hash = { a: true, b: false, c: nil }
hash.compact                        
# => { a: true, b: false }
hash                                
# => { a: true, b: false, c: nil }
hash.compact!                        
# => { a: true, b: false }
hash                                
# => { a: true, b: false }
{ c: nil }.compact                  
# => {}

Heads up: this implementation is not recursive. As a curiosity, they implemented it using #select instead of #delete_if for performance reasons. See here for the benchmark.

In case you want to backport it to your Rails 3 app:

# config/initializers/rails4_backports.rb

class Hash
  # as implemented in Rails 4
  # File activesupport/lib/active_support/core_ext/hash/compact.rb, line 8
  def compact
    self.select { |_, value| !value.nil? }
  end
end

Solution 2 - Ruby on-Rails

Use hsh.delete_if. In your specific case, something like: hsh.delete_if { |k, v| v.empty? }

Solution 3 - Ruby on-Rails

You could add a compact method to Hash like this

class Hash
  def compact
    delete_if { |k, v| v.nil? }
  end
end

or for a version that supports recursion

class Hash
  def compact(opts={})
    inject({}) do |new_hash, (k,v)|
      if !v.nil?
        new_hash[k] = opts[:recurse] && v.class == Hash ? v.compact(opts) : v
      end
      new_hash
    end
  end
end

Solution 4 - Ruby on-Rails

compact_blank (Rails 6.1+)

If you are using Rails (or a standalone ActiveSupport), starting from version 6.1, there is a compact_blank method which removes blank values from hashes.

It uses Object#blank? under the hood for determining if an item is blank.

{ a: "", b: 1, c: nil, d: [], e: false, f: true }.compact_blank
# => { b: 1, f: true }

Here is a link to the docs and a link to the relative PR.

A destructive variant is also available. See Hash#compact_blank!.


If you need to remove only nil values,

please, consider using Ruby build-in Hash#compact and Hash#compact! methods.

{ a: 1, b: false, c: nil }.compact
# => { a: 1, b: false }

Solution 5 - Ruby on-Rails

If you're using Ruby 2.4+, you can call compact and compact!

h = { a: 1, b: false, c: nil }
h.compact! #=> { a: 1, b: false }

https://ruby-doc.org/core-2.4.0/Hash.html#method-i-compact-21

Solution 6 - Ruby on-Rails

This one would delete empty hashes too:

swoop = Proc.new { |k, v| v.delete_if(&swoop) if v.kind_of?(Hash);  v.empty? }
hsh.delete_if &swoop

Solution 7 - Ruby on-Rails

You can use Hash#reject to remove empty key/value pairs from a ruby Hash.

# Remove empty strings
{ a: 'first', b: '', c: 'third' }.reject { |key,value| value.empty? } 
#=> {:a=>"first", :c=>"third"}

# Remove nil
{a: 'first', b: nil, c: 'third'}.reject { |k,v| v.nil? } 
# => {:a=>"first", :c=>"third"}

# Remove nil & empty strings
{a: '', b: nil, c: 'third'}.reject { |k,v| v.nil? || v.empty? } 
# => {:c=>"third"}

Solution 8 - Ruby on-Rails

works for both hashes and arrays

module Helpers
  module RecursiveCompact
    extend self

    def recursive_compact(hash_or_array)
      p = proc do |*args|
        v = args.last
        v.delete_if(&p) if v.respond_to? :delete_if
        v.nil? || v.respond_to?(:"empty?") && v.empty?
      end

      hash_or_array.delete_if(&p)
    end
  end
end

P.S. based on someones answer, cant find

usage - Helpers::RecursiveCompact.recursive_compact(something)

Solution 9 - Ruby on-Rails

I know this thread is a bit old but I came up with a better solution which supports Multidimensional hashes. It uses delete_if? except its multidimensional and cleans out anything with a an empty value by default and if a block is passed it is passed down through it's children.

# Hash cleaner
class Hash
    def clean!
        self.delete_if do |key, val|
            if block_given?
                yield(key,val)
            else
                # Prepeare the tests
                test1 = val.nil?
                test2 = val === 0
                test3 = val === false
                test4 = val.empty? if val.respond_to?('empty?')
                test5 = val.strip.empty? if val.is_a?(String) && val.respond_to?('empty?')

                # Were any of the tests true
                test1 || test2 || test3 || test4 || test5
            end
        end

        self.each do |key, val|
            if self[key].is_a?(Hash) && self[key].respond_to?('clean!')
                if block_given?
                    self[key] = self[key].clean!(&Proc.new)
                else
                    self[key] = self[key].clean!
                end
            end
        end

        return self
    end
end

Solution 10 - Ruby on-Rails

I made a deep_compact method for this that recursively filters out nil records (and optionally, blank records as well):

class Hash
  # Recursively filters out nil (or blank - e.g. "" if exclude_blank: true is passed as an option) records from a Hash
  def deep_compact(options = {})
    inject({}) do |new_hash, (k,v)|
      result = options[:exclude_blank] ? v.blank? : v.nil?
      if !result
        new_value = v.is_a?(Hash) ? v.deep_compact(options).presence : v
        new_hash[k] = new_value if new_value
      end
      new_hash
    end
  end
end

Solution 11 - Ruby on-Rails

Ruby's Hash#compact, Hash#compact! and Hash#delete_if! do not work on nested nil, empty? and/or blank? values. Note that the latter two methods are destructive, and that all nil, "", false, [] and {} values are counted as blank?.

Hash#compact and Hash#compact! are only available in Rails, or Ruby version 2.4.0 and above.

Here's a non-destructive solution that removes all empty arrays, hashes, strings and nil values, while keeping all false values:

(blank? can be replaced with nil? or empty? as needed.)

def remove_blank_values(hash)
  hash.each_with_object({}) do |(k, v), new_hash|
    unless v.blank? && v != false
      v.is_a?(Hash) ? new_hash[k] = remove_blank_values(v) : new_hash[k] = v
    end
  end
end

A destructive version:

def remove_blank_values!(hash)
  hash.each do |k, v|
    if v.blank? && v != false
      hash.delete(k)
    elsif v.is_a?(Hash)
      hash[k] = remove_blank_values!(v)
    end
  end
end

Or, if you want to add both versions as instance methods on the Hash class:

class Hash
  def remove_blank_values
    self.each_with_object({}) do |(k, v), new_hash|
      unless v.blank? && v != false
        v.is_a?(Hash) ? new_hash[k] = v.remove_blank_values : new_hash[k] = v
      end
    end
  end

  def remove_blank_values!
    self.each_pair do |k, v|
      if v.blank? && v != false
        self.delete(k)
      elsif v.is_a?(Hash)
        v.remove_blank_values!
      end
    end
  end
end

Other options:

  • Replace v.blank? && v != false with v.nil? || v == "" to strictly remove empty strings and nil values
  • Replace v.blank? && v != false with v.nil? to strictly remove nil values
  • Etc.

EDITED 2017/03/15 to keep false values and present other options

Solution 12 - Ruby on-Rails

our version: it also cleans the empty strings and nil values

class Hash

  def compact
    delete_if{|k, v|

      (v.is_a?(Hash) and v.respond_to?('empty?') and v.compact.empty?) or
          (v.nil?)  or
          (v.is_a?(String) and v.empty?)
    }
  end

end

Solution 13 - Ruby on-Rails

In Simple one liner for deleting null values in Hash,

rec_hash.each {|key,value| rec_hash.delete(key) if value.blank? } 

Solution 14 - Ruby on-Rails

Could be done with facets library (a missing features from standard library), like that:

require 'hash/compact'
require 'enumerable/recursively'
hash.recursively { |v| v.compact! }

Works with any Enumerable (including Array, Hash).

Look how recursively method is implemented.

Solution 15 - Ruby on-Rails

The recursive version of https://stackoverflow.com/a/14773555/1519240 works, but not with HashWithIndifferentAccess or other classes that are kind of Hash..

Here is the version I am using:

def recursive_compact
  inject({}) do |new_hash, (k,v)|
    if !v.nil?
      new_hash[k] = v.kind_of?(Hash) ? v.recursive_compact : v
    end
    new_hash
  end
end

kind_of?(Hash) will accept more classes that are like a Hash.

You can also replace inject({}) by inject(HashWithIndifferentAccess.new) if you want to access the new hash using both symbol and string.

Solution 16 - Ruby on-Rails

I believe it would be best to use a self recursive method. That way it goes as deep as is needed. This will delete the key value pair if the value is nil or an empty Hash.

class Hash
  def compact
    delete_if {|k,v| v.is_a?(Hash) ? v.compact.empty? : v.nil? }
  end
end

Then using it will look like this:

x = {:a=>{:b=>2, :c=>3}, :d=>nil, :e=>{:f=>nil}, :g=>{}}
# => {:a=>{:b=>2, :c=>3}, :d=>nil, :e=>{:f=>nil}, :g=>{}} 
x.compact
# => {:a=>{:b=>2, :c=>3}}

To keep empty hashes you can simplify this to.

class Hash
  def compact
    delete_if {|k,v| v.compact if v.is_a?(Hash); v.nil? }
  end
end

Solution 17 - Ruby on-Rails

class Hash   
  def compact
    def _empty?(val)
      case val
      when Hash     then val.compact.empty?
      when Array    then val.all? { |v| _empty?(v) }
      when String   then val.empty?
      when NilClass then true
      # ... custom checking 
      end
    end

    delete_if { |_key, val| _empty?(val) }   
  end 
end

Solution 18 - Ruby on-Rails

Try this to remove nil

hash = { a: true, b: false, c: nil }
=> {:a=>true, :b=>false, :c=>nil}
hash.inject({}){|c, (k, v)| c[k] = v unless v.nil?; c}
=> {:a=>true, :b=>false}

Solution 19 - Ruby on-Rails

Here is something I have:

# recursively remove empty keys (hashes), values (array), hashes and arrays from hash or array
def sanitize data
  case data
  when Array
    data.delete_if { |value| res = sanitize(value); res.blank? }
  when Hash
    data.delete_if { |_, value| res = sanitize(value); res.blank? }
  end
  data.blank? ? nil : data
end

Solution 20 - Ruby on-Rails

Deep deletion nil values from a hash.

  # returns new instance of hash with deleted nil values
  def self.deep_remove_nil_values(hash)
    hash.each_with_object({}) do |(k, v), new_hash|
      new_hash[k] = deep_remove_nil_values(v) if v.is_a?(Hash)
      new_hash[k] = v unless v.nil?
    end
  end
  
  # rewrite current hash
  def self.deep_remove_nil_values!(hash)
    hash.each do |k, v|
      deep_remove_nil_values(v) if v.is_a?(Hash)
      hash.delete(k) if v.nil?
    end
  end

Solution 21 - Ruby on-Rails

in ruby 2.7 there are standard compact and transform_values methods, on the Hash so you can do this like this:

class Hash 
  def deep_compact
    compact.transform_values{|vl| vl.is_a?(Hash) ? vl.deep_compact : vl }
  end
end

It's the neatiest implementation, imho.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBrian JordanView Question on Stackoverflow
Solution 1 - Ruby on-RailsdgilperezView Answer on Stackoverflow
Solution 2 - Ruby on-RailsjpemberthyView Answer on Stackoverflow
Solution 3 - Ruby on-RailsopsbView Answer on Stackoverflow
Solution 4 - Ruby on-RailsMarian13View Answer on Stackoverflow
Solution 5 - Ruby on-RailsWilson SilvaView Answer on Stackoverflow
Solution 6 - Ruby on-RailspunundView Answer on Stackoverflow
Solution 7 - Ruby on-Railssmd1000View Answer on Stackoverflow
Solution 8 - Ruby on-RailssrghmaView Answer on Stackoverflow
Solution 9 - Ruby on-RailsKelly BeckerView Answer on Stackoverflow
Solution 10 - Ruby on-RailsmwalsherView Answer on Stackoverflow
Solution 11 - Ruby on-RailsSebastian JayView Answer on Stackoverflow
Solution 12 - Ruby on-RailssahinView Answer on Stackoverflow
Solution 13 - Ruby on-RailsramyaView Answer on Stackoverflow
Solution 14 - Ruby on-RailsDmitry PolushkinView Answer on Stackoverflow
Solution 15 - Ruby on-Railsuser1519240View Answer on Stackoverflow
Solution 16 - Ruby on-Rails6ft DanView Answer on Stackoverflow
Solution 17 - Ruby on-RailsChixView Answer on Stackoverflow
Solution 18 - Ruby on-RailsRahul PatelView Answer on Stackoverflow
Solution 19 - Ruby on-RailsVarun GargView Answer on Stackoverflow
Solution 20 - Ruby on-RailsMiro MudrikView Answer on Stackoverflow
Solution 21 - Ruby on-RailsАлексей ЛещукView Answer on Stackoverflow