Ruby - how to add an array to a hash key? - ruby-on-rails

I have this loop:
car_data = Hash.new
Car.all.each do |c|
car_data[c.brand] = c.id
car_data['NEW'] << c.id if c.new == 1
end
I have this snipper and trying to save all the new cars to car_data['NEW'], but this code keeps only one item in the hash (there should be 8).
I also tried to define that car_data['NEW'] as an array:
car_data = Hash.new
car_data['NEW'] = Hash.new
Car.all.each do |c|
car_data[c.brand] = c.id
car_data['NEW'] << c.id if c.new == 1
end
But the result was the same - just one item.
How do I save the whole array to the hash key element?
Thank you.

car_data['NEW'] has to be declared as Array.
car_data = Hash.new
car_data['NEW'] = []
Car.all.each do |c|
car_data[c.brand] = c.id
car_data['NEW'] << c.id if c.new == 1
end
You can also do it in a single step
car_data = { new: [] }
Car.all.each do |c|
car_data[c.brand] = c.id
car_data[:new] << c.id if c.new == 1
end
Frankly, it seems a little bit odd to me to use a Hash in that way. In particular, mixin different kind of information in Hash is a very bad approach inherited from other non object oriented languages.
I'd use at least two separate variables. But I don't know enough about the context to provide a meaningful example.

You wrote, that you tried to define (initialize) car_data['NEW'] as an array, but what you did is... initialized it as a hash.
Change:
car_data['NEW'] = Hash.new
To:
car_data['NEW'] = []
The full code would look like:
car_data = Hash.new
car_data['NEW'] = []
Car.all.each do |c|
car_data[c.brand] = c.id
car_data['NEW'] << c.id if c.new == 1
end

car_data = Car.all.each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, memo|
memo[c.brand] = c.id
memo['NEW'] << c.id if c.new == 1
end
or, simpler, let’s create it on the fly if needed:
car_data = Car.all.each_with_object({}) do |c, memo|
memo[c.brand] = c.id
(memo['NEW'] ||= []) << c.id if c.new == 1
end
Please also refer to comment by #tadman below, if the NEW key is to be existing in any case.

Related

Ruby hash path to each leaf

First of all I beg your pardon if this question already exists, I deeply searched for a solution here but I've been able to find it, nevertheless I feel it's a problem so common that is seems so strange to not find anything here...
My struggle is the following: given an hash, I need to return all the PATHS to each leaf as an array of strings; so, for example:
{:a=> 1} gives ['a']
{:a=>{:b=>3, :c=>4} returns an array with two results: ["a.b", "a.c"]
{:a=>[1, {:b=>2}]} will result in ["a.0", "a.1.b"]
and so on...
I have found only partial solutions to this and with dozens of codelines. like this
def pathify
self.keys.inject([]) do |acc, element|
return acc if element.blank?
if !(element.is_a?(Hash) || element.is_a?(Array))
if acc.last.is_a?(Array)
acc[acc.size-1] = acc.last.join('.')
else
acc << element.to_s
end
end
if element.is_a?(Hash)
element.keys.each do |key|
if acc.last.is_a?(Array)
acc.last << key.to_s
else
acc << [key.to_s]
end
element[key].pathify
end
end
if element.is_a?(Array)
acc << element.map(&:pathify)
end
acc
end
end
But it does not work in all cases and is extremely inefficient. Summarizing: is there any way to "pathify" an hash to return all the paths to each leaf in form of array of strings?
Thank you for the help!
Edited
Adding some specs
for {} it returns []
for {:a=>1} it returns ["a"]
for {:a=>1, :b=>1} it returns ["a", "b"]
for {:a=>{:b=>1}} it returns ["a.b"] (FAILED - 1) got: ["a"]
for {:a=>{:b=>1, :c=>2}} it returns ["a.b", "a.c"] (FAILED - 2) got: ["a"]
for {:a=>[1]} it returns ["a.0"] (FAILED - 3) got: ["a"]
for {:a=>[1, "b"]} it returns ["a.0", "a.1"] (FAILED - 4) got: ["a"]
def show(key, path)
if path.is_a? Array
path.map {|p| "#{key}.#{p}"}
else
path == "" ? key.to_s : "#{key}.#{path}"
end
end
def pathify(input)
if input.is_a? Hash
input.map do |k,v|
sub_path = pathify(v)
show(k, sub_path)
end.flatten
elsif input.is_a? Array
input.map.with_index do |v, i|
sub_path = pathify(v)
show(i, sub_path)
end.flatten
else
""
end
end
def leaf_paths(enum)
return unless [Hash, Array].include? enum.class
[].tap do |result|
if enum.is_a?(Hash)
enum.each { |k, v| result = attach_leaf_paths(k, v, result) }
elsif enum.is_a?(Array)
enum.each_with_index { |elem, index| result = attach_leaf_paths(index, elem, result) }
end
end
end
def attach_leaf_paths(key, value, result)
if (children = leaf_paths(value))
children.each { |child| result << "#{key}.#{child}" }
else
result << key.to_s
end
result
end
This is very similar to https://github.com/wteuber/yaml_normalizer/blob/b85dca7357df00757c471acb5dadb79a53dd27c1/lib/yaml_normalizer/ext/namespaced.rb
So I tweaked the code a bit to fit your needs:
module Leafs
def leafs(namespace = [], tree = {})
each do |key, value|
child_ns = namespace.dup << key
if value.instance_of?(Hash)
value.extend(Leafs).leafs child_ns, tree
elsif value.instance_of?(Array)
value.each.with_index.inject({}) {|h, (v,k)| h[k]=v; h}.extend(Leafs).leafs child_ns, tree
else
tree[child_ns.join('.')] = value
end
end
tree.keys.to_a
end
end
Here is how to use it:
h = {a: [1, "b"], c: {d:1}}
h.extend(Leafs)
h.leafs
# => ["a.0", "a.1", "c.d"]
I hope you find this helpful.
def pathify(what)
paths = []
if what.is_a?(Array)
what.each_with_index do | element, index |
paths+= pathify(element).map{|e| index.to_s + '.' + e.to_s}
end
elsif what.is_a?(Hash)
what.each do |k,v|
paths+= pathify(v).map{|e| k.to_s + '.' + e.to_s}
end
else
paths.append('')
end
paths.map{|e| e.delete_suffix('.')}
end

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

ruby (w/rails) group_by, then group_by again

Is there a better way to do the following?
prices = Price...all.group_by(&:foreign_key_id)
#prices = Hash.new
prices.each {|k, v| #prices[k] = Hash.new if !#prices[k]; #prices[k] = v.group_by {|g| g.created_at.to_time.to_i } }
I'd like to do something like
prices.each {|k,v| v = v.group_by(&:created_at) }
But this doesn't seem to work because v is an array and group_by produces a hash. Perhaps there's a way to do this with inject?
prices.inject(Hash.new{ |h, k| h[k] = {} }) { |h, (k, v)|
h[k] = v.group_by(&:created_at)
h
}
Added: Less function way:
prices.keys.each { |k|
prices[k] = prices[k].group_by(&:created_at)
}
This should work:
prices = Price...all.inject({}) do |hash, el|
(hash[el.foreign_key_id] ||={})[el.created_at] << el
hash
end
but I doubt that you will have two records with the same created_at value.
You may need set format for created_at.
Something like this:
prices = Price...all.inject({}) do |hash, el|
(hash[el.foreign_key_id] ||={})[el.created_at.to_s(:db)] << el
hash
end
may be
prices.each {|k,v| prices[k] = v.group_by(&:created_at) }
or
new_prices = {}
prices.each {|k,v| new_prices[k] = v.group_by(&:created_at) }
Hash doesn't support inject
I understand the second group_by, but the first one I think it will be a Model logic, so, depend on your type of relation you can use in your model the options: :uniq => true or :group => 'foreign_key_id' . You can read it here http://guides.rubyonrails.org/association_basics.html

Creating a new hash with default keys

I want to create a hash with an index that comes from an array.
ary = ["a", "b", "c"]
h = Hash.new(ary.each{|a| h[a] = 0})
My goal is to start with a hash like this:
h = {"a"=>0, "b"=>0, "c"=>0}
so that later when the hash has changed I can reset it with h.default
Unfortunately the way I'm setting up the hash is not working... any ideas?
You should instantiate your hash h first, and then fill it with the contents of the array:
h = {}
ary = ["a", "b", "c"]
ary.each{|a| h[a] = 0}
Use the default value feature for the hash
h = Hash.new(0)
h["a"] # => 0
In this approach, the key is not set.
h.key?("a") # => false
Other approach is to set the missing key when accessed.
h = Hash.new {|h, k| h[k] = 0}
h["a"] # => 0
h.key?("a") # => true
Even in this approach, the operations like key? will fail if you haven't accessed the key before.
h.key?("b") # => false
h["b"] # => 0
h.key?("b") # => true
You can always resort to brute force, which has the least boundary conditions.
h = Hash.new.tap {|h| ["a", "b", "c"].each{|k| h[k] = 0}}
h.key?("b") # => true
h["b"] # => 0
You can do it like this where you expand a list into zero-initialized values:
list = %w[ a b c ]
hash = Hash[list.collect { |i| [ i, 0 ] }]
You can also make a Hash that simply has a default value of 0 for any given key:
hash = Hash.new { |h, k| h[k] = 0 }
Any new key referenced will be pre-initialized to the default value and this will avoid having to initialize the whole hash.
This may not be the most efficient way, but I always appreciate one-liners that reveal a little more about Ruby's versatility:
h = Hash[['a', 'b', 'c'].collect { |v| [v, 0] }]
Or another one-liner that does the same thing:
h = ['a', 'b', 'c'].inject({}) {|h, v| h[v] = 0; h }
By the way, from a performance standpoint, the one-liners run about 80% of the speed of:
h = {}
ary = ['a','b','c']
ary.each { |a| h[a]=0 }
Rails 6 added index_with on Enumerable module. This will help in creating a hash from an enumerator with default or fetched values.
ary = %w[a b c]
hash = ary.index_with(0) # => {"a"=>0, "b"=>0, "c"=>0}
Another option is to use the Enum#inject method which I'm a fan of for its cleanliness. I haven't benchmarked it compared to the other options though.
h = ary.inject({}) {|hash, key| hash[key] = 0; hash}
Alternate way of having a hash with the keys actually added
Hash[[:a, :b, :c].zip([])] # => {:a=>nil, :b=>nil, :c=>nil}
Hash[[:a, :b, :c].zip(Array.new(3, 0))] # => {:a=>0, :b=>0, :c=>0}

Merging structs with same user id and then sorting based on attributes

I have any array of structs. Each struct in the array has the following attributes:
user_id
num_hot_dogs_eaten
date_last_pigged_out
Here's what I want to do:
Find the structs with matching user_id's, and merge them into one struct record where num_hot_dogs_eaten is the sum of all matching records and date_last_pigged_out is the most-recent date the user pigged out.
Sort the array of structs by num_hot_dogs_eaten (first order of priority) and by date_last_pigged_out (second order of priority...most-recent first).
Return a new sorted array of structs.
Use this:
def f(users)
r = []
users.each do |u|
new_match = false
match = r.find {|x| x.user_id == u.user_id }
unless match
match = u.dup
r << match
new_match = true
end
match.num_hot_dogs_eaten += u.num_hot_dogs_eaten unless new_match
match.date_last_pigged_out =
[match, u].max_by(&:date_last_pigged_out).date_last_pigged_out
end
r.sort_by {|u| [u.num_hot_dogs_eaten, u.date_last_pigged_out] }.
reverse
end
A more functional programming approach:
User = Struct.new(:user_id, :num_hot_dogs_eaten, :date_last_pigged_out)
ONE_DAY = 60 * 60 * 24
class Object
def returning(object)
yield object
object
end
end
users = [
User.new(1, 3, Time.now),
User.new(1, 2, Time.now + ONE_DAY),
User.new(1, 1, Time.now - ONE_DAY),
User.new(2, 2, Time.now - ONE_DAY),
User.new(2, 3, Time.now),
User.new(3, 5, Time.now - ONE_DAY),
]
users.inject(Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = [] } }) do |collection, user|
returning(collection) do
collection[user.user_id][:num_hot_dogs_eaten] << user.num_hot_dogs_eaten
collection[user.user_id][:date_last_pigged_out] << user.date_last_pigged_out
end
end.map do |user_id, stats|
User.new(user_id, stats[:num_hot_dogs_eaten].inject(&:+), stats[:date_last_pigged_out].max)
end.sort_by { |user| [user.num_hot_dogs_eaten, user.date_last_pigged_out] }.reverse
The actual implementation is (assuming you have returning defined):
users.inject(Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = [] } }) do |collection, user|
returning(collection) do
collection[user.user_id][:num_hot_dogs_eaten] << user.num_hot_dogs_eaten
collection[user.user_id][:date_last_pigged_out] << user.date_last_pigged_out
end
end.map do |user_id, stats|
User.new(user_id, stats[:num_hot_dogs_eaten].inject(&:+), stats[:date_last_pigged_out].max)
end.sort_by { |user| [user.num_hot_dogs_eaten, user.date_last_pigged_out] }.reverse

Resources