How to Handle Combination []+= for Auto-Vivifying Hash in Ruby

How to handle combination []+= for auto-vivifying hash in Ruby?

There is no way to define a []+= method in ruby. What happens when you type

x[y] += z

is

x[y] = x[y] + z

so both the [] and []= methods are called on x (and + is called on x[y], which in this case is an AutoHash). I think that the best way to handle this problem would be to define a + method on AutoHash, which will just return it's argument. This will make AutoHash.new[:x] += y work for just about any type of y, because the "empty" version of y.class ('' for strings, 0 for numbers, ...) plus y will almost always equal y.

class AutoHash
def +(x); x; end
end

Adding that method will make both of these work:

# Numbers:
a = AutoHash.new
5.times { a[:sum] += 10 }
a[:sum] #=> 50

# Strings:
a = AutoHash.new
5.times { a[:sum] += 'a string ' }
a[:sum] #=> "a string a string a string a string a string "

And by the way, here is a cleaner version of your code:

class AutoHash < Hash
def initialize(args={})
super
@update, @update_index = args[:update], args[:update_key]
end

def [](k)
if has_key? k
super(k)
else
AutoHash.new :update => self, :update_key => k
end
end

def []=(k, v)
@update[@update_index] = self if @update and @update_index
super
end

def +(x); x; end

def self.few(n)
Array.new(n) { AutoHash.new }
end
end

:)

Is auto-initialization of multi-dimensional hash array possible in Ruby, as it is in PHP?

Try this:

def hash_with_default_hash
Hash.new { |hash, key| hash[key] = hash_with_default_hash }
end

a = hash_with_default_hash

If the key doesn't exist, then the result of the block will be used as the default value. In this case, the default value is also a hash which uses hashes as its default values.

Multiple initialization of auto-vivifying hashes using a new operator in Ruby

Where you ask for a := b := c := AutoHash.new.few 3 I think (not sure I understand your desire) that you really want a,b,c=Autohash.new.few 3


Why does few take variable args, when you only ever use the first?

I also find your creation of the return value to be confusing, maybe try

def few(n=0) 
Array.new(n) { AutoHash.new }
end

Beyond that, it seems like few should be a class method. a,b,c=AutoHash.few 3 which will work if you defined few on the class:

def AutoHash.few(n=0)
Array.new(n) { AutoHash.new }
end

If a,b,c=AutoHash.few 3 isn't what you're looking for, and you really want to implement your own operator, then check out Hacking parse.y, which was a talk given at RubyConf 2009. You can watch the presentation at http://rubyconf2009.confreaks.com/19-nov-2009-17-15-hacking-parsey-tatsuhiro-ujihisa.html and you can see the slides at http://www.slideshare.net/ujihisa/hacking-parsey-rubyconf-2009

How to avoid NoMethodError for missing elements in nested hashes, without repeated nil checks?

Ruby 2.3.0 introduced a method called dig on both Hash and Array.

name = params.dig(:company, :owner, :name)

It returns nil if the key is missing at any level.

If you are using a version of Ruby older than 2.3, you can install a gem such as ruby_dig or hash_dig_and_collect, or implement the functionality yourself:

module RubyDig
def dig(key, *rest)
if value = (self[key] rescue nil)
if rest.empty?
value
elsif value.respond_to?(:dig)
value.dig(*rest)
end
end
end
end

if RUBY_VERSION < '2.3'
Array.send(:include, RubyDig)
Hash.send(:include, RubyDig)
end

Ruby Autovivification

I agree with Jonas that you (and Sam) are making this more complicated than it needs to be, but I think even his version is too complicated. I'd just do this:

require 'fastercsv'
records = Hash.new(0)
FasterCSV.foreach("trades_long.dat", :col_sep => "|") do |row|
date,time,account,ticker,desc,type,amount,price = row.fields
records[[account,ticker,type,price]] += amount.to_f
end

Now you have a hash with total amounts for each unique combination of account, ticker, type and price.

One liner nested hash creation in Ruby? (I come from Perl)

Unfortunately, there is no simple, practical way. A Ruby equivalent would be an ugly, ugly beast like:

((((@date[month] ||= {})[day] ||= {})[hours] ||= {})[min] ||= {})[sec] = 1

There is a way to assign default values for missing keys in hashes, though:

@date = Hash.new { |hash, key| hash[key] = {} }

# @date[:month] is set to a new, empty hash because the key is missing.
@date[:month][:day] = 1

Unfortunately this does not work recursively.

...unless you create it yourself; hooray for Ruby!

class Hash
def self.recursive
new { |hash, key| hash[key] = recursive }
end
end

@date = Hash.recursive
@date[month][day][hours][min][sec] = 1
# @date now equals {month=>{day=>{hours=>{min=>{sec=>1}}}}}

Keep in mind, though, that all unset values are now {} rather than nil.

How do I summarize hashes within a hash?

First and foremost don't use upper-case letters for variables in Ruby as you did with SomeHash and XPath. When a variable name starts with an upper-case letter, it means it's a constant and you probably didn't want it to be a constant.

each is not the best way to do this, you can do this much more simply with inject as in:

countries = {
"Albania"=>"Europe",
"Andorra"=>"Europe",
"Austria"=>"Europe",
"Lebanon"=>"Asia",
"Macau"=>"Asia",
"Malaysia"=>"Asia",
"Papua New Guinea"=>"Asia",
"Jamaica"=>"North America",
"Martinique"=>"North America",
"Argentina"=>"South America",
"Chile"=>"South America",
"Sao Tome and Principe"=>"Africa",
"Senegal"=>"Africa",
"Somalia"=>"Africa"}

by_continents = countries.inject({}) do |memo, (k,v)|
memo[v] ||= []
memo[v] << k
memo
end

The output for this is:

{"Europe"=>["Albania", "Andorra", "Austria"], "Asia"=>["Lebanon", "Macau", "Malaysia", "Papua New Guinea"], "North America"=>["Jamaica", "Martinique"], "South America"=>["Argentina", "Chile"], "Africa"=>["Sao Tome and Principe", "Senegal", "Somalia"]}

You have all countries grouped by continent and you can pick any one of them.

In your code it should be placed like this:

def country
inflation_hash = {}
XPath.match( data, "//country").map do |element|
inflation_hash[element.attributes["name"]] = element.attributes["continent"]
end
by_continents = inflation_hash.inject({}) do |memo, (k,v)|
memo[v] ||= []
memo[v] << k
memo
end
puts by_continents.inspect
by_continents
end


Related Topics



Leave a reply



Submit