Skip to content

Instantly share code, notes, and snippets.

@potatosalad
Created December 31, 2010 04:36
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save potatosalad/760726 to your computer and use it in GitHub Desktop.
Save potatosalad/760726 to your computer and use it in GitHub Desktop.
Ruby quiz for convert hash "dot paths" into actual hash hierarchy.
#require 'rubygems'
require 'pp'
#require 'ap' # Awesome Print
class Object
# expects [ [ symbol, *args ], ... ]
def recursive_send(*args)
args.inject(self) { |obj, m| obj.send(m.shift, *m) }
end
end
class Hash
# auto creates children
# from: http://rubyworks.github.com/facets/doc/api/core/Hash.html
def self.autonew(*args)
leet = lambda { |hsh, key| hsh[key] = new( &leet ) }
new(*args,&leet)
end
def explode(divider = '.')
h = Hash.autonew
for k,v in self.dup
tree = k.split(divider).map { |x| [ :[], x ] }
tree.push([ :[]=, tree.pop[1], v ])
h.recursive_send(*tree)
end
h
end
def implode(divider = '.')
h = Hash.new
self.dup.each_path(divider) do |path, value|
h[path] = value
end
h
end
# each_path method for multi-dimensional Hash
# from: http://snippets.dzone.com/posts/show/3565
def each_path(divider = '.')
raise ArgumentError unless block_given?
self.class.each_path(self, '', divider) { |path, object| yield path, object }
end
protected
def self.each_path(object, path = '', divider = '.', &block)
if object.is_a?(Hash)
object.each do |key, value|
self.each_path value, "#{path}#{path.empty? ? '' : divider}#{key}", divider, &block
end
else
yield path, object
end
end
end
params = {}
params['foo.bar.aaa'] = 'AAA'
params['foo.bar.aab'] = 'AAB'
params['foo.bar.aac'] = 'AAC'
params['foo.bla.aaa'] = '111'
params['foo.bla.aab'] = '112'
params['foo.bla.aac'] = '113'
params['abc.def.foo.bar.aaa'] = '000'
params['abc.def.foo.bar.aab'] = '001'
params['abc.def.foo.bar.aac'] = '002'
puts "== Params =="
#ap params
pp params
puts "\n== Params (exploded) =="
#ap params.explode
pp params.explode
puts "\n== Paths from params (exploded) =="
paths = []
params.explode.each_path { |path, value| paths << [ path, value ] }
#ap paths
pp paths
puts "\n== Example of implode with / as divider =="
#ap params.explode.implode('/')
pp params.explode.implode('/')
puts "\n== Example of implode with reverse explode =="
#ap params.explode.implode('/').explode('/')
pp params.explode.implode('/').explode('/')
== Params ==
{
"foo.bar.aaa" => "AAA",
"foo.bar.aab" => "AAB",
"foo.bar.aac" => "AAC",
"foo.bla.aaa" => "111",
"foo.bla.aab" => "112",
"foo.bla.aac" => "113",
"abc.def.foo.bar.aaa" => "000",
"abc.def.foo.bar.aab" => "001",
"abc.def.foo.bar.aac" => "002"
}
== Params (exploded) ==
{
"foo" => {
"bar" => {
"aaa" => "AAA",
"aab" => "AAB",
"aac" => "AAC"
},
"bla" => {
"aaa" => "111",
"aab" => "112",
"aac" => "113"
}
},
"abc" => {
"def" => {
"foo" => {
"bar" => {
"aaa" => "000",
"aab" => "001",
"aac" => "002"
}
}
}
}
}
== Paths from params (exploded) ==
[
[0] [
[0] "foo.bar.aaa",
[1] "AAA"
],
[1] [
[0] "foo.bar.aab",
[1] "AAB"
],
[2] [
[0] "foo.bar.aac",
[1] "AAC"
],
[3] [
[0] "foo.bla.aaa",
[1] "111"
],
[4] [
[0] "foo.bla.aab",
[1] "112"
],
[5] [
[0] "foo.bla.aac",
[1] "113"
],
[6] [
[0] "abc.def.foo.bar.aaa",
[1] "000"
],
[7] [
[0] "abc.def.foo.bar.aab",
[1] "001"
],
[8] [
[0] "abc.def.foo.bar.aac",
[1] "002"
]
]
== Example of implode with / as divider ==
{
"foo/bar/aaa" => "AAA",
"foo/bar/aab" => "AAB",
"foo/bar/aac" => "AAC",
"foo/bla/aaa" => "111",
"foo/bla/aab" => "112",
"foo/bla/aac" => "113",
"abc/def/foo/bar/aaa" => "000",
"abc/def/foo/bar/aab" => "001",
"abc/def/foo/bar/aac" => "002"
}
== Example of implode with reverse explode ==
{
"foo" => {
"bar" => {
"aaa" => "AAA",
"aab" => "AAB",
"aac" => "AAC"
},
"bla" => {
"aaa" => "111",
"aab" => "112",
"aac" => "113"
}
},
"abc" => {
"def" => {
"foo" => {
"bar" => {
"aaa" => "000",
"aab" => "001",
"aac" => "002"
}
}
}
}
}
@arronmabrey
Copy link

I needed the #explode method so I implemented it as a function without the core extensions

Note: I'm using some ruby 2.x features, like keyword args and Array#to_h but they could be factored out easily.

https://gist.github.com/arronmabrey/185d042e5219be203578

def explode_paths(input, klass: Hash, key_delimiter: '__', key_transform: ->(key) { key.to_sym })
  recursive_hash = klass.new { |hash, key| hash[key] = klass.new(&hash.default_proc) }

  result = input.to_h.reduce(recursive_hash) do |recursive_acc, (input_key_path, input_value)|
    keys = input_key_path.to_s.split(key_delimiter).map(&key_transform)
    tree = keys.map {|key| [:[], key] }
    tree.push([:[]=, tree.pop[1], input_value])
    tree.reduce(recursive_acc) do |acc, tree_node|
      acc.public_send(tree_node.shift, *tree_node)
    end
    recursive_acc
  end

  result.default = klass.new.default

  result
end

# ===== Usage =====
#
# hash_input = {
#   foo__bar__a: :a,
#   foo__bar__b: :b,
#   foo__baz__c: :c,
#   foo__baz__d: :d
# }
#
# explode_paths(hash_input)
#
# # Outputs:
#
# {
#   foo: {
#     bar: {
#       a: :a,
#       b: :b
#     },
#     baz: {
#       c: :c,
#       d: :d
#     }
#   }
# }
#
# # OR
#
# array_input = [
#   [:foo__bar__a, :a],
#   [:foo__bar__b, :b],
#   [:foo__baz__c, :c],
#   [:foo__baz__d, :d],
# ]
#
# explode_paths(array_input)
#
# # Outputs:
#
# {
#   foo: {
#     bar: {
#       a: :a,
#       b: :b
#     },
#     baz: {
#       c: :c,
#       d: :d
#     }
#   }
# }
#

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment