Vedanshu
Vedanshu

Reputation: 627

How to trasform all values in a nested hash?

I want to convert all the values in a nested hash to a utf8 compatible string. I initially thought this would be easy and something like deep_apply should be available for me to use, but I am unable to find anything this simple on a quick google and SO search.

I do not want to write (maintain) a method similar to the lines of Change values in a nested hash . Is there a native API implementation or a shorthand available for this or do I have to write my own method?

Upvotes: 3

Views: 1102

Answers (4)

max pleaner
max pleaner

Reputation: 26768

Interesting to learn of the deep_merge approach taken in the answer by "The F". Here is another approach which requires adding a few helper methods.

First, the helper methods:

From the top answer here (converting-a-nested-hash-into-a-flat-hash):

def flat_hash(h,f=[],g={})
  return g.update({ f=>h }) unless h.is_a? Hash
  h.each { |k,r| flat_hash(r,f+[k],g) }
  g
end

From a Github repo called ruby-bury (this functionality was proposed to Ruby core, but rejected)

class Hash
  def bury *args
    if args.count < 2
      raise ArgumentError.new("2 or more arguments required")
    elsif args.count == 2
      self[args[0]] = args[1]
    else
      arg = args.shift
      self[arg] = {} unless self[arg]
      self[arg].bury(*args) unless args.empty?
    end
    self
  end
end

And then a method tying it together:

def change_all_values(hash, &blk)    
  # the next line makes the method "pure functional"
  # but can be removed otherwise.
  hash = Marshal.load(Marshal.dump(hash))

  flat_hash(hash).each { |k,v| hash.bury(*(k + [blk.call(v)])) }
  hash
end

A usage example:

irb(main):063:0> a = {a: 1, b: { c: 1 } }
=> {:a=>1, :b=>{:c=>1}}
irb(main):064:0> b = change_all_values(a) { |val| val + 1 }
=> {:a=>2, :b=>{:c=>2}}
irb(main):066:0> a
=> {:a=>1, :b=>{:c=>1}}

Upvotes: 2

The F
The F

Reputation: 3714

There is deep_merge

yourhash.deep_merge(yourhash) {|_,_,v| v.to_s}

Merge the hash with itself, inspect the value and call to_s on it.
This method requires require 'active_support/core_ext/hash' at the top of file if you are not using ruby on rails.

Obviously, you may handle the conversion of v inside the deep_merge as you like to meet your requirements.

In rails console:

2.3.0 :001 > h1 = { a: true, b: { c: [1, 2, 3] } }
 => {:a=>true, :b=>{:c=>[1, 2, 3]}}

2.3.0 :002 > h1.deep_merge(h1) { |_,_,v| v.to_s}
 => {:a=>"true", :b=>{:c=>"[1, 2, 3]"}}

Upvotes: 2

Vedanshu
Vedanshu

Reputation: 627

I ended up implementing my own approach, that is in no way perfect but works well for my use case and should be easy to maintain. Posting it here for reference to anyone who wants to try it out

def deep_apply object, klasses, &blk
    if object.is_a? Array
      object.map { |obj_ele| deep_apply(obj_ele, klasses, &blk) }
    elsif object.is_a? Hash
      object.update(object) {|_, value| deep_apply(value, klasses, &blk) }
    elsif klasses.any? { |klass| object.is_a? klass }
      blk.call(object)
    else
      object
    end
  end

usage:

=> pry(main)> deep_apply({a: [1, 2, "sadsad"]}, [String, Integer]) { |v| v.to_s + "asd" }
=> {:a=>["1asd", "2asd", "sadsadasd"]}

Upvotes: 3

Esse
Esse

Reputation: 3298

Well, it's quite simple to write it - so why don't write your own and be absolutely sure how does it behave in all situations ;)

def to_utf8(h)
    if h.is_a? String
        return h.force_encoding('utf-8')
    elsif h.is_a? Symbol
        return h.to_s.force_encoding('utf-8').to_sym
    elsif h.is_a? Numeric
        return h
    elsif h.is_a? Array
        return h.map { |e| to_utf8(e) }.to_s
    else
        return h.to_s.force_encoding('utf-8')
    end
    return hash.to_a.map { |e| result.push(to_utf8(e[0], e[1])) }.to_h
end

You may want to check if all behavior and conversions are correct - and change it if necessary.

Upvotes: 1

Related Questions