Change values in a nested hash - ruby

I have a configuration class in Ruby that used to have keys like "core.username" and "core.servers", which was stored in a YAML file just like that.
Now I'm trying to change it to be nested, but without having to change all the places that refer to keys in the old way. I've managed it with the reader-method:
def [](key)
namespace, *rest = key.split(".")
target = #config[namespace]
rest.each do |k|
return nil unless target[k]
target = target[k]
end
target
end
But when I tried the same with the writer-class, that works, but isn't set in the #config-hash. #config is set with just a call to YAML.load_file
I managed to get it working with eval, but that is not something I would like to keep for long.
def []=(key, value)
namespace, *rest = key.split(".")
target = "#config[\"#{namespace}\"]"
rest.each do |key|
target += "[\"#{key}\"]"
end
eval "#{target} = value"
self[key]
end
Is there any decent way to achieve this, preferably without changing plugins and code throughout?

def []=(key, value)
subkeys = key.split(".")
lastkey = subkeys.pop
subhash = subkeys.inject(#config) do |hash, k|
hash[k]
end
subhash[lastkey] = value
end
Edit: Fixed the split.
PS: You can also replace the inject with an each-loop like in the [] method if you prefer. The important thing is that you do not call [] with the last key, but instead []= to set the value.

I used recursion:
def change(hash)
if hash.is_an? Hash
hash.inject({}) do |acc, kv|
hash[change(kv.first)] = change(kv.last)
hash
end
else
hash.to_s.split('.').trim # Do your fancy stuff here
end
end

Related

Ruby hash use key value in default value

I have the following code to create an array to object hash:
tp = TupleProfile.new(98, 99)
keyDict = Hash[Array[98,99] => tp]
keyDict[[98,99]].addLatency(0.45)
puts keyDict[[98,99]].getAvg()
This works, but I'd like to be able to call addLatency without checking for an existing hash value:
keyDict[[100,98]].addLatency(0.45) #throws error right now
So I want to create a default value that varies based on the key, something like:
keyDict = Hash.new(TupleProfile.new(theKey[0], theKey[1]))
Where theKey is some sort of special directive. Is there any reasonably clean way to do this, or am I better off checking each time or making a wrapper class for the hash?
Try the Hash.new block notation:
keyDict = Hash.new {|hash,key| hash[key] = TupleProfile.new(*key) }
Using the standard parameter notation (Hash.new(xyz)) will really only instantiate a single TupleProfile object for the hash; this way there will be one for each individual key.
If I understand your question, I think you might be able to use a default procedure. The code in the default procedure will get run if you ask for a key that doesn't exist. Here is an example using a tuple key:
class Test
def initialize(a,b); #a = a; #b = b; end
attr_accessor :a, :b
end
keyDict = {}
keyDict.default_proc = proc do |hash, (key1, key2)|
hash[[key1, key2]] = Test.new(key1, key2)
end
keyDict[[99,200]]
=> #<Test:0x007f9681ad2720 #a=99, #b=200>
keyDict[[99,200]].a
=> 99

Subclassing Ruby Hash, object has no methods of Hash?

I'm creating a object of hash in order to write a little script that reads in a file a line at a time, and assigns arrays into my hash class. I get wildly different results depending if I subclass Hash or not, plus using super changes things which I don't' understand.
My main issue is that without subclassing hash ( < Hash) it works perfectly, but I get no methods of Hash (like to iterate over the keys and get things out of it.... Subclassing Hash lets me do those things, but it seems that only the last element of the hashed arrays is ever stored.... so any insight into how you get the methods of a subclass. The Dictionary class is a great example I found on this site, and does exactly what I want, so I'm trying to understand how to use it properly.
filename = 'inputfile.txt.'
# ??? class Dictionary < Hash
class Dictionary
def initialize()
#data = Hash.new { |hash, key| hash[key] = [] }
end
def [](key)
#data[key]
end
def []=(key,words)
#data[key] += [words].flatten
#data[key]
# super(key,words)
end
end
listData = Dictionary.new
File.open(filename, 'r').each_line do |line|
line = line.strip.split(/[^[:alpha:]|#|\.]/)
puts "LIST-> #{line[0]} SUB-> #{line[1]} "
listData[line[0]] = ("#{line[1]}")
end
puts '====================================='
puts listData.inspect
puts '====================================='
print listData.reduce('') {|s, (k, v)|
s << "The key is #{k} and the value is #{v}.\n"
}
If anyone understands what is going on here subclassing hash, and has some pointers, that would be excellent.
Running without explicit < Hash:
./list.rb:34:in `<main>': undefined method `reduce' for #<Dictionary:0x007fcf0a8879e0> (NoMethodError)
That is the typical error I see when I try and iterate in any way over my hash.
Here is a sample input file:
listA billg#microsoft.com
listA ed#apple.com
listA frank#lotus.com
listB evanwhite#go.com
listB joespink#go.com
listB fredgrey#stop.com
I can't reproduce your problem using your code:
d = Dictionary.new #=> #<Dictionary:0x007f903a1adef8 #data={}>
d[4] << 5 #=> [5]
d[5] << 6 #=> [6]
d #=> #<Dictionary:0x007f903a1adef8 #data={4=>[5], 5=>[6]}>
d.instance_variable_get(:#data) #=> {4=>[5], 5=>[6]}
But of course you won't get reduce if you don't subclass or include a class/module that defines it, or define it yourself!
The way you have implemented Dictionary is bound to have problems. You should call super instead of reimplementing wherever possible. For example, simply this works:
class Dictionary < Hash
def initialize
super { |hash, key| hash[key] = [] }
end
end
d = Dictionary.new #=> {}
d['answer'] << 42 #=> [42]
d['pi'] << 3.14 #=> [3.14
d #=> {"answer"=>[42], "pi"=>[3.14]}
If you want to reimplement how and where the internal hash is stored (i.e., using #data), you'd have to reimplement at least each (since that is what almost all Enumerable methods call to) and getters/setters. Not worth the effort when you can just change one method instead.
While Andrew Marshall's answer
already correct, You could also try this alternative below.
Going from your code, We could assume that you want to create an object that
act like a Hash, but with a little bit different behaviour. Hence our first
code will be like this.
class Dictionary < Hash
Assigning a new value to some key in the dictionary will be done differently
in here. From your example above, the assignment won't replace the previous
value with a new one, but instead push the new value to the previous or to
a new array that initialized with the new value if the key doesn't exist yet.
Here I use the << operator as the shorthand of push method for Array.
Also, the method return the value since it's what super do (see the if part)
def []=(key, value)
if self[key]
self[key] << value
return value # here we mimic what super do
else
super(key, [value])
end
end
The advantage of using our own class is we could add new method to the class
and it will be accessible to all of it instance. Hence we need not to
monkeypatch the Hash class that considered dangerous thing.
def size_of(key)
return self[key].size if self[key]
return 0 # the case for non existing key
end
Now, if we combine all above we will get this code
class Dictionary < Hash
def []=(key, value)
if self[key]
self[key] << value
return value
else
super(key, [value])
end
end
def size_of(key)
return self[key].size if self[key]
return 0 # the case for non existing key
end
end
player_emails = Dictionary.new
player_emails["SAO"] = "kirito#sao.com" # note no << operator needed here
player_emails["ALO"] = "lyfa#alo.com"
player_emails["SAO"] = "lizbeth#sao.com"
player_emails["SAO"] = "asuna#sao.com"
player_emails.size_of("SAO") #=> 3
player_emails.size_of("ALO") #=> 1
player_emails.size_of("GGO") #=> 0
p listData
#=> {"SAO" => ["kirito#sao.com", "lizbeth#sao.com", "asuna#sao.com"],
#=> "ALO" => ["lyfa#alo.com"] }
But, surely, the class definition could be replaced with this single line
player_emails = Hash.new { [] }
# note that we wont use
#
# player_emails[key] = value
#
# instead
#
# player_emails[key] << value
#
# Oh, if you consider the comment,
# it will no longer considered a single line
While the answer are finished, I wanna comment some of your example code:
filename = 'inputfile.txt.'
# Maybe it's better to use ARGF instead,
# so you could supply the filename in the command line
# and, is the filename ended with a dot? O.o;
File.open(filename, 'r').each_line do |line|
# This line open the file anonimously,
# then access each line of the file.
# Please correct me, Is the file will properly closed? I doubt no.
# Saver version:
File.open(filename, 'r') do |file|
file.each_line do |line|
# ...
end
end # the file will closed when we reach here
# ARGF version:
ARGF.each_line do |line|
# ...
end
# Inside the each_line block
line = line.strip.split(/[^[:alpha:]|#|\.]/)
# I don't know what do you mean by that line,
# but using that regex will result
#
# ["listA", "", "", "billg#microsoft.com"]
#
# Hence, your example will fail since
# line[0] == "listA" and line[1] == ""
# also note that your regex mean
#
# any character except:
# letters, '|', '#', '|', '\.'
#
# If you want to split over one or more
# whitespace characters use \s+ instead.
# Hence we could replace it with:
line = line.strip.split(/\s+/)
puts "LIST-> #{line[0]} SUB-> #{line[1]} "
# OK, Is this supposed to debug the line?
# Tips: the simplest way to debug is:
#
# p line
#
# that's all,
listData[line[0]] = ("#{line[1]}")
# why? using (), then "", then #{}
# I suggest:
listData[line[0]] = line[1]
# But to make more simple, actually you could do this instead
key, value = line.strip.split(/\s+/)
listData[key] = value
# Outside the block:
puts '====================================='
# OK, that's too loooooooooong...
puts '=' * 30
# or better assign it to a variable since you use it twice
a = '=' * 30
puts a
p listData # better way to debug
puts a
# next:
print listData.reduce('') { |s, (k, v)|
s << "The key is #{k} and the value is #{v}.\n"
}
# why using reduce?
# for debugging you could use `p listData` instead.
# but since you are printing it, why not iterate for
# each element then print each of that.
listData.each do |k, v|
puts "The key is #{k} and the value is #{v}."
end
OK, sorry for blabbering so much, Hope it help.

How do you create an enumerable object that evaluates on demand out of another enumerable object?

Given I have code like the following, what do I need to do to make it work?
config = {} #options for faster csv
input_file = "foo.csv"
# can be in any class or module
def count_item_groups(items)
results = Hash.new(0)
(items || []).each do |current|
results[current.to_s] += 1
end
results
end
row_value_iterator = FasterCSV.foreach(input_file, config) do |row|
yield return row[1]
end
result = count_item_groups(row_value_iterator)
Versus code like this
def do_it_all
results = Hash.new(0)
FasterCSV.foreach(input_file, config) do |row|
results[row[1].to_s] += 1
end
results
end
Result should be a hash with keys of the row[1] values. yield return doesn't exist in Ruby, but I'm sure that Ruby can handle this type of code.
That's what I understand you are asking: "How can I transform a method like FasterCSV.foreach that works imperatively (by doing side-effects) to something functional (that yields values) so I can modularize my code".
Answer: In Ruby you can transform a each method to an Enumerator object with Object#enum_for. Now you could use your count_item_groups with the output of the map, but I'd suggest to use Facets' Enumerable#frequency:
results = FasterCSV.enum_for(:foreach, "file.csv", {}).map do |row|
row[1].to_s
end.frequency
#=> {"val1"=>3, "val2"=>1}
I'm not sure what you're asking, I assumed that is related to chainable feature.
Instead of passing the object iterator to another iterator as parameter, in ruby you can chain these iterators. It mignt look like this.
row_value_iterator = FasterCSV.foreach(input_file, config).map do |row|
row[1]
end
result = row_value_iterator.each_with_object(Hash.new(0)) do |current,results|
results[current.to_s] += 1
end
Or do it in truly chain style:
result = FasterCSV.foreach(input_file,config).each_with_object(Hash.new(0)) do |row,results|
results[row[1].to_s] += 1
end

Can I reject objects which do not meet my criteria as they are entered into an array?

I know there are a number of ways to create new elements in an existing ruby array.
e.g.
myArray = []
myArray + other_array
myArray << obj
myArray[index] = obj
I'm also pretty sure I could use .collect, .map, .concat, .fill, .replace, .insert, .join, .pack and .push as well to add to or otherwise modify the contents of myArray.
However, I want to ensure that myArray only ever includes valid HTTP/HTTPS URLs.
Can anyone explain how I can enforce that kind of behaviour?
I would create a module that allows you to specify an acceptance block for an array, and then override all the methods you mention (and more, like concat) to pre-filter the argument before calling super. For example:
module LimitedAcceptance
def only_allow(&block)
#only_allow = block
end
def <<( other )
super if #only_allow[ other ]
end
def +( other_array )
super( other_array.select(&#only_allow) )
end
end
require 'uri'
my_array = []
my_array.extend LimitedAcceptance
my_array.only_allow do |item|
uri = item.is_a?(String) && URI.parse(item) rescue nil
uri.class <= URI::HTTP
end
my_array << "http://phrogz.net/"
my_array << "ftp://no.way"
my_array += %w[ ssh://bar http://ruby-lang.org http:// ]
puts my_array
#=> http://phrogz.net/
#=> http://ruby-lang.org
Create a class to encapsulate behavior you want. Then you can create your << method doing the verifications you want.
Put all logic that handle this data in methods in this domain class. Probably you will discover code floating around the use of this data to move to the new class.
My 2 cents.
Use this to insert. (untested).
def insert_to_array(first_array, second_array)
second_array.each do |i| {
if URI.parse(i).class == URI::HTTP
first_array.insert(i)
end
}
first_array
end

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