I'm trying to write a Ruby DSL module that can be used to construct a hash with nested keys, with syntax like:
Tree.new do
node :building do
string :name, 'Museum'
node :address do
string :street, 'Wallaby Way'
string :country, 'USA'
end
end
end.to_h
The intended output of that invocation would be this hash:
{
"building": {
"name": "Museum",
"address": {
"street": "Wallaby Way",
"country": "USA"
}
}
}
I may need to support several levels of depth for the nodes, but can't quite work out how to model that.
I've got this so far, initializing the tree class with an empty hash, and evaluating the block passed to it:
class Tree
def initialize(&block)
#tree = {}
instance_eval &block
end
def to_h
#tree
end
def node(name, &block)
#tree[name] = block.call
end
def string(name, value)
#tree[name] = value
end
end
But it gives me this flat structure:
=> {:name=>"Museum", :street=>"Wallaby Way", :country=>"USA", :address=>"USA", :building=>"USA"}
I think I'm sort of on the right lines of trying to call the block within node, but can't quite work out how to have each node apply its contents to its part of the hash, rather than #tree[name], which is obviously that key on the top level hash?
You need to initialize a new tree when you call node.
class Tree
def initialize(&block)
#tree = {}
instance_eval &block
end
def to_h
#tree
end
def node(name, &block)
#tree[name] = (Tree.new &block).to_h
end
def string(name, value)
#tree[name] = value
end
end
Related
Imagine you have an array of objects called parents and each entry is an instance of a Parent. And imagine each parent has a method called children which returns an array of Child instances. Now imagine you want to flat_map from the parents to the children but in a later step you will still need access to the parent (for, say, filtering or whatever). How would you do it?
parents
.flat_map { |parent| parent.children.map { |child| {parent: parent, child: child} } }
Would give me what I need, but... eww. Surely there's a more ruby-esque way of doing this? It almost seems like an RX merge or combineLatest. Can't figure out a ruby way of doing this other than what I already have.
Here's a bit of ruby to generate the parent/child structure I'm talking about using random data:
class Child ; end
class Parent
attr_reader :children
def initialize(children)
#children = children || []
end
end
parents = 100.times.map do
Parent.new(rand(10).times.map { Child.new })
end
Consider adopting the following structure.
class Child
attr_accessor :name # create accessor for name of child
def initialize(name)
#name = name
end
end
class Parent
#families = {} # create class instance variable
class << self # change self to singleton class
attr_reader :families # create accessor for #families
end
def initialize(name, children)
self.class.families[name] = children.map { |name| Child.new(name) }
end
end
Parent.new("Bob and Diane", %w| Hector Lois Rudolph |)
#=> #<Parent:0x00005ac0ddad9aa0>
Parent.new("Hank and Trixie", %w| Phoebe |)
#=> #<Parent:0x00005ac0ddb252c0>
Parent.new("Thelma and Louise", %w| Zaphod Sue |)
#=> #<Parent:0x00005ac0ddcb2890>
Parent.families
#=> {"Bob and Diane" =>[#<Child:0x00005ac0ddadf9f0 #name="Hector">,
# #<Child:0x00005ac0ddadf388 #name="Lois">,
# #<Child:0x00005ac0ddadf1a8 #name="Rudolph">],
# "Hank and Trixie" =>[#<Child:0x00005ac0ddb251d0 #name="Phoebe">],
# "Thelma and Louise"=>[#<Child:0x00005ac0ddcb27c8 #name="Zaphod">,
# #<Child:0x00005ac0ddcb27a0 #name="Sue">]}
Parent.families.keys
#=> ["Bob and Diane", "Hank and Trixie", "Thelma and Louise"]
Parent.families["Bob and Diane"].map { |child| child.name }
#=> ["Hector", "Lois", "Rudolph"]
I'm attempting to adapt the method-chaining example cited in this posting (Method chaining and lazy evaluation in Ruby) to work with an object that implements the Enumerable class (Implement a custom Enumerable collection class in Ruby )
Coffee class:
class Coffee
attr_accessor :name
attr_accessor :strength
def initialize(name, strength)
#name = name
#strength = strength
end
def <=>(other_coffee)
self.strength <=> other_coffee.strength
end
def to_s
"<name: #{name}, strength: #{strength}>"
end
end
Criteria class:
class Criteria
def initialize(klass)
#klass = klass
end
def criteria
#criteria ||= {:conditions => {}}
end
# only show coffee w/ this strength
def strength(strength)
criteria[:strength] = strength
self
end
# if there are multiple coffees, choose the first n=limit
def limit(limit)
criteria[:limit] = limit
self
end
# allow collection enumeration
def each(&block)
#klass.collection.select { |c| c[:strength] == criteria[:strength] }.each(&block)
end
end
CoffeeShop class:
class CoffeeShop
include Enumerable
def self.collection
#collection=[]
#collection << Coffee.new("Laos", 10)
#collection << Coffee.new("Angkor", 7)
#collection << Coffee.new("Nescafe", 1)
end
def self.limit(*args)
Criteria.new(self).limit(*args)
end
def self.strength(*args)
Criteria.new(self).strength(*args)
end
end
When I run this code:
CoffeeShop.strength(10).each { |c| puts c.inspect }
I get an error:
criteria.rb:32:in block in each': undefined method '[]' for #<Coffee:0x007fd25c8ec520 #name="Laos", #strength=10>
I'm certain that I haven't defined the Criteria.each method correctly, but I'm not sure how to correct it. How do I correct this?
Moreover, the each method doesn't support the limit as currently written. Is there a better way to filter the array such that it is easier to support both the strength and limit?
Other coding suggestions are appreciated.
Your Coffee class defines method accessors for name and strength. For a single coffee object, you can thus get the attributes with
coffee.name
# => "Laos"
coffee.strength
# => 10
In your Criteria#each method, you try to access the attributes using the subscript operator, i.e. c[:strength] (with c being an Instance of Coffee in this case). Now, on your Coffee class, you have not implemented the subscript accessor which resulting in the NoMethodError you see there.
You could thus either adapt your Criteria#each method as follows:
def each(&block)
#klass.collection.select { |c| c.strength == criteria[:strength] }.each(&block)
end
or you could implement the subscript operators on your Coffee class:
class Coffee
attr_accessor :name
attr_accessor :strength
# ...
def [](key)
public_send(key)
end
def []=(key, value)
public_send(:"#{key}=", value)
end
end
Noe, as an addendum, you might want to extend your each method in any case. A common (and often implicitly expected) pattern is that methods like each return an Enumerator if no block was given. This allows patterns like CoffeeShop.strength(10).each.group_by(&:strength).
You can implement this b a simple on-liner in your method:
def each(&block)
return enum_for(__method__) unless block_given?
#klass.collection.select { |c| c.strength == criteria[:strength] }.each(&block)
end
Considering the following code:
class Node
def initialize(name=nil)
#name = name
#children = []
end
def node(name, &block)
child = Node.new(name)
#children.push(child)
child.instance_exec(&block) if block
end
end
def tree(name, &block)
#tree = Node.new(name)
#tree.instance_exec(&block)
#tree
end
t = tree("Simpsons family tree") do
node("gramps") do
node("homer+marge") do
node("bart")
node("lisa")
node("maggie")
end
end
end
puts "tree = " + t.inspect
Which is returning:
tree = #<Node:0x007fca1a103268 #name="Simpsons family tree", #children=[#<Node:0x007fca1a103128 #name="gramps", #children=[#<Node:0x007fca1a102fe8 #name="homer+marge", #children=[#<Node:0x007fca1a102ef8 #name="bart", #children=[]>, #<Node:0x007fca1a102e80 #name="lisa", #children=[]>, #<Node:0x007fca1a102e08 #name="maggie", #children=[]>]>]>]>
I would like to know if it was possible to make an update in order to return on-the-fly an embedded array of arrays, without using the #children shared array. I would expect this result:
[
"Simpsons family tree",
[
"gramps",
[
"homer+marge",
[
"bart",
"lisa",
"maggie"
]
]
]
]
Is it possible? Thanks for any suggestions.
Edit:
Actually, I would like to do so with basically the same code, but without any #children instance. So I want to remove #children.
Edit 2:
Here is my best result, after some tests:
class Node
def initialize(name=nil)
#name = name
end
def node(name, &block)
child = Node.new(name)
#sub = child.instance_exec(&block) if block
[
name,
#sub
].compact
end
end
def tree(name, &block)
#tree = Node.new(name)
[
name,
#tree.instance_exec(&block)
]
end
t = tree("Simpsons family tree") do
node("gramps") do
node("homer+marge") do
node("bart")
node("lisa")
node("maggie")
end
end
end
puts t.inspect
# => [
# "Simpsons family tree",
# [
# "gramps",
# [
# "homer+marge",
# [
# "maggie"
# ]
# ]
# ]
# ]
But there's still a trouble with the flatten nodes. Because only the last one is returned by Ruby.
The formatting of mine isn't exactly what you want, but that's not really the important part.
If you allow your nodes to be initialized with a collection of children, you can have your node method return a new node each time it is called.
class Node
def self.new(*args, &block)
instance = super
if block
instance.instance_eval(&block)
else
instance
end
end
def initialize(name, children=[])
#name = name
#children = children
end
attr_reader :children, :name
def node(name, &block)
new_node = Node.new(name)
new_node = new_node.instance_eval(&block) if block
Node.new(self.name, next_children + [new_node])
end
def next_children
children.map{|child| Node.new(child.name, child.next_children) }
end
def inspect
return %{"#{name}"} if children.empty?
%{"#{name}", #{children}}
end
end
t = Node.new("Simpsons family tree") do
node("gramps") do
node("homer+marge") do
node("bart").
node("lisa").
node("maggie")
end
end
end
puts t.inspect
#=> "Simpsons family tree", ["gramps", ["homer+marge", ["bart", "lisa", "maggie"]]]
By changing the behavior of initialization and of the node method you can accumulate the nodes as they are created.
I also took the liberty to remove your tree method since it was just a wrapper for initializing the nodes, but that might be a place to bring back formatting.
I came across this post looking for examples of good use of instance_exec for my Ruby DSL Handbook but you can either use instance_exec or instance_eval for the block since you're not passing any arguments to it.
EDIT: I updated the approach to return new values each time. The change required that nodes without blocks be chained together because each node call return a new object. The return value for the block is a new node.
In order to get the formatting you want, you'd need to do [t].inspect
How do I construct an array of different types given a comma-separated string and another array dictating the type?
By parsing CSV input taken from stdin, I have an array of column header Symbols:
cols = [:IndexSymbol, :PriceStatus, :UpdateExchange, :Last]
and a line of raw input:
raw = "$JX.T.CA,Open,T,933.36T 11:10:00.000"
I would like to construct an an array, cells from the raw input, where each element of cells is a type identified by the corresponding element in cols. What are the idiomatic Ruby-sh ways of doing this?
I have tried this, which works but doesn't really feel right.
1) First, define a class for each type which needs to be encapsulated:
class Sku
attr_accessor :mRoot, :mExch,, :mCountry
def initialize(root, exch, country)
#mRoot = root
#mExch = exch
#mCountry = country
end
end
class Price
attr_accessor :mPrice, :mExchange, :mTime
def initialize(price, exchange, time)
#mPrice = price
#mExchange = exchange
#mTime = time
end
end
2) Then, define conversion functions for each unique column type which needs to be converted:
def to_sku(raw)
raw.match('(\w+)\.(\w{0,1})\.(\w{,2})') { |m| Sku.new(m[1], m[2], m[3])}
end
def to_price(raw)
end
3) Create an array of strings from the input:
cells = raw.split(",")
4) And finally modify each element of cells in-place by constructing the type dictated by the corresponding column header:
cells.each_index do |i|
cells[i] = case cols[i]
when :IndexSymbol
to_sku(cells[i])
when :PriceStatus
cells[i].split(";").collect {|st| st.to_sym}
when :UpdateExchange
cells[i]
when :Last
cells[i].match('(\d*\.*\d*)(\w?) (\d{1,2}:\d{2}:\d{2}\.\d{3})') { |m| Price.new(m[1], m[2], m[3])}
else
puts "Unhandled column type (#{cols[i]}) from input string: \n#{cols}\n#{raw}"
exit -1
end
end
The parts that don't feel right are steps 3 and 4. How is this done in a more Ruby fashion? I was imagining some kind of super concise method like this, which exists only in my imagination:
cells = raw.split_using_convertor(",")
You can make the fourth step simpler with #zip, #map, and destructuring assignment:
cells = cells.zip(cols).map do |cell, col|
case col
when :IndexSymbol
to_sku(cell)
when :PriceStatus
cell.split(";").collect {|st| st.to_sym}
when :UpdateExchange
cell
when :Last
cell.match('(\d*\.*\d*)(\w?) (\d{1,2}:\d{2}:\d{2}\.\d{3})') { |m| Price.new(m[1], m[2], m[3])}
else
puts "Unhandled column type (#{col}) from input string: \n#{cols}\n#{raw}"
exit -1
end
end
I wouldn’t recommend combining that step with the splitting, because parsing a line of CSV is complicated enough to be its own step. See my comment for how to parse the CSV.
You could have the different types inherit from a base class and put the lookup knowledge in that base class. Then you could have each class know how to initialize itself from a raw string:
class Header
##lookup = {}
def self.symbol(*syms)
syms.each{|sym| ##lookup[sym] = self}
end
def self.lookup(sym)
##lookup[sym]
end
end
class Sku < Header
symbol :IndexSymbol
attr_accessor :mRoot, :mExch, :mCountry
def initialize(root, exch, country)
#mRoot = root
#mExch = exch
#mCountry = country
end
def to_s
"##{mRoot}-#{mExch}-#{mCountry}"
end
def self.from_raw(str)
str.match('(\w+)\.(\w{0,1})\.(\w{,2})') { |m| new(m[1], m[2], m[3])}
end
end
class Price < Header
symbol :Last, :Bid
attr_accessor :mPrice, :mExchange, :mTime
def initialize(price, exchange, time)
#mPrice = price
#mExchange = exchange
#mTime = Time.new(time)
end
def to_s
"$#{mPrice}-#{mExchange}-#{mTime}"
end
def self.from_raw(raw)
raw.match('(\d*\.*\d*)(\w?) (\d{1,2}:\d{2}:\d{2}\.\d{3})') { |m| new(m[1], m[2], m[3])}
end
end
class SymbolList
symbol :PriceStatus
attr_accessor :mSymbols
def initialize(symbols)
#mSymbols = symbols
end
def self.from_raw(str)
new(str.split(";").map(&:to_sym))
end
def to_s
mSymbols.to_s
end
end
class ExchangeIdentifier
symbol :UpdateExchange
attr_accessor :mExch
def initialize(exch)
#mExch = exch
end
def self.from_raw(raw)
new(raw)
end
def to_s
mExch
end
end
Then you can replace step #4 like so (CSV parsing not included):
cells.each_index.map do |i|
Header.lookup(cols[i]).from_raw(cells[i])
end
Ruby’s CSV library includes support for this sort of thing directly (as well as better handling of the actual parsing), although the docs are a bit awkward.
You need to provide a proc that will do your conversions for you, and pass it as an option to CSV.parse:
converter = proc do |field, info|
case info.header.strip # in case you have spaces after your commas
when "IndexSymbol"
field.match('(\w+)\.(\w{0,1})\.(\w{,2})') { |m| Sku.new(m[1], m[2], m[3])}
when "PriceStatus"
field.split(";").collect {|st| st.to_sym}
when "UpdateExchange"
field
when "Last"
field.match('(\d*\.*\d*)(\w?) (\d{1,2}:\d{2}:\d{2}\.\d{3})') { |m| Price.new(m[1], m[2], m[3])}
end
end
Then you can parse it almost directly into the format you want:
c = CSV.parse(s, :headers => true, :converters => converter).by_row!.map do |row|
row.map { |_, field| f } #we only want the field now, not the header
end
#AbeVoelker's answer steered me in the right direction, but I had to make a pretty major change because of something I failed to mention in the OP.
Some of the cells will be of the same type, but will still have different semantics. Those semantic differences don't come in to play here (and aren't elaborated on), but they do in the larger context of the tool I'm writing.
For example, there will be several cells that are of type Price; some of them are :Last, ':Bid, and :Ask. They are all the same type (Price), but they are still different enough so that there can't be a single Header##lookup entry for all Price columns.
So what I actually did was write a self-decoding class (credit to Abe for this key part) for each type of cell:
class Sku
attr_accessor :mRoot, :mExch, :mCountry
def initialize(root, exch, country)
#mRoot = root
#mExch = exch
#mCountry = country
end
def to_s
"##{mRoot}-#{mExch}-#{mCountry}"
end
def self.from_raw(str)
str.match('(\w+)\.(\w{0,1})\.(\w{,2})') { |m| new(m[1], m[2], m[3])}
end
end
class Price
attr_accessor :mPrice, :mExchange, :mTime
def initialize(price, exchange, time)
#mPrice = price
#mExchange = exchange
#mTime = Time.new(time)
end
def to_s
"$#{mPrice}-#{mExchange}-#{mTime}"
end
def self.from_raw(raw)
raw.match('(\d*\.*\d*)(\w?) (\d{1,2}:\d{2}:\d{2}\.\d{3})') { |m| new(m[1], m[2], m[3])}
end
end
class SymbolList
attr_accessor :mSymbols
def initialize(symbols)
#mSymbols = symbols
end
def self.from_raw(str)
new(str.split(";").collect {|s| s.to_sym})
end
def to_s
mSymbols.to_s
end
end
class ExchangeIdentifier
attr_accessor :mExch
def initialize(exch)
#mExch = exch
end
def self.from_raw(raw)
new(raw)
end
def to_s
mExch
end
end
...Create a typelist, mapping each column identifier to the type:
ColumnTypes =
{
:IndexSymbol => Sku,
:PriceStatus => SymbolList,
:UpdateExchange => ExchangeIdentifier,
:Last => Price,
:Bid => Price
}
...and finally construct my Array of cells by calling the appropriate type's from_raw:
cells = raw.split(",").each_with_index.collect { |cell,i|
puts "Cell: #{cell}, ColType: #{ColumnTypes[cols[i]]}"
ColumnTypes[cols[i]].from_raw(cell)
}
The result is code that is clean and expressive in my eyes, and seems more Ruby-ish that what I had originally done.
Complete example here.
I'm working on a class that reads some sensor information and returns it as a hash. I would like to use the hash keys as accessors, but I'm not having much luck getting it work. Here are the relevant parts of my code so far:
I've tried it both with method_missing and by using the :define_method method.
attr_reader :sensor_hash
def method_missing(name, *args, &blk)
if args.empty? && blk.nil? && #sensor_hash.has_key?(name.to_s)
#sensor_hash[name.to_s]
else
super
end
end
def sensor(*sensor_to_return)
sensor_output = run_command(this_method_name)
sensor_output = sensor_output.split("\n")
sensor_output.map! { |line| line.downcase! }
unless sensor_to_return.empty?
sensor_to_return = sensor_to_return.to_s.downcase
sensor_output = sensor_output.grep(/^#{sensor_to_return}\s/)
end
#sensor_hash = Hash.new
sensor_output.each { |stat| #sensor_hash[stat.split(/\s+\|\s?/)[0].gsub(' ','_').to_sym] = stat.split(/\s?\|\s?/)[1..-1].each { |v| v.strip! } }
#sensor_hash.each do |k,v|
puts v.join("\t")
self.class.send :define_method, k { v.join("\t") }
end
return #sensor_hash
The data returned is a hash with the sensor name as the key and and the value is an array of everything else returned. My goal is to be able to call Class.sensor.sensor_name and get the output of Class.sensor[:sensor_name]. Currently, all I'm able to get is an undefined method error. Anybody have any idea what I'm doing wrong here?
Maybe OpenStruct does what you want. From the doc :"It is like a hash with a different way to access the data. In fact, it is implemented with a hash, and you can initialize it with one."
require 'ostruct'
s=OpenStruct.new({:sensor_name=>'sensor1',:data=>['something',1,[1,2,3]]})
p s.sensor_name
#=> "sensor1"
Just a quick example. Do you have any reasons to not monkey-patch your Hash?
irb(main):001:0> class Hash
irb(main):002:1> def method_missing(name, *args, &blk)
irb(main):003:2> if self.keys.map(&:to_sym).include? name.to_sym
irb(main):004:3> return self[name.to_sym]
irb(main):005:3> else
irb(main):006:3* super
irb(main):007:3> end
irb(main):008:2> end
irb(main):009:1> end
=> nil
irb(main):012:0> h = {:hello => 'world'}
=> {:hello=>"world"}
irb(main):013:0> h.hello
=> "world"
You could use a wrapper class with method missing so you don't have to monkey patch Hash.
class AccessibleHash
def initialize(hash)
#hash = hash
end
def method_missing(name, *args, &block)
sname = name.to_sym
if #hash.keys.include? sname
return #hash[sname]
else
super
end
end
end
Or, if you are working with Rails it has some nice built in object delegation with SimpleDelegator. That would allow you to define accessors on your hash as well as any nested hashes within it.
class AccessibleHash < SimpleDelegator
def initialize
define_accessors(self.keys)
end
def define_accessors(keys)
keys.each do |key|
defind_accessors(body[key].keys)
self.define_singleton_method(key) { self[key] }
end
end
end
ah = AccessibleHash.new({ some: 'hash', with: { recursive: 'accessors' })
ah.with.recursive == 'accessors'
=> true
This would be less performant at instantiation than method_missing, because it has to run recursively over your delegatee object as soon as it's created. However, it's definitely safer than method_missing, and certainly way safer than monkey patching your Hash class. Of course, safety is relative to your goals, If it's all your application does then monkey patch away.
And if you want the recursive, nested accessors without rails you could do something like this with a combination of the above...
class AccessibleHash
def initialize(hash)
#hash = hash
define_accessors(#hash.keys)
end
def define_accessors(keys)
keys.each do |key|
#hash[key] = self.class.new(#hash[key]) if #hash.keys.present?
self.define_singleton_method(key) { self[key] }
end
end
end
But at that point you're getting pretty crazy and it's probably worth reevaluating your solution in favor of something more Object Oriented. If I saw any of these in a code review it would definitely throw up a red flag. ;)