I've looked over some SO discussions here and, well at least I haven't seen this perspective. I'm trying to write code to count bookings of a given resource, where I want to find the MINIMUM number of resources I need to fulfill all bookings.
Let's use an example of hotel rooms. Given that I have the following bookings
Chris: July 4-July 17
Pat: July 15-July 19
Taylor: July 10-July 11
Chris calls and would like to add some room(s) to their reservation for friends, and wonders how many rooms I have available.
Rooms_available = Rooms_in_hotel - Rooms_booked
The Rooms_booked is where I'm having trouble. It seems like most questions (and indeed my code) just looks at overlapping dates. So it would do something like this:
Booking.where("booking_end >= :start_check AND booking_start <= :end_check", { start_check: "July 4, 2021".to_date, end_check: "July 7, 2021".to_date})
This code would return 3. Which means that if the hotel theoretically had 5 rooms, I would tell Chris that there were 2 more rooms left available.
However, while this method of counting is technically accurate, it misses the possibility of an efficient overlap. Namely that since Taylor checks out 4 days before Pat, they can both be "assigned" the same room. So technically, I can offer 3 more rooms to Chris.
So my question is how do I more accurately calculate Rooms_booked allowing for efficient overlap (i.e., efficient resource allocation)? Is there a query using ActiveRecord or what calculation do I impose on top of the existing query?
i don't think just only a query could solve your problem (or very very complex query).
my idea is group (and count) by (booking_start, booking_end) in order booking_start asc then reassign, e.g. if there're 2 bookings July 15-July 19 and 3 bookings July 10-July 11 then we only could re-assign for 2 pairs, and we need 3 rooms (2 rooms for July 15-July 19-July 10-July 11 and 1 for July 10-July 11.
and re-assign in code not query (we can optimize by pick a narrow range of time)
# when Chris calls and would like to add some room(s) to their reservation for friends,
# and wonders how many available rooms.
# pick (start_time, end_time) so that the range time long enough that
# including those booking {n} month ago
# but no need too long or all the time
scope :booking_available_count, -> (start_time, end_time) {
group_by_time = \
Booking.where("booking_start >= ? AND booking_end <= ?", start_time, end_time)
.group(:booking_start, :booking_end).order(:booking_start).count
# result: {[booking_start, booking_end] => 1, [booking_start, booking_end] => 2, ... }
# in order booking_start ASC
# then we could re-assign from left to right as below
booked = 0
group_by_time.each do |(start_time, end_time), count|
group_by_time.each do |(assign_start_time, assign_end_time), assign_count|
next if end_time > assign_start_time
count -= assign_count # re-assign
break if count <= 0
end
booked += count if count > 0
end
# return number of available rooms
# allow negative number
Room.count - booked
}
Related
I've built a Rails app that logs employee time (clockin/clockout) and calculates total hours, allows exports to CSV/PDF, search timecards based on dates, etc.
What I'm really wanting to do is to implement payroll periods via a scope of some sort of a method.
The payroll period begins on a Sunday and ends on a Saturday 14 days later. What would be the best way to write a scope like this? Also is it possible to split the weeks into two for the payroll period?
I wrote these scopes but they are flawed:
scope :payroll_week_1, -> {
start = Time.zone.now.beginning_of_week - 1.day
ending = Time.zone.now.end_of_week - 1.day
where(clock_in: start..ending)
}
scope :payroll_week_2, -> {
start = Time.zone.now.end_of_week.beginning_of_day
ending = Time.zone.now.end_of_week.end_of_day + 6.days
where(clock_in: start..ending)
}
These works if you are currently in a payroll period, but once you pass the end of the week, the scopes no longer work because I'm basing my timing off of Time.zone.now
Is there any way to actually do this? Even if I have to set some sort of static scope or value which says April 10 - 23 is payroll period 1, etc etc. I'm really not sure how to approach this problem and what might work. So far what I've written works in the current pay period but as time advances the scope drifts.
Any help would be greatly appreciated.
I think what you want is to create a scope, which can receive a start_day as a parameter:
scope :payroll_week_starting, -> (start_day) {
where(clock_in: start_day..(start_day + 1.week))
}
Then, in the future you will be able to call your scope with the first day of your pay period:
ModelName.payroll_week_starting(Date.parse('31/12/2015'))
UPDATE:
As per your comment, it seems that you're looking for a bit more information from an architectural perspective. It's pretty tough to help you without understanding your database architecture, so I'm just going to go from a high level.
Let's assume you have an Employee model and a Shift model with the clock_in and clock_out fields. You may also want a model called PayPeriod with the fields start_date and end_date.
Each Employee has_many :shifts and each PayPeriod has_many :shifts
You might add a couple of class methods on PayPeriod, so you can find and/or create the PayPeriod for any given datetime.
def self.for(time)
find_by("start_date < ? AND end_date > ?", time, time)
end
def self.create_for(time)
# yday is the number of days into the current year
period_start_yday = 14 * (time.yday / 14)
start_date = Date.new(time.year) + period_start_yday.days
next_year = Date.new(time.year) + 1.year
create(
start_date: start_date,
end_date: [start_date + 14.days, last_day_of_year].min,
)
end
def self.find_or_create_for(time)
for(time) || create_for(time)
end
The create_for logic is pretty complicated, but an example will help you understand:
Say I clocked in today May 17th, 2016, the yday for today is 138, if you use integer division (default for ruby) to divide by 14 (the length of your pay periods), you'll get 9. By multiplying that by 14 again, you'll get 126, which is the most recent yday divisible by 14. If you add that number of days to the beginning of this year, you'll get the begining of the PayPeriod. The end of the PayPeriod is 14 days after the start_date, but not rolling over to the next year.
What I would then do, is add a before_save callback to Shift to find or create the corresponding PayPeriod
before_save :associate_pay_period
def associate_pay_period
self.pay_period_id = PayPeriod.find_or_create_for(clock_in)
end
Then every PayPeriod will have a bunch of Shifts, and every Shift will belong to a PayPeriod.
If, for example, you wanted to get all of the shifts for a specific employee during a specific PayPeriod (to perhaps sum the hours worked for that PayPeriod) add a scope to Shift:
scope :for, -> (user) { where(user: user) }
And call pay_period.shifts.for(user)
UPDATE #2:
One other (much simpler) thought I had (if you don't want to create an actual PayPeriod model), would be to just add a method to the model that has clock_in (I'm going to refer to it as Shift):
def pay_period
clock_in.to_date.mjd / 14
end
Which will basically just boil down any clock_in time to an integer that represents a 14 day period. Then you can call
Shift.all.group_by { |shift| shift.pay_period }
If you need each pay_period to be contained within a single calendar year, you can do:
def pay_period
[clock_in.year, clock_in.to_date.yday / 14]
end
My user can have many questions, however the questions are asked in different frequencies. Like weekly, biweekly, monthly, quarterly. Now I store the frequency of a Question in a QuestionFrequency model. That accepts frequency:string and begins:string.
The values accepted for frequency are:
weekly
biweekly
monthly
quarterly
now I use this together with the begins to understand the setting. So begins accepts:
if its biweekly I note down the week number if wants it to start
(thus I can check if that week number is odd or even)
if it's monthly it saves "end" or "beginning" thus I can check if its beginning of month with rails.
quarterly it saves "end" or "beginning"
Thus I can call
question.question_frequency.frequency
f.ex to get one of the 4 accepted values. Now what I'm trying to do is create a grouped list of all questions that might be available to the User in this week.
I have a method in my user model called all_questions, which job it is to get all questions that is relevant to a user "this" week.
# Collection of Users weekly questions
def all_questions
questions
end
now how can I filter "questions" to get things like
if biweekly.odd? and Time.zone.now.strftime("%V").odd?
then add that question whilst if one is odd || even then we don't want that question this week.
I would handle it differently.
Remove the QuestionFrequency model.
Add a frequency column to Question as an integer and use Rails' Enum method to define the frequency names.
Add a valid_at date/datetime column to the Question model and have it set to the next valid date (either 1 week from now, 2 weeks, 1 month, etc.) depending on the frequency.
Now, once a question is shown to the user (or when it's answered), have the valid_at column update for the question according to its frequency:
##question.rb example
enum frequency: [:weekly, :biweekly, :monthly]
before_save :update_valid_at
def update_valid_at
if weekly?
self[:valid_at] = 1.week.from_now
elsif biweekly?
self[:valid_at] = 2.weeks.from_now
elsif monthly?
self[:valid_at] = 1.month.from_now
end
end
This way, you can change your all_questions to:
def all_questions
questions.where('valid_at < ?', Date.today)
end
I have a model with the fields "date" and "frequency" (Frequency is an integer). I'm trying to get the top 5 frequencies per date.
Essentially I want to group by date, then get the top 5 per group.
What I have so far only retrieves the top 1 in the group:
Observation.channel("channelOne").order('date', 'frequency desc').group(:date).having('frequency = MAX(frequency)')
I want the MAX(frequency) PLUS the second, third, fourth and fifth largest PER DATE.
Sorry if this is really simple or if my terminology is off; I've just started with rails :)
You can use this:
Observation
.select("obs1.*")
.from("observations obs1")
.joins("LEFT JOIN observations AS obs2 ON obs1.date = obs2.date AND obs1.frequency <= obs2.frequency")
.group("obs1.date, obs1.id")
.having("count(*) <= 5")
.order("obs1.date, obs2.frequency")
This query returns the top 5 frequencies for each date.
I have an array of hashes. Something like this...
transactions = [{"date"=>"2014-07-21", "amount"=>200},
{"date"=>"2012-06-21", "amount"=>400},
{"date"=>"2014-08-21", "amount"=>100},
{"date"=>"2014-08-12", "amount"=>150},
{"date"=>"2014-06-15", "amount"=>230}
{"date"=>"2013-05-21", "amount"=>900},]
I want to be able to save each months total amounts and then show the most recent 3 months to todays date and their total amount. Something like this...
Totals: 06-14 $230
07-14 $200
08-14 $250
I have this method but i am not sure how to get only the last 3 months to put in my database field and how to print it out.
def income_by_month
#payroll_transactions = current_user.transactions
#recent_payroll = #payroll_transactions.find_all {90.days.ago.to_date..Date.today}.map #finds transactions within 90 days
#amount_by_month = #recent_payroll.group_by { |t| t.date.to_date.month }.map do |month, transactions|
[month, transactions.sum(:amount)] #Groups transactions by month and adds month total
end.to_h
-EDIT-
I figured out a method to only get the transactions from the last 30 days I updated my method to show it. Now my question is how do I save the answer (Do i save it in one field as an Array?) and then how to show the answer in my view. Like I show it here. How do I print each key and value line by line in an order?
Totals: 06-14 $230
07-14 $200
08-14 $250
-EDIT-
Sorry my database is a mongoid db. And I want to save the most recent 3 months to todays date regardless of if an amount is available.
Let me start with a quick note on your code snippet:
group_by { |t| t.date.to_date.month }
Note that grouping objects by a single month does not take a year in count, so it would end summing up amounts for transactions of both 2012 and 2014 years in a one container. So what you really want is to group based on both month and year values.
Thinking of reducing the amount of redundant iterations through the input array (and using unnecessary aggregations), I've came to the following suggestion:
last_months = transactions.map{|i| Date.parse(i["date"]).strftime("%m-%Y")}.uniq.sort.last(3)
result = last_months.inject({}){|result, input| result[input] = 0; result}
transactions.inject(result) do |result, object|
# NOTE: we're already doing dates parsing and strftime two times here.
# In case you operate on Date objects themselves in your code, this is not the case.
# But the real perfomance measurement between summing all values up
# and strftiming more than once should be done additionally.
month = Date.parse(object["date"]).strftime("%m-%Y")
result[month] += object["amount"] if result[month]
result
end
# result now equals to {"06-2014"=>230, "07-2014"=>200, "08-2014"=>250}
First, we obtain those last three months (and years).
Next we create a hash to contain aggregated values with only those last months keys. At the end we sum up amount for only those transactions which seem to be one of the latter 3 months.
So, as long as ruby hashes (ruby v.1.9+) preserve the keys order, you can simply iterate over them to print out:
result.each{|k,v| puts "#{k}: #{v}"}
# 06-2014: 230
# 07-2014: 200
# 08-2014: 250
One last thing to note here is that doing this kind of aggregation inside of your server code is not quite efficient. Much more tempting option would be to move this calculations to your database layer.
ActiveSupport has some pretty slick date methods like Date#beginning_of_month:
require "date"
require "active_support/core_ext"
def process_transaction_group(month, transactions)
{
month: month.strftime("%Y/%m"),
total: transactions.map {|t| t["amount"] }.reduce(:+)
}
end
def process_transactions(transactions)
transactions
.group_by {|t| Date.parse(t["date"]).beginning_of_month }
.select {|month, _trxs| month < 3.months.ago }
.map {|month, trxs| process_transaction_group(month, trxs) }
end
###############
transactions = [{"date"=>"2014-07-21", "amount"=>200},
{"date"=>"2012-06-21", "amount"=>400},
{"date"=>"2014-08-21", "amount"=>100},
{"date"=>"2014-08-12", "amount"=>150},
{"date"=>"2014-06-15", "amount"=>230},
{"date"=>"2013-05-21", "amount"=>900}]
process_transactions(transactions)
#=> [{:month=>"2014/07", :total=>200}, {:month=>"2012/06", :total=>400}, {:month=>"2014/08", :total=>250}, {:month=>"2014/06", :total=>230}, {:month=>"2013/05", :total=>900}]
I have a rails 4 (ruby 2) app that tracks time for employees against various companies. I need to get a sum of the minutes per company per date. My problem is I'm not sure the best way to pad date/company pairs with 0 if there are no time entries for that company on that day.
Tables
Companies Time_Entries
id name ... id, created_at, company_id, minutes ...
Current output given only 2 companies and 2 days,
[{"company_id":1,"company_name":"Company A","date":"2013-06-24","minutes":987},
{"company_id":1,"company_name":"Company A","date":"2013-06-25","minutes":5},
{"company_id":2,"company_name":"Company B","date":"2013-06-24","minutes":500}]
Expected output to do is pad days that aren't recorded with 0's is to have an additional item in the list where the last item is the new item.
[{"company_id":1,"company_name":"Company A","date":"2013-06-24","minutes":987},
{"company_id":1,"company_name":"Company A","date":"2013-06-25","minutes":5},
{"company_id":2,"company_name":"Company B","date":"2013-06-24","minutes":500},
{"company_id":2,"company_name":"Company B","date":"2013-06-25","minutes":0}]
Current Query (PostgreSQL)
#minutes = TimeEntry.where("created_at >= ?", 1.week.ago.utc)
.group('companies.id, date(created_at)')
.joins(:company)
.select("companies.id as company_id", "companies.name as company_name", "date(created_at)", "SUM(minutes) as minutes")
.order("date ASC")
I'm not sure the best way to go about this. I can think of a couple options:
A 3 deep loop that loops through days, than a loop through companies, than a loop through found results to add any day/company pairs that have not already been added.
Do a left join on a generate_series() for a date range in postgresq and coalesce null sums to 0, but I don't think that will get me all the way
Some unknown better more elegant option