Bakery Inventory Calculations - ruby-on-rails

I want to create a Bakery inventory that lists the total amount of ingredients needed for the day by taking in:
Cookie orders from Customers [2 Dozen Sugar Cookies, 1 Dozen Oatmeal Cookies]
Cookie ingredients [Sugar Cookie: 1 cup sugar, etc.]
See a chart here
How would you recommend storing cookie ingredients? My initial thoughts were a hash.
{
flour: 2.75,
sugar: 1,
. . .
}

Why a hash?
You lose the benefits of a relational DB then, e.g, if someone edits the "Sugar" ingredient, your hash is no longer up-to-date. That may be okay, but you'll still have to deserialize and do the math.
Consider instead (pseudo-code):
Recipe => has_many :quantities
Quantity => has_one :ingredient
Order => has_many :recipes # Or whatever; you get the idea
Then you can query for a day's orders' quantities, group by ingredient, and do the math.
If you want to keep the hash you could use ingredient IDs as the key etc.
If you're already using a NoSQL DB then none of the above really applies and you have another set of options to choose from, although in some ways you end up with the same questions.

Related

Rails 4 ActiveRecord group_by and nested loops

The project I am working on has the following models:
Button: has many extra_prices (one for each Currency)
ExtraPrice: belongs to a product (in that case a Button) and has as an attribute a currency_id
Currency: there is a reference to a currency_id on an ExtraPrice as I mentioned above. FYI there are 4 currencies so far in the app.
Some of the buttons don't have an extra_price set in one of the currencies which causes an error in another part of the app.
I am trying to write a rake task that would:
- check all buttons missing an extra_price for one a more currencies
- find out which currency is missing
- create the extra price
So far I toyed with a few options but I am stuck (I am pretty junior as a dev, and especially on the back-end side/DB query).
I was thinking something like:
Button.transaction do
currencies = Currency.all.pluck :id
buttons_no_extra_price = Button.select { |button| button.extra_prices.length <
currencies.length }
end
and then I'm stuck :)
I would like to do something like
buttons_no_extra_price.group_by(|button| button.extra_prices.currency_ids)
(wrong formatting of course since extra_prices is an array and currency_id is an attribute on each extra_price)
but instead of grouping them by currency_id, I would like to group them by the missing currency_id or ids, maybe using the currencies variable above.
missing_prices = {currency1: [button1, button2], currency2: [button192, button208], currency3: [button392, button220]...}
This way I could loop through every Currency and create an extra_price on each button object of the nested array like:
missing_prices.each |currency, array_of_buttons| do
array_of_buttons.each do |button|
ExtraPrice.create!(currency: currency, product: button)
end
end
I am also thinking that from a performance standpoint it needs to be optimized so maybe work more with includes, joins, etc. but it's a bit above my current abilities to be totally honest.
So any help would be appreciated :)
Thanks!
So I think I follow your question, and if I am this should do the trick. Let me know if you have any questions. Note that there is probably a more performant way to do this, but given that it's a rake task performance won't need to be fully optimized unless you are dealing with millions of records.
all_currency_ids = Currency.all.pluck(:id)
Button.eager_load(:extra_price).group('buttons.id').having('count(extra_prices.id) < ?', all_currency_ids.count).each do |button|
missing_currency_ids = all_currency_ids - button.extra_prices.pluck(:currency_id)
missing_currency_ids.each do |missing_currency_id|
ExtraPrice.create!(currency: Currency.find_by(id: missing_currency_id), product: button)
end
end
Button.eager_load(:extra_price).group('buttons.id').having('count(extra_prices.id) < ?', all_currency_ids.count) is what gets you the buttons with missing extra prices. This hinges on the fact that each button has an extra price per currency, so I hope I interpreted that correctly.
(Note: I'm not 100% familiar with Rails 4, only 5 and 6. The underlying SQL principles remain the same regardless and are adaptable.)
You can do this in a single query if your relationships are set up correctly.
class Button < ApplicationRecord
has_many :extra_prices
has_many :currencies, through: :extra_prices
end
class ExtraPrice < ApplicationRecord
belongs_to :button
belongs_to :currency
end
class Currency < ApplicationRecord
has_many :extra_prices
end
I've added a relationship between Currency and ExtraPrice. This allows us to set up a relationship between Button and Currency using has_many :currencies, through: :extra_prices. Then we can get a Button's Currencies with button.currencies.
Now we can do a left join between Button and ExtraPrice and Currency. A left join, as opposed to the normal inner join, will pick up Buttons that have no ExtraPrices nor Currencies. We can't use Rails's left_joins, it will not do the right thing.
buttons_missing_currencies = Button
.includes(:currencies)
.joins("left join extra_prices ep on ep.button_id = buttons.id")
.joins("left join currencies c on c.id = ep.currency_id")
.having("count(c.id) < ?", Currency.count)
.group("buttons.id")
That will give you all the Buttons which lack a Currency in a single, efficient query. includes(:currencies) means each Button's Currencies will already be loaded avoiding making N+1 queries.
Now we can look through each button, discover which currencies are missing, and fill them in.
all_currencies = Currency.all
buttons_missing_currencies.each do |button|
missing_currencies = all_currencies - button.currencies
missing_currencies.each do |missing_currency|
button.extra_prices.create!(currency: missing_currency)
end
end

Rails query a has_many :through conditionally with multiple ids

I'm trying to build a filtering system for a website that has locations and features through a LocationFeature model. Basically what it should do is give me all the locations based on a combination of feature ids.
So for example if I call the method:
Location.find_by_features(1,3,4)
It should only return the locations that have all of the selected features. So if a location has the feature_ids [1, 3, 5] it should not get returned, but if it had [1, 3, 4, 5] it should. However, currently it is giving me Locations that have either of them. So in this example it returns both, because some of the feature_ids are present in each of them.
Here are my models:
class Location < ActiveRecord::Base
has_many :location_features, dependent: :destroy
has_many :features, through: :location_features
def self.find_by_features(*ids)
includes(:features).where(features: {id: ids})
end
end
class LocationFeature < ActiveRecord::Base
belongs_to :location
belongs_to :feature
end
class Feature < ActiveRecord::Base
has_many :location_features, dependent: :destroy
has_many :locations, through: :location_features
end
Obviously this code isn't working the way I want it to and I just can't get my head around it. I've also tried things such as:
Location.includes(:features).where('features.id = 5 AND features.id = 9').references(:features)
but it just returns nothing. Using OR instead of AND give me either again. I also tried:
Location.includes(:features).where(features: {id: 9}, features: {id: 1})
but this just gives me all the locations with the feature_id of 1.
What would be the best way to query for a location matching all the requested features?
When you do an include it makes a "pseudo-table" in memory which has all the combinations of table A and table B, in this case joined on the foreign_key. (In this case there's already a join table included (feature_locations), to complicate things.)
There won't be any rows in this table which satisfy the condition features.id = 9 AND features.id = 1. Each row will only have a single features.id value.
What i would do for this is forget about the features table: you only need to look in the join table, location_features, to test for the presence of specific feature_id values. We need a query which will compare feature_id and location_id from this table.
One way is to get the features, then get a collection of arrays if associated location_ids (which just calls the join table), then see which location ids are in all of the arrays: (i've renamed your method to be more descriptive)
#in Location
def self.having_all_feature_ids(*ids)
location_ids = Feature.find_all_by_id(ids).map(&:location_ids).inject{|a,b| a & b}
self.find(location_ids)
end
Note1: the asterisk in *ids in the params means that it will convert a list of arguments (including a single argument, which is like a "list of one") into a single array.
Note2: inject is a handy device. it says "do this code between the first and second elements in the array, then between the result of this and the third element, then the result of this and the fourth element, etc, till you get to the end. In this case the code i'm doing between the two elements in each pair (a and b) is "&" which, when dealing with arrays, is the "set intersection operator" - this will return only elements which are in both pairs. By the time you've gone through the list of arrays doing this, only elements which are in ALL arrays will have survived. These are the ids of locations which are associated with ALL of the given features.
EDIT: i'm sure there's a way to do this with a single sql query - possibly using group_concat - which someone else will probably post shortly :)
I would do this as a set of subqueries. You can actually also do it as a scope if you wish.
scope :has_all_features, ->(*feature_ids) {
where( ( ["locations.id in (select location_id from location_features where feature_id=?)"] * feature_ids.count).join(' and '), *feature_ids)
}

ActiveRecord query array intersection?

I'm trying to figure out the count of certain types of articles. I have a very inefficient query:
Article.where(status: 'Finished').select{|x| x.tags & Article::EXPERT_TAGS}.size
In my quest to be a better programmer, I'm wondering how to make this a faster query. tags is an array of strings in Article, and Article::EXPERT_TAGS is another array of strings. I want to find the intersection of the arrays, and get the resulting record count.
EDIT: Article::EXPERT_TAGS and article.tags are defined as Mongo arrays. These arrays hold strings, and I believe they are serialized strings. For example: Article.first.tags = ["Guest Writer", "News Article", "Press Release"]. Unfortunately this is not set up properly as a separate table of Tags.
2nd EDIT: I'm using MongoDB, so actually it is using a MongoWrapper like MongoMapper or mongoid, not ActiveRecord. This is an error on my part, sorry! Because of this error, it screws up the analysis of this question. Thanks PinnyM for pointing out the error!
Since you are using MongoDB, you could also consider a MongoDB-specific solution (aggregation framework) for the array intersection, so that you could get the database to do all the work before fetching the final result.
See this SO thread How to check if an array field is a part of another array in MongoDB?
Assuming that the entire tags list is stored in a single database field and that you want to keep it that way, I don't see much scope of improvement, since you need to get all the data into Ruby for processing.
However, there is one problem with your database query
Article.where(status: 'Finished')
# This translates into the following query
SELECT * FROM articles WHERE status = 'Finished'
Essentially, you are fetching all the columns whereas you only need the tags column for your process. So, you can use pluck like this:
Article.where(status: 'Finished').pluck(:tags)
# This translates into the following query
SELECT tags FROM articles WHERE status = 'Finished'
I answered a question regarding general intersection like queries in ActiveRecord here.
Extracted below:
The following is a general approach I use for constructing intersection like queries in ActiveRecord:
class Service < ActiveRecord::Base
belongs_to :person
def self.with_types(*types)
where(service_type: types)
end
end
class City < ActiveRecord::Base
has_and_belongs_to_many :services
has_many :people, inverse_of: :city
end
class Person < ActiveRecord::Base
belongs_to :city, inverse_of: :people
def self.with_cities(cities)
where(city_id: cities)
end
# intersection like query
def self.with_all_service_types(*types)
types.map { |t|
joins(:services).merge(Service.with_types t).select(:id)
}.reduce(scoped) { |scope, subquery|
scope.where(id: subquery)
}
end
end
Person.with_all_service_types(1, 2)
Person.with_all_service_types(1, 2).with_cities(City.where(name: 'Gold Coast'))
It will generate SQL of the form:
SELECT "people".*
FROM "people"
WHERE "people"."id" in (SELECT "people"."id" FROM ...)
AND "people"."id" in (SELECT ...)
AND ...
You can create as many subqueries as required with the above approach based on any conditions/joins etc so long as each subquery returns the id of a matching person in its result set.
Each subquery result set will be AND'ed together thus restricting the matching set to the intersection of all of the subqueries.

See if one person is before another in the alphabet, ruby, rails

I'm doing an app for a membership database.
Each person may have a partner. When it comes to displaying the list, I only want to have one row for each family, so at the moment I'm comparing first names and not displaying the row if the person's name is second. Like this
person.first_name != [person.first_name, person.partner.first_name].sort[0]
This means each family only gets displayed once, not twice - once for each partner.
And I'm doing this in the view.
There must be a better way of doing this, and it'd be really great if I could do it at the database level. I'm using postgresql if that makes a difference.
Edit
Sorry if it was unclear.
Say Person 1 has the first_name "Edward" and Person 2 has the first_name "Fay". Edward and Fay are married.
I only want to show them once in my list - I want a row to look like this
Surname First name Address etc
Mysurname Edward ....
Fay
I don't want to display it again with Fay first because I've got both Fay and Edward in list of people, so I use the ruby in the first part of the question to check if I should display the row - it compares their first names and only does the row if the person has a fist name that's before his/her partner's first name.
Here's the relevant part of my person model
class Person < ActiveRecord::Base
has_one :relationship_link, :foreign_key => :person_id, :dependent => :destroy, :include => :partner
has_one :partner, :through => :relationship_link, :source => :person_b, :class_name => "Person"
I hope that's clearer
You need to use DISTINCT ON or GROUP BY. In postgres you need to be careful to group by everything that you are selecting. If you only need to get the last names you can select("DISTINCT ON(last_name) last_name").pluck("last_name"). You will only get an array of last names though.
Maybe you can get records if you order by every other fields in your table, like this:
select("DISTINCT ON(people.last_name) people.*").order("people.last_name ASC, people.first_name ASC, people.field2 DESC, people.field3 ASC...")
You need to order by every attribute so the result is not ambigious.
For this case, i would create a data structure (a Hash) to store people instances given a specific surname. Something like this:
def build_surnames_hash(people_array)
surnames_hash = {}
people_array.each do |person|
last_name = person.last_name
surnames_hash[last_name] ||= []
surnames_hash[last_name] << person
end
surnames_hash
end
That way, you can iterate over the hash and display people using their surnames stored as hash's keys:
surnames_hash = build_surnames_hash(Person.all)
surnames_hash.each do |surname, person_instances_array|
# display the surname once
# iterate over person_instances_array displaying their properties
end

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Resources