Rails group and sum array of objects - ruby-on-rails

A production has_many :production_lines,
production_line belongs_to :item,
item has_one :recipe,
recipe has_many :recipe_lines,
recipe_line belongs_to :item,
production_line and recipe line have attribute quantity. I need to group recipe_lines for a production by item, with quantity that equals to production_line.quantity * recipe_line.quantity
def item_quantities
array = production_lines.map do |p|
p.item.recipe.recipe_lines.map do |r|
{
item_id: r.item_id,
item_name: r.item.name,
quantity: r.quantity * p.quantity
}
end
end
array.flatten(1).group_by { |p| p[:item_id] }
.transform_values { |vals| vals.sum { |val| val[:quantity] } }
end
This returns:
item_quantities = {
1: 10,
2: 5
}
where key is item_id and value is quantity. Values are correct.
However I would like to return:
item_quantities = [
{
id: 1,
name: "Tomato",
quantity: 10,
},
{
id: 2,
name: "Carrot",
quantity: 5
}
]
How should I change my solution to achieve that?

First of all, your nested map followed by flatten(1) can be simplified by making the first map into flat_map. If you do this you could remove the flatten(1).
From this point your code is most of the way there, but you could make the following changes to get the desired output:
you can group by multiple attributes, name and id. In another language you might use a tuple for this. Ruby doesn't have tuples, so we can just use a len-2 array:
.group_by { |p| [p[:item_id], p[:item_name]] }
.transform_values { |vals| vals.sum { |val| val[:quantity] } }
At this point you have a hash mapping [id,name] tuple to quantity:
{ [1,"foo"] => 123, [2, "bar"] => 456 }
and you can coerce this to the desired data type using reduce (or each_with_object, if you prefer):
.reduce([]) do |memo, ((id, name), quantity)|
memo << {
id: id,
name: name,
quantity: quantity
}
end
The wierd looking ((id, name), quantity) is a kind of destructuring. See https://jsarbada.wordpress.com/2019/02/05/destructuring-with-ruby/ specifically the sections on "Destructuring Block Arguments" and "Destructuring Hashes".

Related

Using group_by but return an array of hashes

I simply want to group cities by their state and from there have the array of the hash key (i.e. State Name) return an array of hash data pertaining to it's cities. Right now I have something like this:
City.all.group_by { |c| c.state.name }
Which will return:
{
"Illinois": [# < City id: 3, name: "Chicago", state_id: 3 > ],
"Texas": [# < City id: 2, name: "Houston", state_id: 2 > ],
"California": [# < City id: 1, name: "Los Angeles", state_id: 1 > ],
"New York": [# < City id: 4, name: "New York City", state_id: 4 > ]
}
Notice how it returns an array of rails objects. Instead I want to return an array of hashes with certain attributes, like their id and name.
The reason the grouped values are Rails objects (your models) is due to the fact that you also start with these objects. You can use the attributes method to retrieve the attributes of a model instance as a hash.
The following achieves the result you want:
City.all.group_by { |city| city.state.name }
.transform_values { |cities| cities.map(&:attributes) }
If you only want specific attributes, use slice instead:
City.all.group_by { |city| city.state.name }
.transform_values { |cities| cities.map { |city| city.slice(:id, :name) } }
Note that slice will return an ActiveSupport::HashWithIndifferentAccess instance. Which mostly can be used in the same manner as a normal hash, but returns the same value for both hash[:name] and hash['name']. If you rather use a normal hash append a to_hash call after the slice call.
This should be enough for you
City.all.group_by { |c| c.state.name }.map {|k,v| [k, v.attributes] }.to_h
and to select only specified attributes do
v.attributes.slice(:name, :id)
One of the easiest approach is to convert it into json object
City.all.as_json.group_by { |c| c.state.name }
this will fix the issue

ActiveModel::Errors when mass updating

On a single instance, validation would produce something like:
foo = Foo.new(price: -2)
foo.valid?
foo.errors
=> #<ActiveModel::Errors:0x00007fc66e670430
#base=#<Foo:0x00007fc6503f8658 id: nil, price: nil,
#details={:price=>[{:error=>:greater_than_or_equal_to, :value=>-0.2e1, :count=>0}]},
#messages={:price=>["must be greater than or equal to 0"]}>
Is there a rails way of obtaining the errors when using the update method?:
Foo.update([1, 2, 3], [{ price: 10 }, { price: -20 }, { price: 3 }])
Thank you!
Here is an example how to gather errors from the Model.update(...) method:
# first create a payload with ids and attributes
payload = { 1 => { price: 10 }, 2 => { price: -20 } }
# next update records
result = Foo.update(payload.keys, payload.values)
# the update method returns processed records
# in case of array it will return array of records
# iterate over all objects and find invalid
with_errors = result.map { |r| !r.errors.any? ? nil : r }.compact
# after compact the with_errors variable contains only invalid records.
What I did in the end:
class FooUpdateService
attr_reader :errors
def update(ids, values)
self.errors = ActiveModel::Errors.new(Foo.new)
Foo.update(ids, values).select(&:invalid?).each { |invalid_foo| self.errors.merge!(invalid_foo.errors) }
end
private
attr_writer :errors
end

How can i map one query result to another query result in ruby on rails

Hi I am new to Ruby on Rails development. I have two queries with different model. My first_query is get from question model and second query is get from favourite model. I want to map with a column user_favourite from second query result to first query result.
this is my controller queries
def index
#first_query = Question.order('created_at DESC').page(params[:page]).per( (ENV['ILM_QUESTIONS_PER_PAGE'] || 5).to_i )
#second_query=Favourite.with_user_favourite(#user)
#combined_queries = #first_query + #second_query
end
favourite.rb
scope :with_user_favourite, -> (user) {
joins(:user).
where(favourites: {user_id: user})
}
index.json.builder
json.questions #combined_events
json for the result is
{
questions: [ #this is first query result
{
id: 88,
user_id: 28,
content: "test32",
image: {
url: null,
thumb: {
url: null
},
mobile: {
url: null
}
}
},
{
id: 87,
user_id: 18,
content: "testing riyas",
image: {
url: null,
thumb: {
url: null
},
mobile: {
url: null
}
}
},
{ #this is second query result
id: 1,
user_id: 2,
question_id: 84,
created_at: "2016-05-12T06:51:54.555-04:00",
updated_at: "2016-05-12T06:51:54.555-04:00"
},
{
id: 2,
user_id: 2,
question_id: 81,
created_at: "2016-05-12T07:23:47.770-04:00",
updated_at: "2016-05-12T07:23:47.770-04:00"
}
]
}
i want response like
{
questions: [
{ #first query result
id: 88,
user_id: 28,
content: "test32",
image: {
url: null,
thumb: {
url: null
},
mobile: {
url: null
}
},
user_favorite: { #corresponding result from second query result
id: 1,
user_id: 2,
question_id: 88
}
},
{ #first query result
id: 87,
user_id: 18,
content: "testing riyas",
image: {
url: null,
thumb: {
url: null
},
mobile: {
url: null
}
},
user_favorite: {} #corresponding result from second query result if there is no result for particular question in favourite table
},
]
}
The model relationships are:
class Question
belongs_to :user
has_many :favourite
end
class Favourite
belongs_to :user
belongs_to :question
end
class User
has_many :questions
has_many :favourite
end
You should modify your jBuilder template to support nesting.Since your model association is like one question has_many favorite so it will be an array and you can easily nest one object inside another.
json.array! #questions do |question|
json.user_id question.user_id
json.content question.content
json.user_favorites question.favorites do |json,favorite|
json.id question.favorite.id
json.user_id question.favorite.user.id
json.question_id question.id
end
end
Here is a link that you can refer to for more clarity.
Generate a nested JSON array in JBuilder
Using JBuilder to create nested JSON output in rails
Hope it helps!.
You can add an association between user_favourite and question so that you can select all user favourites on one question.
Question.rb:
has_many :user_favourites
UserFavourite.rb:
belongs_to :question
Then, as your web action:
def index
#questions = Question.all.order('created_at DESC').page(params[:page]).per((ENV['ILM_QUESTIONS_PER_PAGE'] || 5).to_i)
end
And finally, in index.json.builder:
json.questions #questions do |question|
json.user_favourites question.user_favourites
end
including whatever other fields you want.

Group and sum 2 columns in rails activerecord

I have this query:
Order.joins(cart: { cart_items: :product })
.group("products.name", "cart_items.amount")
.count
.map { |a, b| { name: a[0], amount: a[1] * b} }
It returns something like this:
[{:name=>"Lorem", :amount=>1},
{:name=>"Lorem", :amount=>2},
{:name=>"Foo", :amount=>1}]
The problem is because if I have 2 cart_items with the same product, it returns 2 values, as you can see in the product "Lorem"
I expect something like this:
[{:name=>"Lorem", :amount=>3},
{:name=>"Foo", :amount=>1}]
This is what you want:
Order.joins(cart: { cart_items: :product })
.group("products.name")
.select("products.name as product_name", "SUM(cart_items.amount) as cart_items_amount")
.map { |order| { name: order.product_name, amount: order.cart_items_amount } }

How to group-by and nest results in each group?

I tried to do this:
Things.order("name").group("category_name")
I was expecting the results to be something like this:
[
{
"category_name": "Cat1",
"things":
[
{ "name": "Cat1_Thing1" },
{ "name": "Cat1_Thing1" }
]
},
{
"category_name": "Cat2",
"things":
[
{ "name": "Cat2_Thing3" },
{ "name": "Cat2_Thing4" }
]
}
]
So I would have expected to get an array of "categories" each with an array of "items" which are within that category. Instead, it appears to give me a list of things, sorted by the field I grouped on.
Note: category_name is a column in the thing table.
Try something like
my_grouping = Category.includes(:things).
select("*").
group('categories.id, things.id').
order('name')
=> #<ActiveRecord::Relation [#<Category id: 1, name: "Oranges">, #<Category id: 2, name: "Apples">]>
Though, you'll still have to access the Thing objects via my_grouping.things, they'll already be at your hand, and you won't have to wait for the results. This is likely the sort of interaction you're looking for, vs. mapping them into an actual Array.
One option is to do the grouping in Rails (it returns a hash)
Things.order("name").group_by(&:category_name)
#=> {"cat1" => [thing1,thing2,..], "cat2" => [thing3,thing4,..],..}
ActiveRecord::Base#group performs a SQL GROUP BY. I think, but i'm not sure (depends on your db adapter) that as you don't specify any SELECT clause, you get the first record for each category.
To achieve what you want, there are different ways.
For instance, using #includes :
Category.includes(:things).map do |category|
{
category_name: category.name,
things: things.sort_by(&:name).map{|t| {name: t.name} }
}
end.to_json
Note that the standard (albeit often frowned upon) way to serialize models as json is to use (and override if need be) as_json and to_json. so you would have something along the lines of this :
class Category < ActiveRecord::Base
def as_json( options = {} )
defaults = { only: :name, root: false, include: {links: {only: :name}} }
super( defaults.merge(options) )
end
end
Use it like this :
Category.includes(:links).map(&:to_json)
EDIT
As category_name is only a column, you can do :
Thing.order( :category_name, :name ).sort_by( &:category_name ).map do |category, things|
{ category_name: category, things: things.map{|t| {name: t.name} } }
end.to_json
such thing could belong in the model :
def self.sorted_by_category
order( :category_name, :name ).sort_by( &:category_name ).map do |category, things|
{ category_name: category, things: things.map{|t| {name: t.name} } }
end
end
so you can do :
Thing.sorted_by_category.to_json
this way, you can even scope things further :
Thing.where( foo: :bar ).sorted_by_category.to_json

Resources