Unexpected hash's behavior - ruby

I'm newbie.
Help me, please.
I write Puppet function
Piece of code :
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth={}
puts kk,vv,weth
weth = arguments[0]
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if['eth'+ kk.to_s]=weth
end
Data readed from 2 files, and passed into arguments[0] and arguments[1] respectively:
# template of ethernet interfaces
eth_:
method: "static"
family: "inet"
ip: ""
netmask: "255.255.0.0"
onboot: true
options:
MTU: ""
in_vlan: ""
# values for include into ethernet interfaces
eth_values:
0:
mtu: 1500
in_vlan: 15
1:
mtu: 9000
in_vlan: 125
I expect get hash with keys 'eth0' and 'eth1' as follow:
eth1methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000eth0methodstaticfamilyinetin_vlan15ipnetmask255.255.0.0onboottrueoptionsMTU1500
But I get :
eth1methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000eth0methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000
What is my mistake?

First, some comments:
Your code is not indented in a way that most others do it, which makes it hard for others to help you. It should look something like this:
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth={}
puts kk,vv,weth
weth = arguments[0]
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if['eth'+ kk.to_s]=weth
end
Perhaps your variable names make sense to you, but they don't make sense to me. What is n_if, weth, over_if, kk and vv?
You assign weth to be a hash inside your each, and then you assign it to be something else. What are you really trying to do?
You say that arguments[0] and arguments[1] are data read in from files. How are these read in? Are these YAML files? It would be helpful if you would include code to actually reproduce your problem. Pare it down to the essentials.
In Ruby it is generally more idiomatic and performant not to concatenate strings, but to use string interpolation:
n_if["eth#{kk}"] = weth
Now, some answers:
My guess is that your setup holds data like this:
arguments = {
"eth_"=>{
"method"=>"static",
"family"=>"inet",
"ip"=>"",
"netmask"=>"255.255.0.0",
"onboot"=>true,
"options"=>{"MTU"=>""},
"in_vlan"=>""
},
"eth_values"=>{
0=>{"mtu"=>1500, "in_vlan"=>15},
1=>{"mtu"=>9000, "in_vlan"=>125}
}
}
arguments[0] = arguments['eth_']
arguments[1] = arguments['eth_values']
I believe (based on many guesses as to what you have and what you may want) that your problem is this combination:
weth={}
weth=arguments[0]
I think your intent here is to say "weth is a hash type of object; now fill it with values from arguments[0]". What those lines actually say is:
Set weth to an empty hash.
Nevermind, throw away that empty hash and set weth to the same object as arguments[0].
Consequently, each time through the loop you are modifying the same hash with weth. Instead, I think you want to duplicate the hash for weth. Does the following modified code give you what you need?
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth = arguments[0].dup
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if["eth#{kk}"]=weth
end
require 'pp' # for nice wrapping inspection
pp n_if
#=> {"eth0"=>
#=> {"method"=>"static",
#=> "family"=>"inet",
#=> "ip"=>"",
#=> "netmask"=>"255.255.0.0",
#=> "onboot"=>true,
#=> "options"=>{"MTU"=>9000},
#=> "in_vlan"=>15},
#=> "eth1"=>
#=> {"method"=>"static",
#=> "family"=>"inet",
#=> "ip"=>"",
#=> "netmask"=>"255.255.0.0",
#=> "onboot"=>true,
#=> "options"=>{"MTU"=>9000},
#=> "in_vlan"=>125}}
If not, please edit your question with more details on what you ACTUALLY have (hint: p arguments and show us the result) and what you really want as the result.
Edit: For fun, here's a functional transformation instead. It is left as an exercise to the reader to understand how it works and level-up their functional programming skills. Note that I have modified eth_values to match the hierarchy of the template so that simple merging can be applied. I've left the "MTU"=>"" and "in_vlan"=>"" entries in, but note that they are not necessary for the code to work, you could delete both (and the resulting "options"=>{}) and achieve the same result.
args = {
"eth_"=>{
"method"=>"static",
"family"=>"inet",
"ip"=>"",
"netmask"=>"255.255.0.0",
"onboot"=>true,
"options"=>{"MTU"=>""},
"in_vlan"=>""
},
"eth_values"=>{
0=>{"options"=>{"MTU"=>1500}, "in_vlan"=>15},
1=>{"options"=>{"MTU"=>9000}, "in_vlan"=>125}
}
}
n_if = Hash[
args['eth_values'].map do |num,values|
[ "eth#{num}",
args['eth_'].merge(values) do |k,v1,v2|
if v1.is_a?(Hash) and v2.is_a?(Hash) then
v1.merge(v2)
else
v2
end
end ]
end
]
pp n_if #=> Same result as in the previous code.

Related

Ruby - Handling JSON hash when key may not exist

In Ruby, I'd like to know how best to handle getting a variable from a json hash and then proceeding only if it exists, else raise an error.
So here is some example code:
digest = cover_id(map)
def cover_id(a_map)
cover_from_map = a_map['metadata']['cover'][0]['#']
a_map['resources'].find {|resource| resource['id']==cover_from_map}['-major-md5-digest']
end
Looking at this there are 2 things I'm not clear how best to handle:
If firstly song_map['metadata']['cover'][0]['#'] did not exist - is it just a case of using song_map.key?(['metadata']['cover'][0]['#']). I feel using this means I've duplicated code?
Instead of hardcoding that 0 is there any way I can just say get the first?
Basically, from what I know now, I was thinking:
digest = cover_id(map) rescue nil
def cover_id(a_map)
unless a_map['metadata']['cover'][0]['#'] return nil
cover_from_map = a_map['metadata']['cover'][0]['#']
a_map['resources'].find {|resource| resource['id']==cover_from_map}['-major-md5-digest']
end
But I dont think that would handle if a_map['metadata']['cover'][0]['#'] didn't actually exist.
Any help appreciated.
Check method dig of Hash:
https://ruby-doc.org/core-2.3.0_preview1/Hash.html#method-i-dig
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil

How to exclude rows starting with A, B, or C in CSV with Ruby

I have written the following code:
dataexc = data.select do |element|
element[:cz_name] || element[:tor_other_cz_name]
false if [0] == “A” || [0] == “B” || [0] == “C”
end
end
This returns NameError: undefined local variable or method `“A”' for main:Object
I am trying to exclude all locations that start with A, B, or C that exist in either of two columns in my spreadsheet. I have already put the CSV file into an array in "data", and now I'm using "dataexc" to try to narrow down the criteria to exclude the rows with those locations. Can anybody please help me write this so it works?
I am also trying to figure out how to solve an encoding error (?) that is causing one of my CSVs to open with the wrong amount of values in irb. If you think you may be able to help me (any and all help is appreciated!), there is more information about what I am doing here.
Update:
In case anyone else was trying to find a solution a beginner could actually understand, the current working state of the -ABC code is:
cleaned_data = data.reject do |e|
letters = ["A", "B", "C"]
if e[:cz_name]
letters.include?(e[:cz_name][0])
end
end
cleaned_data = cleaned_data.reject do |e|
letters = ["A", "B", "C"]
if e[:tor_other_cz_name]
letters.include?(e[:tor_other_cz_name][0])
end
end
Please do not post here if you are going to be condescending.
That said, if anyone else wants to help with this project, I would really appreciate it and I will credit you for it on Github.
I'd write it thusly:
data.reject { |row|
row.values_at(:cz_name, :tor_other_cz_name).any? { |val|
val =~ /^[ABC]/
}
}
"reject the rows where any of the values in columns :cz_name and :tor_other_cz_name matches the pattern 'starts with A, B or C'."
As to your code, many errors there.
def defines a method, not a variable. If you were trying to define a method, don't use =.
for is hardly ever used in Ruby, and you can't use it with this syntax
element[:cz_name] || element[:tor_other_cz_name] will just be element[:cz_name] if it's present, or the other one if it is not. It will not check both.
[0] is not the first character of a string, it is an array with a zero inside. element[:cz_name][0] would be the first character of that string.
if condition; bool = true; else; bool = false; end is equivalent to bool = condition
“A” is not the same as "A". Don't write your code in Word or any other word processor, use a text editor: smart quotes are a death in programming.
if your last value is false if condition, in case of condition being true the return value will be nil. Both nil and false are falsey, so you end up not discriminating anything.
I suggest relearning Ruby grammar. Find a book or a tutorial somewhere, and do all exercises, in order. You have way too many basic errors.

Convert "object.property" to "object[property]" in Ruby

Say I've got strings like "object.property.property" and I'd like to be able to use those strings to dynamically create form fields for those properties; I'd need to convert them to "object[property][property]".
I was trying something along the lines of: "object.property.property".split('.')
Which of course leaves me with an array: ["object", "property", "property"]
But I'm unsure how to rejoin them so the properties are surrounded by brackets ("object[property][property]"). I tried using join, but as far as I can tell that only lets you specify the seperator between each element in the array.
Any thoughts appreciated.
This is an option
result, *sub_str = "object.property.property".split('.')
result += sub_str.map{|property| "[#{property}]"}.join
#=> "object[property][property]"
"object.property.property".gsub('.', '][').sub('][', '[') << ']'
or, less productive, but more readable:
arr = "object.property.property".split('.')
[arr.shift, *arr.map { |e| "[#{e}]" }].join
Ah, the beauty of ruby:
obj, *props = "object.property.property".split('.')
obj #=> "object"
props #=> ["property", "property"]
"#{obj}[#{props.join('][')}]"
#=> "object[property][property]"
Update: to also cover objects without properties:
def form_name(property)
obj, *props = property.split('.')
"#{obj}[#{props.join('][')}]"
props.any? ? "#{obj}[#{props.join('][')}]" : obj
end
form_name "object.property.property"
#=> "object[property][property]"
form_name "object"
#=> "object"
I would suggest this:
"object.property.property".gsub(/\.([\w\d_]+[^\.])/, "[#{$1}]")
Though this is only readable to someone who understands Regular Expressions well.

Functionally find mapping of first value that passes a test

In Ruby, I have an array of simple values (possible encodings):
encodings = %w[ utf-8 iso-8859-1 macroman ]
I want to keep reading a file from disk until the results are valid. I could do this:
good = encodings.find{ |enc| IO.read(file, "r:#{enc}").valid_encoding? }
contents = IO.read(file, "r:#{good}")
...but of course this is dumb, since it reads the file twice for the good encoding. I could program it in gross procedural style like so:
contents = nil
encodings.each do |enc|
if (s=IO.read(file, "r:#{enc}")).valid_encoding?
contents = s
break
end
end
But I want a functional solution. I could do it functionally like so:
contents = encodings.map{|e| IO.read(f, "r:#{e}")}.find{|s| s.valid_encoding? }
…but of course that keeps reading files for every encoding, even if the first was valid.
Is there a simple pattern that is functional, but does not keep reading the file after a the first success is found?
If you sprinkle a lazy in there, map will only consume those elements of the array that are used by find - i.e. once find stops, map stops as well. So this will do what you want:
possible_reads = encodings.lazy.map {|e| IO.read(f, "r:#{e}")}
contents = possible_reads.find {|s| s.valid_encoding? }
Hopping on sepp2k's answer: If you can't use 2.0, lazy enums can be easily implemented in 1.9:
class Enumerator
def lazy_find
self.class.new do |yielder|
self.each do |element|
if yield(element)
yielder.yield(element)
break
end
end
end
end
end
a = (1..100).to_enum
p a.lazy_find { |i| i.even? }.first
# => 2
You want to use the break statement:
contents = encodings.each do |e|
s = IO.read( f, "r:#{e}" )
s.valid_encoding? and break s
end
The best I can come up with is with our good friend inject:
contents = encodings.inject(nil) do |s,enc|
s || (c=File.open(f,"r:#{enc}").valid_encoding? && c
end
This is still sub-optimal because it continues to loop through encodings after finding a match, though it doesn't do anything with them, so it's a minor ugliness. Most of the ugliness comes from...well, the code itself. :/

how to name an object reference (handle) dynamically in ruby

So I have a class like this:
def Word
end
and im looping thru an array like this
array.each do |value|
end
And inside that loop I want to instantiate an object, with a handle of the var
value = Word.new
Im sure there is an easy way to do this - I just dont know what it is!
Thanks!
To assign things to a dynamic variable name, you need to use something like eval:
array.each do |value|
eval "#{value} = Word.new"
end
but check this is what you want - you should avoid using eval to solve things that really require different data structures, since it's hard to debug errors created with eval, and can easily cause undesired behaviour. For example, what you might really want is a hash of words and associated objects, for example
words = {}
array.each do |value|
words[value] = Word.new
end
which won't pollute your namespace with tons of Word objects.
Depending on the data structure you want to work with, you could also do this:
# will give you an array:
words = array.map { |value| Word.new(value) }
# will give you a hash (as in Peter's example)
words = array.inject({}) { |hash, value| hash.merge value => Word.new }
# same as above, but more efficient, using monkey-lib (gem install monkey-lib)
words = array.construct_hash { |value| [value, Word.new ] }

Resources