Nesting loop within a block in Ruby - ruby

I have a helper module to generate an array hash data, which is something like:
[{:date => d, :total_amount => 31, :first_category => 1, :second_category => 2,...},
{:date => d+1, :total_amount => 31, :first_category => 1, :second_category => 2,...}]
So I make the method like:
def records_chart_data(category = nil, start = 3.weeks.ago)
total_by_day = Record.total_grouped_by_day(start)
category_sum_by_day = Record.sum_of_category_by_day(start)
(start.to_date..Time.zone.today).map do |date|
{
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0,
Category.find(1).title => category_sum_by_day[0][date].try(:first).try(:total_amount) || 0,
Category.find(2).title => category_sum_by_day[1][date].try(:first).try(:total_amount) || 0,
Category.find(3).title => category_sum_by_day[2][date].try(:first).try(:total_amount) || 0,
}
end
end
Since the Category will always change, I try to use loop in this method like:
def records_chart_data(category = nil, start = 3.weeks.ago)
total_by_day = Record.total_grouped_by_day(start)
category_sum_by_day = Record.sum_of_category_by_day(start)
(start.to_date..Time.zone.today).map do |date|
{
:date => date,
Category.all.each_with_index do |category, index|
category.title => category_sum_by_day[index][date].try(:first).try(:total_amount) || 0,
end
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}
end
end
But ruby alerts me with an error:
/Users/tsu/Code/CashNotes/app/helpers/records_helper.rb:10: syntax error, unexpected tASSOC, expecting keyword_end
category.title => category_sum_by_day[index][d...
Why does it say expecting keyword_end, and how should I fix it?
The method category_sum_by_day it calls looks like:
def self.sum_of_category_by_day(start)
records = where(date: start.beginning_of_day..Time.zone.today)
records = records.group('category_id, date(date)')
records = records.select('category_id, date, sum(amount) as total_amount')
records = records.group_by{ |r| r.category_id }
records.map do |category_id, value|
value.group_by {|r| r.date.to_date}
end
end
Or should I alter this method to generate a more friendly method for the helper above?

Category.all.each_with_index do |category, index|
category.title => category_sum_by_day # ...snip!
end
Unfortunately, this piece of code does not adhere to Ruby's grammar. The problem is the body of the block. x => y is not an expression and the syntax requires bodies of blocks to be expressions.
If you want to generate a hash by one key-value pair at a time try the following combination of Hash::[], Array#flatten and the splat operator (i.e. unary *):
Hash[*5.times.map { |i| [i * 3, - i * i] }.flatten]
As a result I'd rewrite the last expresion of records_chart_data more or less as follows
(start.to_date..Time.zone.today).map do |date|
categories = Hash[*Category.all.each_with_index do |category, index|
[ category.title, category_sum_by_day[...] ]
end .flatten]
{ :date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}.merge categories
end
If you consider it unreadable you can do it in a less sophisticated way, i.e.:
(start.to_date..Time.zone.today).map do |date|
hash = {
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}
Category.all.each_with_index do |category, index|
hash[category.title] = category_sum_by_day[...]
end
hash
end
Another idea is to use Array#reduce and adopt a more functional approach.
(start.to_date..Time.zone.today).map do |date|
Category.all.each_with_index.reduce({
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}) do |hash, (category, index)|
hash.merge category.title => category_sum_by_day[...]
end
hash
end

Related

Ruby, iterating over a multi-value Hash using if/else, trying to return key/value pairs, fails when value is not found

I am trying to scan a string of raw input from a user and return a sentence that's composed of an array of arrays with the (TOKEN, WORD) pairings. If a word isn't part of the lexicon, then it should still return the WORD but set the TOKEN to an error token.
Inside the method "##dictionary.each do |type, list|" the initial if statement works fine at building a key/value array of found words, as long as the else statement is set to return nil. However, when I try and place error/words pairs into the array for the words not contained in the ##dictionary hash (i.e. those that fall into the else part of the code), I receive 5 separate pairs in the array for each word that the user entered, one for each iteration over each key for each word entered.
Does anybody have an idea how to return just one error/value pair to the array, instead of one for each of the five iterations for every word?
class Lexicon
##dictionary = {
'direction' => ['north', 'south', 'east', 'west', 'down', 'up', 'left', 'right', 'back'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
'stop words' => ['the', 'in', 'of', 'on', 'at', 'it'],
'nouns' => ['door', 'bear', 'princess', 'cabinet'],
'numbers' => [0..9]
}
stuff = $stdin.gets.chomp
##words = stuff.split(' ')
def self.scan
result = []
##words.each do |text_element|
categorized = []
##dictionary.each do |type, list|
if
list.include?(text_element.downcase)
categorized = [type, text_element]
result.push(categorized)
else
nil
#categorized = ["ERROR", text_element]
#result.push(categorized)
end
end
end
print result
end
Lexicon.scan
end
It happens because of each iterates over all elements and it is true once or never.
This reduction of your code should help you understand what happen:
dictionary = {
'direction' => ['north', 'south'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
'whathever' => ['blah']
}
text = 'go'
dictionary.each do |type, list|
if p list.include?(text) # added p
then
p text
else
p 'error'
end
end
It returns:
# false
# "error"
# true
# "go"
# false
# "error"
You need a different approach, for example:
text = 'nothing'
result = dictionary.find { |_, v| v.include? text }
result ? [result.keys, text] : "Error"
While it may feel organize to have the dictionary categorized by lists, this would be both simplified and much faster were the dictionary to be flattened and have a default set to the 'ERROR' token.
For example:
##dictionary = {
'direction' => ['north', 'south', 'east', 'west', 'down', 'up', 'left', 'right', 'back'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
...
Becomes this:
##dictionary = {
'north' => 'direction',
'south' => 'direction',
...
'go' => 'verbs',
'stop' => 'verbs',
...
}
##dictionary.default = 'ERROR'
This way, your lookup becomes linear and without unnecessary boolean logic, like so.
def scan
result = stuff.split(' ').map do |word|
[##dictionary[word.downcase], word]
end
print result
end
This has worked for me. Thanks to Sebastian Scholl for the idea of simplifying the dictionary.
class Lexicon
##dictionary = {
'direction' => ['north', 'south', 'east', 'west', 'down', 'up', 'left', 'right', 'back'],
'verbs' => ['go', 'stop', 'kill', 'eat'],
'stop words' => ['the', 'in', 'of', 'on', 'at', 'it'],
'nouns' => ['door', 'bear', 'princess', 'cabinet'],
'numbers' => [0..9]
}
stuff = $stdin.gets.chomp
##words = stuff.downcase.split(' ')
def self.scan
result = []
values = []
##dictionary.each do |key, value|
values << value
end
value_list = values.flatten.uniq
##words.each do |text_element|
if value_list.include?(text_element)
##dictionary.each do |key, value|
if value.include?(text_element)
categorized = [key, text_element]
result.push(categorized)
else
nil
end
end
else
result.push(["Error, #{text_element}"])
end
end
print result
end
Lexicon.scan
end

to_json introduces strange character

With this code I implemented a tree
groups = {"al1o0"=>"A1", "al2o2"=>"A10", "al2o3"=>"A11", "al1o1"=>"A2"}
map = {}
arr = []
groups.each_with_index do |group, index|
level = (group.first.split("o")[0].split("al")[1]).to_i - 1
level = level == 0 ? nil : level
order = group.first.split("o")[1]
arr.append({ :id=> index + 1, :order => order, :name => group.last, :parent => level})
end
root = {:id => 0, :name => '', :order => 0, :parent => nil}
arr.each do |e|
map[e[:id]] = e
end
tree = {}
arr.each do |e|
pid = e[:parent]
if pid == nil
(tree[root] ||= []) << e
else
(tree[map[pid]] ||= []) << e
end
end
tree has
=> {{:id=>0, :name=>"", :order=>0, :parent=>nil}=>[{:id=>1, :order=>"0", :name=>"A1", :parent=>nil}, {:id=>4, :order=>"1", :name=>"A2", :parent=>nil}], {:id=>1, :order=>"0", :name=>"A1", :parent=>nil}=>[{:id=>2, :order=>"2", :name=>"A10", :parent=>1}, {:id=>3, :order=>"3", :name=>"A11", :parent=>1}]}
Up to here all right but If I do tree.to_json, the output is
=> "{\"{:id=\\u003e0, :name=\\u003e\\\"\\\", :order=\\u003e0, :parent=\\u003enil}\":[{\"id\":1,\"order\":\"0\",\"name\":\"A1\",\"parent\":null},{\"id\":4,\"order\":\"1\",\"name\":\"A2\",\"parent\":null}],\"{:id=\\u003e1, :order=\\u003e\\\"0\\\", :name=\\u003e\\\"A1\\\", :parent=\\u003enil}\":[{\"id\":2,\"order\":\"2\",\"name\":\"A10\",\"parent\":1},{\"id\":3,\"order\":\"3\",\"name\":\"A11\",\"parent\":1}]}"
Why It changed :id=>0 in :id=\u003e0?
First of all tree looks weird.
{{:id=>0, :name=>"", :order=>0, :parent=>nil}=>[{:id=>1, :order=>"0", :name=>"A1", :parent=>nil}, ...]}}
here is a key
{:id=>0, :name=>"", :order=>0, :parent=>nil}
and
[{:id=>1, :order=>"0", :name=>"A1", :parent=>nil}, ...]
is a value.
Key should not be a hash. How to call it later then.
You might need something like
{"A1" => {name: 'foo', order: '0' }, 'A2' => ...}

Most performant way to group/summarise two hashes?

I have two hashes with some data that I need to aggregate. The first one is a mapping of which ids (id_1, id_2, id_3, id_4) belong under what category (a, b, c):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
The second hash holds values of how many events happened per id for a given date (date_1, date_2, date_3):
hash_2 = {
'id_1' => {'date_1' => 5, 'date_2' => 6, 'date_3' => 8},
'id_2' => {'date_1' => 0, 'date_3' => 6},
'id_3' => {'date_1' => 0, 'date_2' => nil, 'date_3' => 1},
'id_4' => {'date_1' => 10, 'date_2' => 1}
}
What I want is to get the total event per category (a,b,c). For the above example, the result would look something like:
hash_3 = {'a' => (5+6+8+0+6), 'b' => (0+0+1), 'c' => (10+1)}
My problem is, that there are about 5000 categories, each pointing to typically 1 to 3 ids, and each ID having event counts for 30 dates or more. So this takes quite a bit of computation. What will be the most performant (time effective) way to do this grouping in Ruby?
update
This is what I tried so far (took like 6-8 seconds!, horribly slow):
def total_clicks_per_category
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
end
def total_event_per_ids(ids)
ids.reduce(0) do |memo, id|
events = hash_2.fetch(id, {})
memo + (events.values.reduce(:+) || 0)
end
end
P.S. I’m using Ruby 2.3.
I'm writing this on a phone so I cannot test right now, but it looks OK.
g = hash_2.each_with_object({}) { |(k,v),g| g[k] = v.values.compact.sum }
hash_3 = hash_1.each_with_object({}) { |(k,v),h| h[k] = g.values_at(*v).sum }
First, create an intermediate hash that holds the sum of hash_2:
hash_4 = hash_2.map{|k, v| [k, v.values.inject(:+)]}.to_h
# => {"id_1"=>19, "id_2"=>6, "id_3"=>1, "id_4"=>11}
Then do the final summation:
hash_3 = hash_1.map{|k, v| [k, v.map{|k| hash_4[k]}.inject(:+)]}.to_h
# => {"a"=>25, "b"=>1, "c"=>11}
Theory
5000*3*30 isn't that many. Ruby probably will need a second at most for this kind of job.
Hash lookup is fast by default, you won't be able to optimize much.
You could pre-calculate hash_2_sum, though :
hash_2_sum = {
'id_1' => 5+6+8,
'id_2' => 0+6,
'id_3' => 0+0+1,
'id_4' => 10+1
}
A loop on hash1 with hash_2_sum lookup, and you're done.
Code
Your example has been updated with some nil values. You need to remove them with compact, and make sure the sum is 0 when no element is found with inject(0, :+):
hash_1 = {'a' => ['id_1','id_2'], 'b' => ['id_3'], 'c' => ['id_4']}
hash_2 = {
'id_1' => { 'date_1' => 5, 'date_2' => 6, 'date_3' => 8 },
'id_2' => { 'date_1' => 0, 'date_3' => 6 },
'id_3' => { 'date_1' => 0, 'date_2' => nil, 'date_3' => 1 },
'id_4' => { 'date_1' => 10, 'date_2' => 1 }
}
hash_2_sum = hash_2.each_with_object({}) do |(key, dates), sum|
sum[key] = dates.values.compact.inject(0, :+)
end
hash_3 = hash_1.each_with_object({}) do |(key, ids), sum|
sum[key] = hash_2_sum.values_at(*ids).inject(0, :+)
end
# {"a"=>25, "b"=>1, "c"=>11}
Note
{}.tap do |res|
hash_1.each do |cat, ids|
res[cat] = total_event_per_ids(ids)
end
end
isn't very readable IMHO.
You can either use each_with_object or Array#to_h :
result = [1, 2, 3].each_with_object({}) do |i, hash|
hash[i] = i * i
end
#=> {1=>1, 2=>4, 3=>9}
result = [1, 2, 3].map { |i| [i, i * i] }.to_h
#=> {1=>1, 2=>4, 3=>9}

Access to merged cells using Ruby-Roo

According to example below: Value is stored only in A1, other cells return nil.
How is possible to get the A1'a value from the others merged cells, or simply check range of the A1 cell?
here is my take, if all merged fields are same as prev - then non-merged fields should become array
xlsx = Roo::Excelx.new(__dir__ + "/output.xlsx", { expand_merged_ranges: true })
parsed = xlsx.sheet(0).parse(headers: true).drop(1)
parsed_merged = []
.tap do |parsed_merged|
parsed.each do |x|
if parsed_merged.empty?
parsed_merged << {
"field_non_merged1" => x["field_non_merged1"],
"field_merged1" => [x["field_merged1"]],
"field_merged2" => [x["field_merged2"]],
"field_merged3" => [x["field_merged3"]],
"field_merged4" => [x["field_merged4"]],
"field_non_merged2" => x["field_non_merged2"],
"field_non_merged3" => x["field_non_merged3"],
}
else
field_merged1_is_same_as_prev = x["field_non_merged1"] == parsed_merged.last["field_non_merged1"]
field_merged2_is_same_as_prev = x["field_non_merged2"] == parsed_merged.last["field_non_merged2"]
field_merged3_is_same_as_prev = x["field_non_merged3"] == parsed_merged.last["field_non_merged3"]
merged_rows_are_all_same_as_prev = field_non_merged1_is_same_as_prev && field_merged2_is_same_as_prev && field_merged3_is_same_as_prev
if merged_rows_are_all_same_as_prev
parsed_merged.last["field_merged1"].push x["field_merged1"]
parsed_merged.last["field_merged2"].push x["field_merged2"]
parsed_merged.last["field_merged3"].push x["field_merged3"]
parsed_merged.last["field_merged4"].push x["field_merged4"]
else
parsed_merged << {
"field_non_merged1" => x["field_non_merged1"],
"field_merged1" => [x["field_merged1"]],
"field_merged2" => [x["field_merged2"]],
"field_merged3" => [x["field_merged3"]],
"field_merged4" => [x["field_merged4"]],
"field_non_merged2" => x["field_non_merged2"],
"field_non_merged3" => x["field_non_merged3"],
}
end
end
end
end
.map do |x|
{
"field_non_merged1" => x["field_non_merged1"],
"field_merged1" => x["field_merged1"].compact.uniq,
"field_merged2" => x["field_merged2"].compact.uniq,
"field_merged3" => x["field_merged3"].compact.uniq,
"field_merged4" => x["field_merged4"].compact.uniq,
"field_non_merged2" => x["field_non_merged2"],
"field_non_merged3" => x["field_non_merged3"],
}
end
This is not possible without first assigning the value to all the cells of the range, even in Excel VBA this is the case.
See this sample
require 'axlsx'
p = Axlsx::Package.new
wb = p.workbook
wb.add_worksheet(:name => "Basic Worksheet") do |sheet|
sheet.add_row ["Val", nil]
sheet.add_row [nil, nil]
merged = sheet.merge_cells('A1:B2')
p sheet.rows[0].cells[0].value # "Val"
p sheet.rows[0].cells[1].value # nil
sheet[*merged].each{|cell|cell.value = sheet[*merged].first.value}
p sheet.rows[0].cells[0].value # "Val"
p sheet.rows[0].cells[1].value # "Val"
end
p.serialize('./simple.xlsx')
Please add a sample yourself next time so that we see which gem you used, which code, error etc.

Check for `nil` and set if it's `try` in a hash

I want:
{
"CATTLE" => {"Heifers" => 647, "Cows" => 633, "Weaners" => 662, "Steers" => 653},
"BULL" => {"Bulls" => 196},
"SHEEP" => {"Rams" => 410, "Ewes" => 1629, "Wethers" => 1579, "Calves" => 1241, "Weaners" => 300}
}
To get that, I start with an empty mobs = {} hash, and then populate it as I loop. If the key is nil, I set it and then populate it. I was wondering if there was a nicer way to do as below:
mob_livestock_group_response.each do |livestock_group|
mobs[livestock_group['assetType']] = {} unless mobs[livestock_group['assetType']]
mobs[livestock_group['assetType']][livestock_group['subtype']] = 0 unless mobs[livestock_group['assetType']][livestock_group['subtype']]
mobs[livestock_group['assetType']][livestock_group['subtype']] += livestock_group['size']
end
You could write:
mob_livestock_group_response.each do |livestock_group|
mobs[livestock_group['assetType']] ||= {}
mobs[livestock_group['assetType']][livestock_group['subtype']] ||= 0
mobs[livestock_group['assetType']][livestock_group['subtype']] += livestock_group['size']
end
Furthermore I would write this like this:
mob_livestock_group_response.each do |livestock_group|
type = livestock_group['assetType']
sub = livestock_group['subtype']
size = livestock_group['size']
mobs[type] ||= {}
mobs[type][sub] ||= 0
mobs[type][sub] += size
end

Resources