Ruby replace hash key using a Regex - ruby-on-rails

I am parsing an Excel file using Creek. This is the first row (the header):
{"A"=>"Date", "B"=>"Portfolio", "C"=>"Currency"}
and all the other rows are:
[
{"A"=>2019-05-16 00:00:00 +0200, "B"=>"TEXT", "C"=>"INR"},
{"A"=>2019-05-20 00:00:00 +0200, "B"=>"TEXT2", "C"=>"EUR"}
]
My goal is to have the same array, where all hash keys are replaced with key of mapping using a regex expression in the values of the mapping hash.
For example, in the header, the keys match these REGEX:
mapping = {
date: /Date|Data|datum|Fecha/,
portfolio_name: /Portfolio|portafoglio|Portfolioname|cartera|portefeuille/,
currency: /Currency|Valuta|Währung|Divisa|Devise/
}
So I need all data rows to be replaced like this:
[
{"date"=>2019-05-16 00:00:00 +0200, "portfolio_name"=>"TEXT", "currency"=>"INR"},
{"date=>2019-05-20 00:00:00 +0200, "portfolio_name"=>"TEXT2", "currency"=>"EUR"}
]

Detect column names in a separate step. Intermediate mapping will look like {"A"=>:date, "B"=>:portfolio_name, "C"=>:currency}, and then you can transform data array.
This is pretty straightforward:
header_mapping = header.transform_values{|v|
mapping.find{|key,regex| v.match?(regex) }&.first || raise("Unknown header field #{v}")
}
rows.map{|row|
row.transform_keys{|k| header_mapping[k].to_s }
}
Code requires Ruby 2.4+ for native Hash#transform_* or ActiveSupport

TL:DR;
require 'time'
mappings = {
date: /Date|Data|datum|Fecha/,
portfolio_name: /Portfolio|portafoglio|Portfolioname|cartera|portefeuille/,
currency: /Currency|Valuta|Währung|Divisa|Devise/
}
rows = [
{"A"=>"Date", "B"=>"Portfolio", "C"=>"Currency"},
{"A"=>Time.parse('2019-05-16 00:00:00 +0200'), "B"=>"TEXT", "C"=>"INR"},
{"A"=>Time.parse('2019-05-20 00:00:00 +0200'), "B"=>"TEXT2", "C"=>"EUR"}
]
header_row = rows.first
mapped_header_row = header_row.inject({}) do |hash, (k, v)|
mapped_name = mappings.find do |mapped_name, regex|
v.match? regex
end&.first
# defaults to `v.to_sym` (Header Name), if not in mappings
# you can also raise an Exception here instead if not in mappings, depending on your expectations
hash[k] = mapped_name || v.to_sym
hash
end
mapped_rows = rows[1..-1].map do |row|
new_row = {}
row.each do |k, v|
new_row[mapped_header_row[k]] = v
end
new_row
end
puts mapped_rows
# => [
# {:date=>2019-05-16 00:00:00 +0200, :portfolio_name=>"TEXT", :currency=>"INR"},
# {:date=>2019-05-20 00:00:00 +0200, :portfolio_name=>"TEXT2", :currency=>"EUR"}
# ]
Given:
require 'time'
mappings = {
date: /Date|Data|datum|Fecha/,
portfolio_name: /Portfolio|portafoglio|Portfolioname|cartera|portefeuille/,
currency: /Currency|Valuta|Währung|Divisa|Devise/
}
rows = [
{"A"=>"Date", "B"=>"Portfolio", "C"=>"Currency"},
{"A"=>Time.parse('2019-05-16 00:00:00 +0200'), "B"=>"TEXT", "C"=>"INR"},
{"A"=>Time.parse('2019-05-20 00:00:00 +0200'), "B"=>"TEXT2", "C"=>"EUR"}
]
Steps:
We first extract the first row, to get the column names.
header_row = rows.first
puts header_row
# => {"A"=>"Date", "B"=>"Portfolio", "C"=>"Currency"}
We need to loop through each of the Hash pairs: (key, value), and we need to find if the "value" corresponds to any of our mappings variable.
In short for this step, we need to somehow convert (i.e.):
header_row = {"A"=>"Date", "B"=>"Portfolio", "C"=>"Currency"}
into
mapped_header_row = {"A"=>"date", "B"=>"portfolio_name", "C"=>"currency"}
And so...
mapped_header_row = header_row.inject({}) do |hash, (k, v)|
mapped_name = mappings.find do |mapped_name, regex|
v.match? regex
end&.first
# defaults to `v.to_sym` (Header Name), if not in mappings
# you can also raise an Exception here instead if not in mappings, depending on your expectations
hash[k] = mapped_name || v.to_sym
hash
end
puts mapped_header_row
# => {"A"=>"date", "B"=>"portfolio_name", "C"=>"currency"}
See inject
See find
Now that we have the mapped_header_row (or the "mapped" labels / names for each column), then we can just simply update all of the "keys" of 2nd row until the last row, with the "mapped" name: the keys being "A", "B", and "C"... to be replaced correspondingly with "date", "portfolio_name", and "currency"
# row[1..-1] means the 2nd element in the array until the last element
mapped_rows = rows[1..-1].map do |row|
new_row = {}
row.each do |k, v|
new_row[mapped_header_row[k]] = v
end
new_row
end
puts mapped_rows
# => [
# {:date=>2019-05-16 00:00:00 +0200, :portfolio_name=>"TEXT", :currency=>"INR"},
# {:date=>2019-05-20 00:00:00 +0200, :portfolio_name=>"TEXT2", :currency=>"EUR"}
# ]
See map

Related

Dynamically create hash from array of arrays

I want to dynamically create a Hash without overwriting keys from an array of arrays. Each array has a string that contains the nested key that should be created. However, I am running into the issue where I am overwriting keys and thus only the last key is there
data = {}
values = [
["income:concessions", 0, "noi", "722300", "purpose", "refinancing"],
["fees:fee-one", "0" ,"income:gross-income", "900000", "expenses:admin", "7500"],
["fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
]
What it should look like:
{
"income" => {
"concessions" => 0,
"gross-income" => "900000"
},
"expenses" => {
"admin" => "7500",
"other" => "0"
}
"noi" => "722300",
"purpose" => "refinancing",
"fees" => {
"fee-one" => 0,
"fee-two" => 0
},
"address" => {
"zip" => "10019"
}
}
This is the code that I currently, have how can I avoid overwriting keys when I merge?
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
a = data.merge!(hh)
end
end
end
The code you've provided can be modified to merge hashes on conflict instead of overwriting:
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
data.merge!(hh) { |_, old, new| old.merge(new) }
end
end
end
But this code only works for the two levels of nesting.
By the way, I noted ruby-on-rails tag on the question. There's deep_merge method that can fix the problem:
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
data.deep_merge!(hh)
end
end
end
values.flatten.each_slice(2).with_object({}) do |(f,v),h|
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
end
#=> {"income"=>{"concessions"=>0, "gross-income"=>"900000"},
# "noi"=>"722300",
# "purpose"=>"refinancing",
# "fees"=>{"fee-one"=>"0", "fee-two"=>"0"},
# "expenses"=>{"admin"=>"7500", "other"=>"0"},
# "address"=>{"zip"=>"10019"}}
The steps are as follows.
values = [
["income:concessions", 0, "noi", "722300", "purpose", "refinancing"],
["fees:fee-one", "0" ,"income:gross-income", "900000", "expenses:admin", "7500"],
["fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
]
a = values.flatten
#=> ["income:concessions", 0, "noi", "722300", "purpose", "refinancing",
# "fees:fee-one", "0", "income:gross-income", "900000", "expenses:admin", "7500",
# "fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
enum1 = a.each_slice(2)
#=> #<Enumerator: ["income:concessions", 0, "noi", "722300",
# "purpose", "refinancing", "fees:fee-one", "0", "income:gross-income", "900000",
# "expenses:admin", "7500", "fees:fee-two", "0", "address:zip", "10019",
# "expenses:other","0"]:each_slice(2)>
We can see what values this enumerator will generate by converting it to an array.
enum1.to_a
#=> [["income:concessions", 0], ["noi", "722300"], ["purpose", "refinancing"],
# ["fees:fee-one", "0"], ["income:gross-income", "900000"],
# ["expenses:admin", "7500"], ["fees:fee-two", "0"],
# ["address:zip", "10019"], ["expenses:other", "0"]]
Continuing,
enum2 = enum1.with_object({})
#=> #<Enumerator: #<Enumerator:
# ["income:concessions", 0, "noi", "722300", "purpose", "refinancing",
# "fees:fee-one", "0", "income:gross-income", "900000", "expenses:admin", "7500",
# "fees:fee-two", "0", "address:zip", "10019", "expenses:other", "0"]
# :each_slice(2)>:with_object({})>
enum2.to_a
#=> [[["income:concessions", 0], {}], [["noi", "722300"], {}],
# [["purpose", "refinancing"], {}], [["fees:fee-one", "0"], {}],
# [["income:gross-income", "900000"], {}], [["expenses:admin", "7500"], {}],
# [["fees:fee-two", "0"], {}], [["address:zip", "10019"], {}],
# [["expenses:other", "0"], {}]]
enum2 can be thought of as a compound enumerator (though Ruby has no such concept). The hash being generated is initially empty, as shown, but will be filled in as additional elements are generated by enum2
The first value is generated by enum2 and passed to the block, and the block values are assigned values by a process called array decomposition.
(f,v),h = enum2.next
#=> [["income:concessions", 0], {}]
f #=> "income:concessions"
v #=> 0
h #=> {}
We now perform the block calculation.
f.is_a?(String)
#=> true
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
#=> ["income", "concessions"]
e.nil?
#=> false
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
#=> {"concessions"=>0}
h[k] equals nil if h does not have a key k. In that case (h[k] || {}) #=> {}. If h does have a key k (and h[k] in not nil).(h[k] || {}) #=> h[k].
A second value is now generated by enum2 and passed to the block.
(f,v),h = enum2.next
#=> [["noi", "722300"], {"income"=>{"concessions"=>0}}]
f #=> "noi"
v #=> "722300"
h #=> {"income"=>{"concessions"=>0}}
Notice that the hash, h, has been updated. Recall it will be returned by the block after all elements of enum2 have been generated. We now perform the block calculation.
f.is_a?(String)
#=> true
k,e = f.is_a?(String) ? f.split(':') : [f,nil]
#=> ["noi"]
e #=> nil
e.nil?
#=> true
h[k] = e.nil? ? v : (h[k] || {}).merge(e=>v)
#=> "722300"
h #=> {"income"=>{"concessions"=>0}, "noi"=>"722300"}
The remaining calculations are similar.
merge overwrites a duplicate key by default.
{ "income"=> { "concessions" => 0 } }.merge({ "income"=> { "gross-income" => "900000" } } completely overwrites the original value of "income". What you want is a recursive merge, where instead of just merging the top level hash you're merging the nested values when there's duplication.
merge takes a block where you can specify what to do in the event of duplication. From the documentation:
merge!(other_hash){|key, oldval, newval| block} → hsh
Adds the contents of other_hash to hsh. If no block is specified, entries with duplicate keys are overwritten with the values from other_hash, otherwise the value of each duplicate key is determined by calling the block with the key, its value in hsh and its value in other_hash
Using this you can define a simple recursive_merge in one line
def recursive_merge!(hash, other)
hash.merge!(other) { |_key, old_val, new_val| recursive_merge!(old_val, new_val) }
end
values.each do |row|
Hash[*row].each do |key, value|
keys = key.split(':')
if !data.dig(*keys)
hh = keys.reverse.inject(value) { |a, n| { n => a } }
a = recursive_merge!(data, hh)
end
end
end
A few more lines will give you a more robust solution, that will overwrite duplicate keys that are not hashes and even take a block just like merge
def recursive_merge!(hash, other, &block)
hash.merge!(other) do |_key, old_val, new_val|
if [old_val, new_val].all? { |v| v.is_a?(Hash) }
recursive_merge!(old_val, new_val, &block)
elsif block_given?
block.call(_key, old_val, new_val)
else
new_val
end
end
end
h1 = { a: true, b: { c: [1, 2, 3] } }
h2 = { a: false, b: { x: [3, 4, 5] } }
recursive_merge!(h1, h2) { |_k, o, _n| o } # => { a: true, b: { c: [1, 2, 3], x: [3, 4, 5] } }
Note: This method reproduces the results you would get from ActiveSupport's Hash#deep_merge if you're using Rails.
This is how I would handle this:
def new_h
Hash.new{|h,k| h[k] = new_h}
end
values.flatten.each_slice(2).each_with_object(new_h) do |(k,v),obj|
keys = k.is_a?(String) ? k.split(':') : [k]
if keys.count > 1
set_key = keys.pop
obj.merge!(keys.inject(new_h) {|memo,k1| memo[k1] = new_h})
.dig(*keys)
.merge!({set_key => v})
else
obj[k] = v
end
end
#=> {"income"=>{
"concessions"=>0,
"gross-income"=>"900000"},
"noi"=>"722300",
"purpose"=>"refinancing",
"fees"=>{
"fee-one"=>"0",
"fee-two"=>"0"},
"expenses"=>{
"admin"=>"7500",
"other"=>"0"},
"address"=>{
"zip"=>"10019"}
}
Explanation:
Define a method (new_h) for setting up a new Hash with default new_h at any level (Hash.new{|h,k| h[k] = new_h})
First flatten the Array (values.flatten)
then group each 2 elements together as sudo key value pairs (.each_slice(2))
then iterate over the pairs using an accumulator where each new element added is defaulted to a Hash (.each_with_object(new_h.call) do |(k,v),obj|)
split the sudo key on a colon (keys = k.is_a?(String) ? k.split(':') : [k])
if there is a split then create the parent key(s) (obj.merge!(keys.inject(new_h.call) {|memo,k1| memo[k1] = new_h.call}))
merge the last child key equal to the value (obj.dig(*keys.merge!({set_key => v}))
other wise set the single key equal to the value (obj[k] = v)
This has infinite depth as long as the depth chain is not broken say [["income:concessions:other",12],["income:concessions", 0]] in this case the latter value will take precedence (Note: this applies to all the answers in one way or anther e.g. the accepted answer the former wins but a value is still lost dues to inaccurate data structure)
repl.it Example

How can i count words frenquency and append results every time i run the script in ruby

["one", "two", "three", "three"]
I want to open a file and write
{"one" => 1, "two" => 1, "three" => 2}
["one", "two"]
and in the next time open the same file and search for the each word if exsit append + 1 else create new word
{"one" => 2, "two" => 2, "three" => 2}
This should do :
hash = ["one", "two", "three", "three"]
frequency_file = 'frequency.dat'
if File.exists?(frequency_file)
old_frequency = File.open(frequency_file) {|f| Marshal.load(f.read)}
else
old_frequency = {}
end
old_frequency.default = 0
frequency = hash.group_by{|name| name}.map{|name, list| [name,list.count+old_frequency[name]]}.to_h
File.open(frequency_file,'w'){|f| f.write(Marshal.dump(frequency))}
puts frequency.inspect
# => {"one"=>1, "two"=>1, "three"=>2}
# => {"one"=>2, "two"=>2, "three"=>4}
If you prefer a human-readable file :
require 'yaml'
hash = ["one", "two", "three", "three"]
frequency_file = 'frequency.yml'
if File.exists?(frequency_file)
old_frequency = YAML.load_file(frequency_file)
else
old_frequency = {}
end
old_frequency.default = 0
frequency = hash.group_by{|name| name}.map{|name, list| [name,list.count+old_frequency[name]]}.to_h
File.open(frequency_file,'w'){|f| f.write frequency.to_yaml}
puts frequency.inspect
# => {"one"=>1, "two"=>1, "three"=>2}
# => {"one"=>2, "two"=>2, "three"=>4}
Here are some variations that'd do it:
ary = %w[a b a c a b]
ary.group_by { |v| v }.map{ |k, v| [k, v.size] }.to_h # => {"a"=>3, "b"=>2, "c"=>1}
ary.each_with_object(Hash.new(0)) { |v, h| h[v] += 1} # => {"a"=>3, "b"=>2, "c"=>1}
ary.uniq.map { |v| [v, ary.count(v)] }.to_h # => {"a"=>3, "b"=>2, "c"=>1}
Since they're all about the same length it becomes important to know which is the fastest.
require 'fruity'
ary = %w[a b a c a b] * 1000
compare do
group_by { ary.group_by { |v| v }.map{ |k, v| [k, v.size] }.to_h }
each_with_object { ary.each_with_object(Hash.new(0)) { |v, h| h[v] += 1} }
uniq_map { ary.uniq.map { |v| [v, ary.count(v)] }.to_h }
end
# >> Running each test 4 times. Test will take about 1 second.
# >> group_by is faster than uniq_map by 30.000000000000004% ± 10.0%
# >> uniq_map is faster than each_with_object by 19.999999999999996% ± 10.0%
How to persist the data and append to it is a separate question and how to do it depends on the size of the data you're checking, and how fast you need the code to run. Databases are very capable of doing these sort of checks extremely fast as they have code optimized to search and count unique occurrences of records. Even SQLite should have no problem doing this. Using an ORM like Sequel or ActiveRecord would make it painless to talk to the DB and to scale or port to a more capable database manager if needed.
Writing to a local file is OK if you occasionally need to update, or you don't have a big list of words, and you don't need to share the information with other pieces of code or with another machine.
Reading a file to recover the hash then incrementing it assumes a word will never be deleted, they'll only be added. I've written a lot of document analysis code and that case hasn't occurred, so I'd recommend thinking about long-term use before settling on your particular path.
Could you put the string representation of a hash (the first line of the file) in a separate (e.g., JSON) file? If so, consider something like the following.
First let's create a JSON file for the hash and a second file, the words of which are to be counted.
require 'json'
FName = "lucy"
JSON_Fname = "hash_counter.json"
File.write(JSON_Fname, JSON.generate({"one" => 1, "two" => 1, "three" => 2}))
#=> 27
File.write(FName, '["one", "two", "three", "three"]')
#=>32
First read the JSON file, parse the hash and give h a default value of zero.1.
h = JSON.parse(File.read(JSON_Fname))
#=> {"one"=>1, "two"=>1, "three"=>2}
h.default = 0
(See Hash#default=). Then read the other file and update the hash.
File.read(FName).downcase.scan(/[[:alpha:]]+/).each { |w| h[w] += 1 }
h #=> {"one"=>2, "two"=>2, "three"=>4}
Lastly, write the hash h to the JSON file (as I did above).2
1 Ruby expands h[w] += 1 to h[w] = h[w] + 1 before parsing the expression. If h does not have a key w, Hash#[] returns the hash's default value, if it has one. Here h["cat"] #=> 0 since h has no key "cat" and the default has been set to zero. The expression therefore becomes h[w] = 0 + 1 #=> 1. Note that the method on the left of the equality is Hash#[]=, which is why the default value does not apply there.
2 To be safe, write the new JSON string to a temporary file, delete the JSON file, then rename the temporary file to the former JSON file name.

map array with condition

I have array like
strings = ["by_product[]=1", "by_product[]=2", "page=1", "per_page=10", "select[]=current", "select[]=requested", "select[]=original"]
which is array of params from request
Then there is code that generates hash from array
arrays = strings.map do |segment|
k,v = segment.split("=")
[k, v && CGI.unescape(v)]
Hash[arrays]
CUrrent output -
"by_product[]": "2",
"page":"1",
"per_page":"10",
"select[]":"original"
Expected output -
"by_product[]":"1, 2",
"page":"1",
"per_page":"10",
"select[]":"current, requested, original"
The problem is - after split method there are few by_product[] and the last one just overrides any other params, so in result instead of hash with array as value of these params im getting only last one. And i'm not sure how to fix it. Any ideas? Or at least algorithms
So try this:
hash = {}
arrays = strings.map do |segment|
k,v = segment.split("=")
hash[k]||=[]
hash[k] << v
end
output is
1.9.3-p547 :025 > hash
=> {"by_product[]"=>["1", "2"], "page"=>["1"], "per_page"=>["10"], "select[]"=>["current", "requested", "original"]}
or if you want just strings do
arrays = strings.map do |segment|
k,v = segment.split("=")
hash[k].nil? ? hash[k] = v : hash[k] << ", " + v
end
Don't reinvent the wheel, CGI and Rack can already handle query strings.
Assuming your strings array comes from a single query string:
query = "by_product[]=1&by_product[]=2&page=1&per_page=10&select[]=current&select[]=requested&select[]=original"
you can use CGI::parse: (all values as arrays)
require 'cgi'
CGI.parse(query)
#=> {"by_product[]"=>["1", "2"], "page"=>["1"], "per_page"=>["10"], "select[]"=>["current", "requested", "original"]}
or Rack::Utils.parse_query: (arrays where needed)
require 'rack'
Rack::Utils.parse_nested_query(query)
# => {"by_product[]"=>["1", "2"], "page"=>"1", "per_page"=>"10", "select[]"=>["current", "requested", "original"]}
or Rack::Utils.parse_nested_query: (values without [] suffix)
require 'rack'
Rack::Utils.parse_nested_query(query)
# => {"by_product"=>["1", "2"], "page"=>"1", "per_page"=>"10", "select"=>["current", "requested", "original"]}
And if these are parameters for a Rails controller, you can just use params.
this will also work :
strings.inject({}){ |hash, string|
key, value = string.split('=');
hash[key] = (hash[key]|| []) << value;
hash;
}
output :
{"by_product[]"=>["1", "2"], "page"=>["1"], "per_page"=>["10"], "select[]"=>["current", "requested", "original"]}
As simple as that
array.map { |record| record*3 if condition }
record*3 is the resultant operation you wanna do to the array while mapping

Ruby Array conversion best way

What is the best way to achieve the following, I have following array of actions under ABC
ABC:-
ABC:Actions,
ABC:Actions:ADD-DATA,
ABC:Actions:TRANSFER-DATA,
ABC:Actions:EXPORT,
ABC:Actions:PRINT,
ABC:Detail,
ABC:Detail:OVERVIEW,
ABC:Detail:PRODUCT-DETAIL,
ABC:Detail:EVENT-LOG,
ABC:Detail:ORDERS
I want to format this as:
ABC =>{Actions=> [ADD-DATA,TRANSFER-DATA,EXPORT,PRINT], Detail => [Overview, Product-detail, event-log,orders]}
There's probably a ton of ways to do it but here's one:
a = ["ABC:Actions",
"ABC:Actions:ADD-DATA",
"ABC:Actions:TRANSFER-DATA",
"ABC:Actions:EXPORT",
"ABC:Actions:PRINT",
"ABC:Detail",
"ABC:Detail:OVERVIEW",
"ABC:Detail:PRODUCT-DETAIL",
"ABC:Detail:EVENT-LOG",
"ABC:Detail:ORDERS"]
a.map { |action| action.split(":") }.inject({}) do |m, s|
m[s.at(0)] ||= {}
m[s.at(0)][s.at(1)] ||= [] if s.at(1)
m[s.at(0)][s.at(1)] << s.at(2) if s.at(2)
m
end
The map call returns an array where each of the strings in the original array have been split into an array of elements that were separated by :. For example [["ABC","Actions","ADD-DATA"] ... ]
The inject call then builds up a hash by going through each of these "split" arrays. It creates a mapping for the first element, if one doesn't already exist, to an empty hash, e.g. "ABC" => {}. Then it creates a mapping in that hash for the second element, if one doesn't already exist, to an empty array, e.g. "ABC" => { "Detail" => [] }. Then it adds the third element to that array to give something like "ABC" => { "Detail" => ["OVERVIEW"] }. Then it goes onto the next "split" array and adds that to the hash too in the same way.
I will do this as below :
a = ["ABC:Actions",
"ABC:Actions:ADD-DATA",
"ABC:Actions:TRANSFER-DATA",
"ABC:Actions:EXPORT",
"ABC:Actions:PRINT",
"ABC:Detail",
"ABC:Detail:OVERVIEW",
"ABC:Detail:PRODUCT-DETAIL",
"ABC:Detail:EVENT-LOG",
"ABC:Detail:ORDERS"]
m = a.map{|i| i.split(":")[1..-1]}
# => [["Actions"],
# ["Actions", "ADD-DATA"],
# ["Actions", "TRANSFER-DATA"],
# ["Actions", "EXPORT"],
# ["Actions", "PRINT"],
# ["Detail"],
# ["Detail", "OVERVIEW"],
# ["Detail", "PRODUCT-DETAIL"],
# ["Detail", "EVENT-LOG"],
# ["Detail", "ORDERS"]]
m.each_with_object(Hash.new([])){|(i,j),ob| ob[i] = ob[i] + [j] unless j.nil? }
# => {"Actions"=>["ADD-DATA", "TRANSFER-DATA", "EXPORT", "PRINT"],
# "Detail"=>["OVERVIEW", "PRODUCT-DETAIL", "EVENT-LOG", "ORDERS"]}
It was just interesting to do it with group_by :)
a = ['ABC:Actions',
'ABC:Actions:ADD-DATA',
'ABC:Actions:TRANSFER-DATA',
'ABC:Actions:EXPORT',
'ABC:Actions:PRINT',
'ABC:Detail',
'ABC:Detail:OVERVIEW',
'ABC:Detail:PRODUCT-DETAIL',
'ABC:Detail:EVENT-LOG',
'ABC:Detail:ORDERS']
result = a.map { |action| action.split(":") }.group_by(&:shift)
result.each do |k1,v1|
result[k1] = v1.group_by(&:shift)
result[k1].each { |k2,v2| result[k1][k2] = v2.flatten }
end
p result
{"ABC"=>{"Actions"=>["ADD-DATA", "TRANSFER-DATA", "EXPORT", "PRINT"], "Detail"=>["OVERVIEW", "PRODUCT-DETAIL", "EVENT-LOG", "ORDERS"]}}

There has got to be a cleaner way to do this

I have this code here and it works but there has to be a better way.....i need two arrays that look like this
[
{
"Vector Arena - Auckland Central, New Zealand" => {
"2010-10-10" => [
"Enter Sandman",
"Unforgiven",
"And justice for all"
]
}
},
{
"Brisbane Entertainment Centre - Brisbane Qld, Austr..." => {
"2010-10-11" => [
"Enter Sandman"
]
}
}
]
one for the past and one for the upcoming...the problem i have is i am repeating myself and though it works i want to clean it up ...here is my data
..
Try this:
h = Hash.new {|h1, k1| h1[k1] = Hash.new{|h2, k2| h2[k2] = []}}
result, today = [ h, h.dup], Date.today
Request.find_all_by_artist("Metallica",
:select => "DISTINCT venue, showdate, LOWER(song) AS song"
).each do |req|
idx = req.showdate < today ? 0 : 1
result[idx][req.venue][req.showdate] << req.song.titlecase
end
Note 1
In the first line I am initializing an hash of hashes. The outer hash creates the inner hash when a non existent key is accessed. An excerpt from Ruby Hash documentation:
If this hash is subsequently accessed by a key that doesn‘t correspond to a hash
entry, the block will be called with the hash object and the key, and should
return the default value. It is the block‘s responsibility to store the value in
the hash if required.
The inner hash creates and empty array when the non existent date is accessed.
E.g: Construct an hash containing of content as values and date as keys:
Without a default block:
h = {}
list.each do |data|
h[data.date] = [] unless h[data.date]
h[data.date] << data.content
end
With a default block
h = Hash.new{|h, k| h[k] = []}
list.each do |data|
h[data.date] << data.content
end
Second line simply creates an array with two items to hold the past and future data. Since both past and the present stores the data as Hash of Hash of Array, I simply duplicate the value.
Second line can also be written as
result = [ h, h.dup]
today = Date.today

Resources