combine keys in array of hashes - ruby-on-rails

I map results of my query to create an array of hashes grouped by organisation_id like so:
results.map do |i|
{
i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}
}
}
a capability is defined outside the map.
The result looks like:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4}}}, {1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{12=>5}}}, {2 => {...}}...]
For every organisation_id there is a separate entry in the array. I would like to merge these hashes and combine the scores key as so:
[{1=>{:name=>"cap1", :tags=>["tag A"], :scores=>{26=>4, 12=>5}}}, {2=>{...}}... ]
EDIT
To create the results I use the following AR:
Valuation.joins(:membership)
.where(capability: capability)
.select("valuations.id, valuations.score, valuations.capability_id, valuations.membership_id, memberships.location_id, memberships.organisation_id")
.map(&:serializable_hash)
A Valuation model:
class Valuation < ApplicationRecord
belongs_to :membership
belongs_to :capability
end
A Membership model:
class Membership < ApplicationRecord
belongs_to :organisation
belongs_to :location
has_many :valuations
end
results snippet:
[{"id"=>1, "score"=>4, "capability_id"=>1, "membership_id"=>1, "location_id"=>26, "organisation_id"=>1}, {"id"=>16, "score"=>3, "capability_id"=>1, "membership_id"=>2, "location_id"=>36, "organisation_id"=>1}, {"id"=>31, "score"=>3, "capability_id"=>1, "membership_id"=>3, "location_id"=>26, "organisation_id"=>2}, {"id"=>46, "score"=>6, "capability_id"=>1, "membership_id"=>4, "location_id"=>16, "organisation_id"=>2}...

I'll assume for each organization: the name, taglist and organization_id remains the same.
your_hash = results.reduce({}) do |h, i|
org_id = i['organisation_id']
h[org_id] ||= {
name: capability.name,
tags: capability.taglist,
organisation_id: org_id,
scores: {}
}
h[org_id][:scores][i['location_id']] = i['score']
# If the location scores are not strictly exclusive, you can also just +=
h
end

I believe this works, but data is needed to test it.
results.each_with_object({}) do |i,h|
h.update(i['organisation_id'] => {
name: capability.name,
tags: capability.tag_list,
organisation_id: i['organisation_id'],
scores: {i['location_id'] => i['score']}) { |_,o,n|
o[:scores].update(n[:score]); o }
}
end.values
This uses the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged. Please consult the doc for the contents of each of the block variables _, o and n.

Assume, that result is your final array of hashes:
result.each_with_object({}) do |e, obj|
k, v = e.flatten
if obj[k]
obj[k][:scores] = obj[k][:scores].merge(v[:scores])
else
obj[k] = v
end
end

Related

ROR: Serializing output for multiple objects

I am learning rails, and using active_model_serializers gem.
I need to get the customized output in JSON from two different tables. Here is an example
car: id, name, color, size, shape, date...
battery: id, car_id, price, weight, name, date...
class battery < ApplicationRecord
belongs_to :car
end
query I ran is
## This is sample query will not work
Car.includes(:battery).where(car: {color: red}).where(date >= 'blah')
I have three hashes in calculated in my controller
#cars_controller.rb
available_cars ={
available_cars:[{id:id,
name:name,
color:color,
size:size,
date:date..
}]
}
available_battries = {
available_batteries:[{id:id,
card_id:car_id,
price:price,
weight:weight,
name:name,
date:date..
}]
}
cars_with_battires = {
cars_with_battry: [{id:id,
name:name,
color:color,
size:size,
date:date..},
battries:[{id: 1,
name: name1
},
{id: 2,
name: name2
}
]
]}
render json: {
available_cars: available_cars,
available_batteries: available_batteries,
cars_with_battry: cars_with_battry }
serializer: CarIndexListSerializer }
This is my serializer file looks like and I need to fill those functions
#car_index_list_serializer.rb
class CarIndexListSerializer < ActiveModel::Serializer
attributes :available_cars,
:available_batteries,
:cars_with_battry
def available_cars
#todo?
end
def available_batteries
#todo?
end
def cars_with_battry
#todo?
end
I can just remove the fields I dont want and return the result in JSON but I want to use active_model_serializers to return it in following format.
{
available_cars:[{id: 1, name:name1, size: abc}],
available_batteries:[{id: 1, name: name1}],
cars_with_battry:[{id: 1, name:name1, size: abc,
battries:[{id: 1, name: name1}]
}]
}
if any of the hash is empty, I do not want to show it in the results
result = {}
available_cars = Car.where xxx
if available_cars.count.positive?
result[:available_cars] = available_cars.map {|car| CarSerializer.new(car).as_json }
end
# do the same thing for available battery and car with battery
render json: result
just a rough idea
Update
Usually for #index action on controller, it render list of resources
so CarIndexListSerializer is not required
What you have to create is two serializers:
car serializer
battery serializer
even you can add a relationship data to your car serializer
class CarSerializer < ActiveModel::Serializer
has_many :batteries
end
class Car < ApplicationRecord
has_many :batteries
end
so when you call this
car = Car.last
CarSerializer.new(car).as_json
it would return the car and batteries data
don't forget to create BatterySerializer too

How do I create an array of associations from a given string?

Given I have an AR object (object) and a string (s). s is based out of the associations of object. ie. Product belongs to Aisle, Aisle belongs to Department, Department belongs to Shop
I want to get an array of all those associations. This is what Ive come up with
object = Product.last
s='aisle.department.shop'
new_array = s.split(',').each_with_object([]).with_index do |(assoc_set, array), index|
a = assoc_set.split('.')
a.each_with_index do |assoc, index|
if index == 0
array << object.send(assoc)
elsif index == 1
array << object.send(a[index - 1]).send(a[index])
elsif index == 2
array << object.send(a[index - 2]).send(a[index - 1]).send(a[index])
elsif index == 3
array << object.send(a[index - 3]).send(a[index - 2]).send(a[index - 1]).send(a[index])
end
end
end
Outputs exactly what I want:
[
#<Aisle:0x00007fa001d6f800 ...>,
#<Department:0x00007fa001d44b00 ...>,
#<Shop:0x00007fa020f4bc68 ...>,
]
Except code isnt dynamic. As you can see, it only goes up to 3 levels deep. How can I refactor this?
This should work as you wanted
object = Product.last
relations_chain='aisle.department.shop'
relations_chain.split('.').each_with_object([]).with_index do |(relation_name, array), index|
prev_object = index.zero? ? object : array[index - 1]
array << prev_object.send(relation_name)
end
You can delegate relations and call everything on Product model.
class Department < ApplicationRecord
delegate :shop, to: :department, allow_nil: true
end
class Product < ApplicationRecord
delegate :department, to: :aisle, allow_nil: true
delegate :shop, to: :aisle, allow_nil: true
end
relations_chain.split('.').map { |relation_name| object.send(relation_name) }

Rails to_json(methods: => [...]) for different ActiveRecords

In Rails, i have an object called values that could be 1 of 20 kinds of ActiveRecord, and in only 1 of them there's a method(may be the wrong term, rails newbie) that can add a customized field in returned JSON object where the method name is the field name and method returned value is the field value. For example
class XXXController < ApplicationController
..
if a
values = A
elsif b
values = B
elseif c
values = C
..
end
render :json => values.to_json(:methods => :type_needed)
and you will see response like
{
..
"type_needed": true,
..
}
I only have type_needed defined in A which will return true in some cases. For others like B, C, D... which in total 19, i want them to all have type_needed returned as false, is there a way i can do that in one place instead of add type_needed method in the rest 19?
I will do it as follows:
json = values.to_json(:methods => :type_needed)
# => "[{\"id\":1,\"name\":\"Aaa\"},{\"id\":\"2\",\"name\":\"Bbb\"}]" # => Representational value only
ary = JSON.parse(json)
# => [{"id"=>1, "name"=>"Aaa"}, {"id"=>2, "name"=>"Bbb"}]
ary.map! { |hash| hash[:type_needed] = false unless hash.key?(:type_needed); hash }
# => [{"id"=>1, "name"=>"Aaa", :type_needed=>false}, {"id"=>2, "name"=>"Bbb", :type_needed=>false}]
ary.to_json
# => "[{\"id\":1,\"name\":\"Aaa\",\"type_needed\":false},{\"id\":\"2\",\"name\":\"Bbb\",\"type_needed\":false}]"
If I am understanding your question correctly then you want to define type_needed method once and have it included on all your 20 models. If yes, then you can define a concern and include it in all your 20 models.
app/models/concerns/my_model_concern.rb
module MyModelConcern
extend ActiveSupport::Concern
def type_needed?
self.respond_to?(:some_method)
end
end
app/models/a.rb
class A < ApplicationRecord
include MyModelConcern
def some_method
end
end
app/models/b.rb
class B < ApplicationRecord
include MyModelConcern
end
app/models/c.rb
class C < ApplicationRecord
include MyModelConcern
end
With the above
a = A.new
a.type_needed?
=> true
b = B.new
b.type_needed?
=> false
c = C.new
c.type_needed?
=> false
See if this helps.

Instance Variables in a Rails Model

I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method

N+1 while enumerating self-referencing records

I'm doing a pretty basic thing - displaying a tree of categories in topological order and ActiveRecord issues extra query for enumerating each category's children.
class Category < ActiveRecord::Base
attr_accessible :name, :parent_id
belongs_to :parent, :class_name => 'Category'
has_many :children, :class_name => 'Category', :foreign_key => 'parent_id'
def self.in_order
all = Category.includes(:parent, :children).all # Three queries as it should be
root = all.find{|c| c.parent_id == nil}
queue = [root]
result = []
while queue.any?
current = queue.shift
result << current
current.children.each do |child| # SELECT * FROM categories WHERE parent_id = ?
queue << child
end
end
result
end
end
UPD. As far as I understand what's going here is that when a category is referred as a children of some category it's not the same object as the one in the initial list and so it hasn't it's children loaded. Is there a way to implement desired behavior without resorting to creating extra adjacency list?
UPD2: Here's the manual adjacency list solution. It uses only one query but I'd really like to use something more idiomatic
def self.in_order_manual
cache = {}
adj = {}
root = nil
all.each do |c|
cache[c.id] = c
if c.parent_id != nil
(adj[c.parent_id] ||= []) << c.id
else
root = c.id
end
end
queue = [root]
result = []
while queue.any?
current = queue.shift
result << current
(adj[current] || []).each{|child| queue << child}
end
result.map{|id| cache[id]}
end

Resources