Hash to OpenStruct (#81)


#1

The three rules of Ruby Q.:

  1. Please do not post any solutions or spoiler discussion for this quiz
    until
    48 hours have passed from the time on this message.

  2. Support Ruby Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

  1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby T. follow the discussion. Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Hans F.

More than a few times I’ve wished I could get a nice nested OpenStruct
out of
YAML data, instead of the more unwieldy nested hashes. It’s mostly a
matter of
style. It’s a straightforward task to convert a nested hash structure
into a
nested OpenStruct, but it’s the sort of task that you can do a lot of
ways, and
I’ll bet some of you can come up with more elegant and/or more efficient
ways
than I have so far.

Here’s a sample YAML document to get you started:

---
foo: 1
bar:
  baz: [1, 2, 3]
  quux: 42
  doctors:
    - William Hartnell
    - Patrick Troughton
    - Jon Pertwee
    - Tom Baker
    - Peter Davison
    - Colin Baker
    - Sylvester McCoy
    - Paul McGann
    - Christopher Eccleston
    - David Tennant
  a: {x: 1, y: 2, z: 3}

#2

On Sat, 3 Jun 2006, Ruby Q. wrote:

style. It’s a straightforward task to convert a nested hash structure into a
quux: 42
- David Tennant
a: {x: 1, y: 2, z: 3}

can we make it more realistic? how bout this a sample data

 ---
 foo: 1
 bar:
   baz: [1, 2, 3]
   quux: 42
   doctors:
     - William Hartnell
     - Patrick Troughton
     - Jon Pertwee
     - Tom Baker
     - Peter Davison
     - Colin Baker
     - Sylvester McCoy
     - Paul McGann
     - Christopher Eccleston
     - David Tennant
   a: {x: 1, y: 2, z: 3}
 table: walnut
 method: linseed oil
 type: contemporary
 id: 1234
 send: fedex

??

-a


#3

On 6/2/06, Ruby Q. removed_email_address@domain.invalid wrote:

More than a few times I’ve wished I could get a nice nested OpenStruct out of
YAML data, instead of the more unwieldy nested hashes. It’s mostly a matter of
style. It’s a straightforward task to convert a nested hash structure into a
nested OpenStruct, but it’s the sort of task that you can do a lot of ways, and
I’ll bet some of you can come up with more elegant and/or more efficient ways
than I have so far.

First p0st!

Ok, I cheated… Hans pointed me at the rubyquiz site before this
showed up in the list, so I got a head start. But in only 15 minutes,
I’ve got it solved, with only 18 lines (only 5 of those are in method
bodies), sans whitespace. Golfers… go!

I’d like to thank Hans for a really straightforward (such that I can
do it in my limited time), yet still interesting quiz!

Jacob F.


#4

I was throwing different yaml files at my solution and I came across
this
sample of valid YAML which doesn’t easily fit into an OpenStruct


1: for the money
2: for the show
3: to get ready
4: go go go

Is this a valid testcase?

-Adam


#5
        - Jon Pertwee
        - Tom Baker
        - Peter Davison
        - Colin Baker
        - Sylvester McCoy
        - Paul McGann
        - Christopher Eccleston
        - David Tennant
      a: {x: 1, y: 2, z: 3}

Ahh, a nice, quick one.

I had a 7 line method, that became 6 lines, that became 4 lines…
became 2 really ugly lines if I can include and not count a helper
method to turn mapped Hashes back into Hashes.

My output, reformatted for prettiness (it also handles Ara’s additions):

#<OpenStruct
foo=1,
bar=#<OpenStruct
a=#,
quux=42,
doctors=[
“William Hartnell”,
“Patrick Troughton”,
“Jon Pertwee”,
“Tom Baker”,
“Peter Davison”,
“Colin Baker”,
“Sylvester McCoy”,
“Paul McGann”,
“Christopher Eccleston”,
“David Tennant”
],
baz=[1, 2, 3]

  • Jamie

#6

On Jun 2, 2006, at 2:31 PM, Jacob F. wrote:

efficient ways
do it in my limited time), yet still interesting quiz!

Jacob F.

I thought we were waiting to announce completion. I did it in 17
lines, 9 in the body (whitespace included). Plus a unit test and a
benchmark. Short and sweet! Thanks Hans!
-Mat


#7

On Sat, 3 Jun 2006 03:31:05 +0900, “Jacob F.” removed_email_address@domain.invalid
wrote:

Ok, I cheated… Hans pointed me at the rubyquiz site before this
showed up in the list, so I got a head start. But in only 15 minutes,
I’ve got it solved, with only 18 lines (only 5 of those are in method
bodies), sans whitespace. Golfers… go!

Hold on. Golfers, can your solutions handle … this?


&verily
lemurs:
unite: *verily
beneath:
- patagonian
- bread
- products
thusly: [1, 2, 3, 4]


#8

On Jun 2, 2006, at 3:09 PM, Adam S. wrote:

Is this a valid testcase?
I set up my test case to work with strange keys (numbers, OpenStruct
methods), but I’m not sure what the right behavior is. I see 3
possible behaviors:

  1. accept the data and let the client figure out how to get keys like
    “methods” back out again. (my choice)
  2. Throw an exception when trying to store a key that doesn’t map to
    a legal, free function name
    • then what if they define the same key twice?
  3. Try to remove the functions that are already defined, then redefine
    • can you undefine core ruby functions like ‘methods’?
      -Mat

#9

On Jun 2, 2006, at 3:59 PM, MenTaLguY wrote:

&verily
lemurs:
unite: *verily
beneath:
- patagonian
- bread
- products
thusly: [1, 2, 3, 4]

Wicked test case, MenTaLguY. I had to think that one through a
couple times to get it right. I’m up to 21 lines now (whitespace
included, no comments), but I can handle it. Still not sure how I
can condense it (short of inserting ; anyway).
-Mat


#10

On Jun 2, 2006, at 3:59 PM, MenTaLguY wrote:

&verily
lemurs:
unite: *verily
beneath:
- patagonian
- bread
- products
thusly: [1, 2, 3, 4]

My non-golf solution can’t handle that. You sir, are very very sick
(I mean that in the nicest way possible). Also I didn’t know YAML
could have cycles (thought it had to be a tree). This gives me a
sinking feeling in the pit of my stomach. If YAML is meant to be used
for serialization, then of course it must support cycles… but this
makes me worry about all the projects that use YAML as a config file.


#11

On Sat, 3 Jun 2006 05:49:30 +0900, Logan C.
removed_email_address@domain.invalid wrote:

If YAML is meant to be used for serialization, then of course it must support
cycles… but this makes me worry about all the projects that use YAML as a config file.

For most of the configuration files I’ve seen, I think it’d mostly only
be an issue if you’re doing blind recursive transformations of the tree
(as in this case).

Otherwise, in most cases I’ve seen, there simply isn’t any room for an
arbitrarily deep set of nested hashes in the schema – either you’d get
an error from a hash being in an unexpected place, or that recursive
subtree would simply get ignored.

You might have to think about these sorts of things on rare occasions,
but it’s not the end of the world.

-mental


#12

Ross B. wrote:

That YAML’s a bit of a dark horse, isn’t it?

I’m a sky-puncher, too.

Cheers,
Dave


#13

On Sat, 2006-06-03 at 04:59 +0900, MenTaLguY wrote:

lemurs:
unite: *verily
beneath:
- patagonian
- bread
- products
thusly: [1, 2, 3, 4]

Ouch, that knocked me back for a bit. I think I’ve sussed it now
though :slight_smile: That YAML’s a bit of a dark horse, isn’t it?


#14

My solution was very simple:
require ‘yaml’;require ‘ostruct’;def h(h)h.map{|k,v|h[k]=Hash
===v ?h(v):v};OpenStruct.new(h)end;puts h(YAML.load($<.read))

This can’t deal with the recursion that MentalGuy(sorry for the wrong
capitalisation!) posted.
The following is basically the same code:
class Hash
def to_ostruct
copy = {}
each do |(key,value)|
if value.class == Hash
copy[key] = value.to_ostruct
else
copy[key] = value
end
end
OpenStruct.new(copy)
end
end
I tried to look into changing YAML.load to make OpenStruct’s instead of
hashes, but I soon gave up on that :slight_smile:

j`ey


#15

My solution started off from the most basic hash to openstruct
conversion I could think of: OpenStruct.new(some_hash). Those pesky
nested hashes still needed to be dealt with, so I came up with:

class Hash
def to_ostruct(clz = OpenStruct)
clz.new Hash[*inject([]){|ar,(k,v)|ar<<k<<(v.to_ostruct(clz) rescue
v)}]
end
end

This works, but it’s very inefficient, it doesn’t pass the case
Mentalguy posted, and it doesn’t fail well with invalid keys or other
errors. To handle those things, I had to go a bit longer:

class Hash
def to_ostruct(clz = OpenStruct, cch = {})
cch[self] = (os = clz.new)
each do |k,v|
raise “Invalid key: #{k}” unless k =~ /[a-z_][a-zA-Z0-9_]*/
os.send("#{k}=", v.is_a?(Hash)? cch[v] ||
v.to_ostruct(clz,cch) : v)
end
os
end
end

I chose to fail for invalid keys, rather than introducing potentially
confusing renaming rules or similar. It’s still not as efficient as it
might be, but a bit better than the first one.

Neither solution takes into consideration the problems Ara pointed out -
this is the reason for the optional ‘clz’ parameter to both methods.
Undef’ing methods from OpenStruct turned out to be a non-starter, since
it uses them itself, so I just implemented a simple, naive DumbStruct
that can be used with the to_ostruct methods above:

class DumbStruct
alias :iv_set :instance_variable_set
alias :class :class
instance_methods.each do |m|
undef_method(m) unless m =~ /^(__|method_missing|inspect|to_s)|?$/
end

def initialize(hsh = {})
hsh.each { |k,v| method_missing("#{k}=", v) }
end

def method_missing(name, *args, &blk)
if (name = name.to_s) =~ /[^=]=$/
name = name[0…-2]
iv_set("@#{name}", args.first)
(class << self; self; end).class_eval { attr_accessor name }
else
super
end
end
end

Attached are the full files including testcases and a basic benchmark.
Thanks for another fun and interesting quiz :slight_smile:


#16

Here’s mine:

% cat hash_to_open_struct2.rb
require ‘yaml’
require ‘ostruct’
class Object
def hash_to_ostruct(visited = [])
self
end
end

class Array
def hash_to_ostruct(visited = [])
map { |x| x.hash_to_ostruct(visited) }
end
end

class Hash
def hash_to_ostruct(visited = [])
os = OpenStruct.new
each do |k, v|
item = visited.find { |x| x.first.object_id == v.object_id }
if item
os.send("#{k}=", item.last)
else
os.send("#{k}=", v.hash_to_ostruct(visited + [ [self, os] ]))
end
end
os
end
end

yaml_source = <<YAML

foo: 1
bar:
baz: [1, 2, 3]
quux: 42
doctors:
- William Hartnell
- Patrick Troughton
- Jon Pertwee
- Tom Baker
- Peter Davison
- Colin Baker
- Sylvester McCoy
- Paul McGann
- Christopher Eccleston
- David Tennant
- {w: 1, t: 7}
a: {x: 1, y: 2, z: 3}
YAML
evil_yaml = <<EVIL

&verily
lemurs:
unite: *verily
beneath:
- patagonian
- bread
- products
thusly: [1, 2, 3, 4]
EVIL

loaded = YAML.load(yaml_source).hash_to_ostruct
p loaded.bar.doctors.last.w

evil_loaded = YAML.load(evil_yaml).hash_to_ostruct
p evil_loaded.lemurs.beneath
p evil_loaded.lemurs.unite.thusly

% ruby hash_to_open_struct2.rb
1
[“patagonian”, “bread”, “products”]
[1, 2, 3, 4]


#17

I manged a very small solution – practically one line. Only problem
is, it doesn’t work :wink: But honestly, it’s not my fault! No really. Let
me explain.

When I first read the quiz my thoughts intitally went to the usual
concepts and I considered the Hash#traverse method I wrote some time
ago (BTW this quiz helped me improve that method. Many thanks!) But I
have good bit of experience with YAML and I immediately had a second
thought which would allow me to solve the quiz very quickily and
easily. The solution is as follows (were s containes the yaml sample).

YAML.add_builtin_type(‘map’){ |t,v| OpenStruct.new(v) }; o =
YAML.load(s)

But like I said, as clever as it may be, it doesn’t work. For whatever
reason Syck doesn’t handle it properly. Perhaps YAML’s ‘map’ type is
too fundamental that it can’t comply, or perhaps it’s a bug. I don’t
know. But it just end up returning the same old Hash.

Okay I thought. There’s more than one way to skin a cat. And I came up
with this close to one-liner that works around the above problem in a
most clever way.

i = YAML::load(s)
def Hash.def to_yaml_type
“!ruby/object:OpenStruct”
end
o = YAML::load(i.to_yaml)

The nice thing about this soluiton is that it uses a built-in library
(YAML/Syck) to do all the hard work --since Syck already understands
graphs it takes care of all those messy issues. Cool.

T.


#18

Ross B. wrote:

My solution started off from the most basic hash to openstruct
conversion I could think of: OpenStruct.new(some_hash). Those pesky
nested hashes still needed to be dealt with, so I came up with:

class Hash
def to_ostruct(clz = OpenStruct)
clz.new Hash[*inject([]){|ar,(k,v)|ar<<k<<(v.to_ostruct(clz) rescue v)}]
end
end

class OpenStruct
alias :old_init :initialize
def initialize(hash=nil)
old_init(hash.each{ |k,v| hash[k] = self.class.new(v) if
v.is_a?(Hash) })
end
end

To handle parameters that are the same as existant method names (i.e.
Ara’s sample) requires removal of the ‘unless’ from new_ostruct_member:

def new_ostruct_member(name)
name = name.to_sym
meta = class << self; self; end
meta.send(:define_method, name) { @table[name] }
meta.send(:define_method, “#{name}=”"#{name}=") { |x| @table[name] =
x }
end

Regards,

Dan


#19

Joey wrote:

end
OpenStruct.new(copy)
end
end

This would be cleaner

class Hash
def to_ostruct
copy = dup
copy.each do |key, value|
copy[key] = value.to_ostruct if value.respond_to? :to_ostruct
end
return copy
end
end

Daniel


#20

Gotta post this before I look at other solutions. This caused me to
look up what an OpenStruct was so that was benefit #1. Since it was
a simple one, I worked through it with my son who is going through
Chris P.'s Learning_to_Program right now so that was benefit #2.

I don’t know if this will handle the crazier recursive YAML files,
but it seems to be fine for normal ones. My son actually struck on
the OpenStruct#send being a problem with the presence of the ‘send’
key in the YAML.

-Rob

RubyQuiz81: Hash to OpenStruct

2006-06-02

require ‘ostruct’
require ‘yaml’

class HashToOpenStruct
def self.from_yaml(yamlfile)
self.to_ostruct(YAML.load(File.open(yamlfile)))
end

def self.to_ostruct(h)
c = OpenStruct.new
h.each { |k,v| c.send("#{k}=".to_sym,
v.kind_of?(Hash) ? to_ostruct(v) : v) }
c
end
end

END

Rob B. http://agileconsultingllc.com
removed_email_address@domain.invalid
+1 513-295-4739