Ruby recursive map of a hash of objects - ruby

I have a hash that has an unknown collection and mixture of nested arrays, hashes, arrays of hashes and strings. This is the result of JSON.parse. The structure of the data must be the same as it started with. The end goal is to convert strings to Fixnums that could be Fixnums.
The following works just fine, but I was wondering if it could be shortened. Note how I need the key and the value in the clean method as not all strings that can be Fixnums should be. Any ideas?
def clean_node(node)
if node.class == String
clean(node)
elsif node.class == Array
node.each_with_index do |obj, i|
if obj.class == String
node[i] = clean(node[i], obj)
else
clean_node(obj)
end
end
elsif node.class == Hash
node.each_pair do |key, value|
if value.class == String
node[key] = clean(key, value)
else
clean_node(value)
end
end
end
end
def clean(key, value)
FIXNUM_KEYS.include?(key)? value.to_i : value
end

There is no need to split up the string processing, you can do it all in one recursive routine, if you return the value of node at the end of the routine. This has the side effect of removing some parameters, and you can use in-place .map! for handling arrays.
Using case makes the selection by type slightly easier to read.
Adding the ability to deal with arrays of strings could be done in lots of ways. I've chosen to add a state variable (a common pattern in recursion, for instance to count depth). Then in the case of arrays, the recursion inherits the state from the current level, whilst the hash case determines new state (to convert or not) based on lookup in the list of keys to apply conversions.
def clean_node( node, convert_item = false )
case node
when String
node = node.to_i if convert_item
when Array
node.map! do |obj|
clean_node( obj, convert_item )
end
when Hash
node.each_pair do |key, value|
node[key] = clean_node( value, FIXNUM_KEYS.include?(key) )
end
end
node
end

Although I have not looked into the recursion, I must comment on the fact that you are writing if statements that would be easier to read in a case statement:
def clean_node(node)
case node
when String then clean(node)
when Array
node.each_with_index do |obj, i|
case obj
when String
node[i] = clean(node[i], obj)
else
clean_node(obj)
end
end
when Hash....

Related

Ruby Enumerable: get the first which is not nil, but the result must be what's inside the block

The Enumerable#find method works by evaluating until the it finds an element which matches the condition in the block. Is there something similar for returning the first time that the block is not evaluated to nil? imagining one would have a collection of hashes:
value = nil
options.each do |o|
break if value = o[:desired]
end
value ||= DEFAULT
isn't there a method which already accomplishes this?
No point in making a lot of transformations to the collection, i'd like to minimize the number of allocations, so any solution which allocates a new Array will not be good for me.
find method will work for finding first element which has :desired key with minimum iterations.
I think you wish to get the value of desired key from the block instead of element itself - there is no method in Enumerable that behaves like a mixture of find and map - you will have to use the outer variable to which value is assigned inside the block as shown below.
options = [{foo: 1}, {desired: 2}, {bar: 3}]
value = nil
options.find do |o|
value = o[:desired]
break if value
end
p value
#=> 2
It more or less looks like your code, which should also work just fine.
Below is one way which you can use if you want to use Enumerable methods, but it will iterate over all elements.
p value = options.map { |o| value = o[:desired] }.compact.first
You can use reduce:
value = options.reduce(nil){|memo, entry| memo || entry[:desired] } || DEFAULT
As of Ruby 2.0, this can be accomplished by combining #map and #find with lazy enumerables:
value = options.lazy.map { |o| o[:desired] }.find { |x| !x.nil? } # or find(&:present?) with ActiveSupport
value ||= DEFAULT
This came up for me today: I think we can use break with a value from reduce
treasure = [1,2,3].reduce(nil) do |memo, value|
break memo if memo
foo(value)
end
How about
options.reduce{ |_,o|
break o if o[:desired]
DEFAULT
}
or
catch do |tag|
options.each{ |_,o| o[:desired] and throw tag, o }
DEFAULT
end
The latter allows for recursion.

Return to Each Iterator from For Loop

I was wondering if it is possible to return to an Each iterator in Ruby from within a for-loop placed within the block passed to Each.
def find member = ""
productHash = {}
##entries is a hash, with both the keys and values being strings
#the member parameter is a string
#entries.each do |key, value|
for i in 0...member.size
if(key[i] != member[i])
next #the next keyword doesn't work...all it does is return to the for iterator. I'm looking for a keyword that would return to the each iterator, and allow each to pass back in the next key-value pair.
end
end
productHash[key] = value
end
productHash
end
What I'm trying to accomplish is this: the moment I see that a character in the member parameter doesn't match the corresponding character in a given key, I move on to the next key-value pair.
It looks like you're trying to do some kind of comparison where if the key matches a particular prefix specified by member then you would make an assignment.
This code should be functionally similar:
def find(member = "")
hash = { }
#entries.each do |key, value|
# Unless key begins with prefix, skip it.
next unless (key[0, prefix.length] == prefix)
hash[key] = value
end
hash
end
There's no official goto statement in Ruby, and many would argue this is a good thing, but it means that busting out of nested blocks can be a bit tricky.
Still, if you approach the problem in the right way, there's almost always a solution that's elegant enough.
Update:
To break out of nested loops, an approach might be:
list.each do |i|
broken = false
inner_list.each do |j|
if (j > 10)
broken = true
break
end
end
break if (broken)
end

A way to specify and initialize the type of a map's values?

I want to count all the words in a line of text. I'm using a map to do this, with the words for keys and integers for values. I don't know how to tell Ruby that all the values will be integers. It forces me to put an ugly branching inside my iterator's block:
# in the constructor
#individual_words = {}
def count_words_from( text_line )
text_line.each do |line|
line.scan(/\p{Word}+/)
.reject{ |string| string =~ /\d/ }
.each do |word|
if #individual_words[ word ] == nil then # This is ugly
#individual_words[ word ] = 1 # This is ugly as well
else
#individual_words[ word ] += 1
end
end
end
end
In simple, I'd like to do something like this Java line:
Map<String, Integer> individualWords;
to avoid having to change the type of the first occurence of a word from Nil to Integer.
You can set a default value in your hash like this:
individual_words = Hash.new(0)
Then when you come across a word, whether its key is in the hash or not, all you have to do is:
individual_words[word] += 1
You can also do something like this
#individual_words[word] ||= 0
#individual_words[word] += 1
||= ensures that the value gets set if it's not truthy (ie. nil)

Ruby thinks element of hash is Integer

I have a function in Ruby:
def find_item(keyword)
potential = []
$items.each do |item|
puts item # <-- for debugging purposes
if item["name"].downcase== keyword
potential << item["name"].downcase
elsif item["keywords"].index(keyword) != nil
potential << item["name"].downcase
end
end
return potential
end
(The global variable $items is a Hash object that maps a few strings to some values that determine the properties of the item.)
When I puts the current item it is iterating over (the line with the comment does just that), it gives me:
{"name"=>"Thing1", "keywords"=>["thing", "green"], ...}
but when I try item["name"] on the next line (which should definitely return Thing1), it gives me:
C:/somepath/someprogram.rb:125:in '[]': can't convert String into Integer (TypeError)
if $items is a Hash, then $items.each do |item| will yield [key, value] pairs (Arrays) to the block. If you only want the values, use each_value.

Search ruby hash for empty value

I have a ruby hash like this
h = {"a" => "1", "b" => "", "c" => "2"}
Now I have a ruby function which evaluates this hash and returns true if it finds a key with an empty value. I have the following function which always returns true even if all keys in the hash are not empty
def hash_has_blank(hsh)
hsh.each do |k,v|
if v.empty?
return true
end
end
return false
end
What am I doing wrong here?
Try this:
def hash_has_blank hsh
hsh.values.any? &:empty?
end
Or:
def hash_has_blank hsh
hsh.values.any?{|i|i.empty?}
end
If you are using an old 1.8.x Ruby
I hope you're ready to learn some ruby magic here. I wouldn't define such a function globally like you did. If it's an operation on a hash, than it should be an instance method on the Hash class you can do it like this:
class Hash
def has_blank?
self.reject{|k,v| !v.nil? || v.length > 0}.size > 0
end
end
reject will return a new hash with all the empty strings, and than it will be checked how big this new hash is.
a possibly more efficient way (it shouldn't traverse the whole array):
class Hash
def has_blank?
self.values.any?{|v| v.nil? || v.length == 0}
end
end
But this will still traverse the whole hash, if there is no empty value
I've changed the empty? to !nil? || length >0 because I don't know how your empty method works.
If you just want to check if any of the values is an empty string you could do
h.has_value?('')
but your function seems to work fine.
I'd consider refactoring your model domain. Obviously the hash represents something tangible. Why not make it an object? If the item can be completely represented by a hash, you may wish to subclass Hash. If it's more complicated, the hash can be an attribute.
Secondly, the reason for which you are checking blanks can be named to better reflect your domain. You haven't told us the "why", but let's assume that your Item is only valid if it doesn't have any blank values.
class MyItem < Hash
def valid?
!invalid?
end
def invalid?
values.any?{|i| i.empty?}
end
end
The point is, if you can establish a vocabulary that makes sense in your domain, your code will be cleaner and more understandable. Using a Hash is just a means to an end and you'd be better off using more descriptive, domain-specific terms.
Using the example above, you'd be able to do:
my_item = MyItem["a" => "1", "b" => "", "c" => "2"]
my_item.valid? #=> false

Resources