dynamically create a hash in ruby - ruby-on-rails

I am sure this is a question that has been asked before, but my research still hasn't led me to any conclusive answer. I am trying to create a standard key value pair in a loop.
The code below iterates as expected, but it is only storing the last value. I understand why, because I am overwriting the delivery_hash variable each time by use of the =. I tried using the shovel operator that I have used for arrays, but that didn't work.
def calculate_job_delivery_costs
delivery_hash = {}
DELIVERY_COST_SCOPES.each do |scope|
delivery_hash = { scope => job_delivery_costs.send(scope).inject(0) { |total, item| (total + item.cost_per_unit * item.hour_count) * item.quantity } }
end
delivery_hash
end
my desired output is simply like this
"foo" => 234.32, 'bah' => 345.76, 'baz' => 33.87

Well, one thing is just stop blowing away the variable, and use the hash you created in the first place:
def calculate_job_delivery_costs
delivery_hash = {}
DELIVERY_COST_SCOPES.each do |scope|
delivery_hash[scope] = job_delivery_costs.send(scope).inject(0) do |total, item|
(total + item.cost_per_unit * item.hour_count) * item.quantity
end
end
delivery_hash
end
Or, simpler, just build the hash from the individual results:
def calculate_job_delivery_costs
delivery_hashes = DELIVERY_COST_SCOPES.map do |scope|
cost = job_delivery_costs.send(scope).inject(0) do |total, item|
(total + item.cost_per_unit * item.hour_count) * item.quantity
end
[scope, cost]
end
Hash[delivery_hashes]
end

try with merge
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h1.merge(h2) #=> {"a"=>100, "b"=>254, "c"=>300}
according to the ruby documentation: http://www.ruby-doc.org/core-2.1.2/Hash.html#method-i-merge

Related

Refactor if-else with a logic of exists condition check

I have this two methods I want to refactor
def construct_discount_hash(product_adjustments)
discounts = {
events: {},
subjects: {},
products: {}
}
# product_adjustments is a model
# This is a problem from legacy database structure where event_id, subject_id, product_id is in their own column
product_adjustments.each do |adjustment|
if (adjustment.event_id.present?)
discounts[:events][adjustment.event_id] = {'$' => adjustment.amount, '%' => adjustment.percentage}
end
if (adjustment.subject_id.present?)
discounts[:subjects][adjustment.subject_id] = {'$' => adjustment.amount, '%' => adjustment.percentage}
end
if (adjustment.product_id.present?)
discounts[:products][adjustment.product_id] = {'$' => adjustment.amount, '%' => adjustment.percentage}
end
end
discounts
end
and I will use above method results within below method
# discounts is a hash generated from above code, item is a rails model
def calculate_order_content_price(discounts, item)
product = item.product
if (item.product_variant.present?)
price = item.product_variant.price
else
price = product.price
end
price_adjustments = {}
popped_from = []
if (discounts[:products][item.product_id])
price_adjustments = discounts[:products][item.product_id]
discounts[:products].delete(item.product_id)
popped_from = [:products, item.product_id]
elsif (discounts[:subjects][product.subject_id])
price_adjustments = discounts[:subjects][product.subject_id]
discounts[:subjects].delete(product.subject_id)
popped_from = [:subjects, product.subject_id]
elsif (discounts[:events][product.event_id])
price_adjustments = discounts[:events][product.event_id]
discounts[:events].delete(product.event_id)
popped_from = [:events, product.event_id]
end
if (adjustment = price_adjustments['$'])
adjusted_price = price + adjustment
elsif (adjustment = price_adjustments['%'])
adjusted_price = price + price * (adjustment / 100.0)
discounts[popped_from[0]][popped_from[1]] = price_adjustments
else
adjusted_price = price
end
{ price: adjusted_price, discount: (price - adjusted_price) }
end
I know from above code there's a lot of code-smell there. Firstly I'm thinking that the if-else logic can be refactored somehow. Can someone give an advice a technique that I can use to refactor that if-else condition? I'm confused because of the if-condition is checking for the existences of the value.
Any suggestions will be helpful.
I've tried to simplify a little bit your construct_discount_hash with two helpers:
def amount_and_percentage(adjustment, model)
{ adjustment.attributes["#{model}_id"] => { '$': adjustment.amount, '%': adjustment.percentage } }
end
def construct_discount_hash(product_adjustments)
product_adjustments.each_with_object({}) do |adjustment, hash|
case
when adjustment.event_id.present?
hash[:event] = amount_and_percentage(adjustment, 'event')
when adjustment.subject_id.present?
hash[:subject] = amount_and_percentage(adjustment, 'subject')
when adjustment.product_id.present?
hash[:product] = amount_and_percentage(adjustment, 'product')
end
end
end
A refacto for you.
def construct_discount_hash(product_adjustments)
# product_adjustments is a model
# This is a problem from legacy database structure where event_id, subject_id, product_id is in their own column
product_adjustments.each do |adjustment|
adjustement_hash = {'$' => adjustment.amount, '%' => adjustment.percentage} if adjustement
(discounts ||= {})[:events][adjustment.event_id] = adjustement_hash if adjustment.event_id.present?
(discounts ||= {})[:subjects][adjustment.subject_id] = adjustement_hash if adjustment.subject_id.present?
(discounts ||= {})[:products][adjustment.product_id] = adjustement_hash if adjustment.product_id.present?
end
discounts
end
I hope it will help you. Have fun.

Counting several elements inside an array

I just wrote a method that I'm pretty sure is terribly written. I can't figure out if there is a better way to write this in ruby. It's just a simple loop that is counting stuff.
Of course, I could use a select or something like that, but that would require looping twice on my array. Is there a way to increment several variables by looping without declaring the field before the loop? Something like a multiple select, I don't know. It's even worst when I have more counters.
Thank you!
failed_tests = 0
passed_tests = 0
tests.each do |test|
case test.status
when :failed
failed_tests += 1
when :passed
passed_tests +=1
end
end
You could do something clever like this:
tests.each_with_object(failed: 0, passed: 0) do |test, memo|
memo[test.status] += 1
end
# => { failed: 1, passed: 10 }
You can use the #reduce method:
failed, passed = tests.reduce([0, 0]) do |(failed, passed), test|
case test.status
when :failed
[failed + 1, passed]
when :passed
[failed, passed + 1]
else
[failed, passed]
end
end
Or with a Hash with default value, this will work with any statuses:
tests.reduce(Hash.new(0)) do |counter, test|
counter[test.status] += 1
counter
end
Or even enhancing this with #fivedigit's idea:
tests.each_with_object(Hash.new(0)) do |test, counter|
counter[test.status] += 1
end
Assuming Rails 4 ( using 4.0.x here). I would suggest:
tests.group(:status).count
# -> {'passed' => 2, 'failed' => 34, 'anyotherstatus' => 12}
This will group all records by any possible :status value, and count each individual ocurrence.
Edit: adding a Rails-free approach
Hash[tests.group_by(&:status).map{|k,v| [k,v.size]}]
Group by each element's value.
Map the grouping to an array of [value, counter] pairs.
Turn the array of paris into key-values within a Hash, i.e. accessible via result[1]=2 ....
hash = test.reduce(Hash.new(0)) { |hash,element| hash[element.status] += 1; hash }
this will return a hash with the count of the elements.
ex:
class Test
attr_reader :status
def initialize
#status = ['pass', 'failed'].sample
end
end
array = []
5.times { array.push Test.new }
hash = array.reduce(Hash.new(0)) { |hash,element| hash[element.status] += 1; hash }
=> {"failed"=>3, "pass"=>2}
res_array = tests.map{|test| test.status}
failed_tests = res_array.count :failed
passed_tests = res_array.count :passed

constructing a new hash from the given values

I seem lost trying to achieve the following, I tried all day please help
I HAVE
h = {
"kv1001"=> {
"impressions"=>{"b"=>0.245, "a"=>0.754},
"visitors" =>{"b"=>0.288, "a"=>0.711},
"ctr" =>{"b"=>0.003, "a"=>0.003},
"inScreen"=>{"b"=>3.95, "a"=>5.031}
},
"kv1002"=> {
"impressions"=>{"c"=>0.930, "d"=>0.035, "a"=>0.004, "b"=>0.019,"e"=>0.010},
"visitors"=>{"c"=>0.905, "d"=>0.048, "a"=>0.005, "b"=>0.026, "e"=>0.013},
"ctr"=>{"c"=>0.003, "d"=>0.006, "a"=>0.004, "b"=>0.003, "e"=>0.005},
"inScreen"=>{"c"=>4.731, "d"=>4.691, "a"=>5.533, "b"=>6.025, "e"=>5.546}
}
}
MY GOAL
{
"segment"=>"kv1001=a",
"impressions"=>"0.754",
"visitors"=>"0.711",
"inScreen"=>"5.031",
"ctr"=>"0.003"
}, {
"segment"=>"kv1001=b",
"impressions"=>"0.245",
"visitors"=>"0.288",
"inScreen"=>"3.95",
"ctr"=>"0.003"
}, {
"segment"=>"kv1002=a",
"impressions"=>"0.004"
#... etc
}
My goal is to create a hash with 'kv1001=a' i.e the letters inside the hash and assign the keys like impressions, visitors etc. The example MY GOAL has the format
So format type "kv1001=a" must be constructed from the hash itself, a is the letter inside the hash.
I have solved this now
`data_final = []
h.each do |group,val|
a = Array.new(26){{}}
val.values.each_with_index do |v, i|
keys = val.keys
segment_count = v.keys.length
(0..segment_count-1).each do |n|
a0 = {"segment" => "#{group}=#{v.to_a[n][0]}", keys[i] => v.to_a[n][1]}
a[n].merge! a0
if a[n].count > 4
data_final << a[n]
end
end
end
end`
Here's a simpler version
h.flat_map do |segment, attrs|
letters = attrs.values.flat_map(&:keys).uniq
# create a segment entry for each unique letter
letters.map do |letter|
seg = {"segment" => "#{segment}=#{letter}"}
seg.merge Hash[attrs.keys.map {|key| [key,attrs[key][letter]]}]
end
end
Output:
[{"segment"=>"kv1001=b",
"impressions"=>0.245,
"visitors"=>0.288,
"ctr"=>0.003,
"inScreen"=>3.95},
{"segment"=>"kv1001=a",
"impressions"=>0.754,
"visitors"=>0.711,
"ctr"=>0.003,
"inScreen"=>5.031},
{"segment"=>"kv1002=c",
"impressions"=>0.93,
"visitors"=>0.905,
"ctr"=>0.003,
"inScreen"=>4.731},
{"segment"=>"kv1002=d",
"impressions"=>0.035,
"visitors"=>0.048,
"ctr"=>0.006,
"inScreen"=>4.691},
{"segment"=>"kv1002=a",
"impressions"=>0.004,
"visitors"=>0.005,
"ctr"=>0.004,
"inScreen"=>5.533},
{"segment"=>"kv1002=b",
"impressions"=>0.019,
"visitors"=>0.026,
"ctr"=>0.003,
"inScreen"=>6.025},
{"segment"=>"kv1002=e",
"impressions"=>0.01,
"visitors"=>0.013,
"ctr"=>0.005,
"inScreen"=>5.546}]

Ruby - "can't convert Symbol into Integer" when try to access data in array

Here's a sample of array:
{"C1"=>[
{:upc=>"51857195821952", :product_id=>"1234", :name=>"name", :price=>" $15 ", :color=>"green", :size=>"L", :description=>"descr"},
{:upc=>"352353wegs", :product_id=>"456", :name=>"name2", :price=>"$21", :color=>"black", :size=>"S", :description=>"descr"}, # ...
],
#...
}
And here as I am trying to fetch data from that array:
#array.each do |p|
product = Product.new
product.sku = p[0]
product.name = p[1][0][:name] #can't convert Symbol into Integer
price = p[1].select{ |pr| !pr[:price].nil? and pr[:price] != "0" }.min_by{ |i| i[:price].to_f }[:price]
product.price = "%.2f" % (price.to_f)
...
end
Every time I try to fetch data from the array, I get on the line product.name = the error can't convert Symbol into Integer.
What is wrong in this case? I spent a part of afternoon on this issue, but unfortunately I still cannot figure out it...
Thanky you
Your #array is actually a hash. It is formated like following:
{
'name1' => [{:upc => "..."},{:upc => "..."}],
'name2' => [{:upc => "..."},{:upc => "..."}],
#...
}
Since it is a Hash, you can use 2 arguments in the each (works for map also) method (one for the key, the other for the value):
#array.each do |name, array|
product = Product.new
product.sku = name # returns "C1"
array.each do |data|
data[:upc]
data[:name]
#etc...
end
end
The fundamental problem is that the sample array you showed above is not actually an array. It's a hash with key-value pairs. Therefore, your code like p[0] or p[1][0] doesn't make sense because a hash doesn't have index like array. Hash is not ordered. Hashes values are accessed with a "key" rather than an "index" like array.
Iterating through key-value pairs of a hash is done something like this.
1.9.3p194 :001 > x = {:x => 10, :y => 9, :z => 10}
=> {:x=>10, :y=>9, :z=>10}
1.9.3p194 :002 > x.each do |key, value|
1.9.3p194 :003 > puts "#{key} : #{value}"
1.9.3p194 :004?> end
x : 10
y : 9
z : 10
=> {:x=>10, :y=>9, :z=>10}
It looks like you may be confusing Arrays and Hashes a bit.
Given this:
#array = {"C1"=>[
{:upc=>"51857195821952", :product_id=>"1234", :name=>"name", :price=>" $15 ", :color=>"green", :size=>"L", :description=>"descr"},
{:upc=>"352353wegs", :product_id=>"456", :name=>"name2", :price=>" $21 ", :color=>"black", :size=>"S", :description=>"descr"}
] }
Then #array.class.name is Hash
You can get the actual array by accessing it like so:
#actual_array = #array["C1"]
Then, #actual_array.class.name will be Array
So, taking this approach and re-writing:
#array = {"C1"=>[
{:upc=>"51857195821952", :product_id=>"1234", :name=>"name", :price=>" $15 ", :color=>"green", :size=>"L", :description=>"descr"},
{:upc=>"352353wegs", :product_id=>"456", :name=>"name2", :price=>" $21 ", :color=>"black", :size=>"S", :description=>"descr"}
] }
#actual_array = #array["C1"]
#actual_array.each do |p|
puts p[:name]
end
If you do this, you'll find that the value of the :name element will be printed neatly out.

Add to class value in ruby on rails data in loop

I have such code:
def accum_search
if params[:akbcap].present?
akbcap_array = [12,18,19,20,25,30,35,36,38,40,41,42,44,45,46,47,50,52,53,54,55,56,58,60,61,62,63,64,65,66,68,69,70,71,72,74,75,77,80,85,88,90,91,92,95,98,100,102,110,115,120,125,130,135,140,170,180,185,190,192,200,210,220,225]
min, max = params[:akbcap].split('-').map {|s| s.to_i }
logger.warn("!#!!!!!!!!!!!! AAA !!!!!!!!!!")
logger.warn(min)
logger.warn(max)
caprange = min...max
sa = akbcap_array.select {|n| caprange.include? n }
##cross = OtherProductsCrossList.find(:all, :conditions => {:cross_value => 1})
cap = "*"+params[:akbcap]+"*"
sa.each do |s|
logger.warn(s)
#accums = Accumulator.by_capacity(s).by_size(params[:akbsize]).by_brand(params[:akbbrand])
end
else
#accums = Accumulator.by_capacity(50).by_size(params[:akbsize]).by_brand(params[:akbbrand])
end
end
As you see i have such part:
sa.each do |s|
logger.warn(s)
#accums = Accumulator.by_capacity(s).by_size(params[:akbsize]).by_brand(params[:akbbrand])
end
but could i add on every iteration in #accums data from search? now it has last value( I could done it via arrays... but how to do via class-variable?
Yes, initiate it before the loop and use the << operator to append. End with flatten to make it a single dimension array.
#accums = []
# ...
sa.each do |s|
#accums << Accumulator.several_method_calls......
end
#accums.flatten!
or for compactness:
result = sa.map{|s| Accumulator.several_method_calls...... }.flatten

Resources