Google CalendarV3 Event date format - error - ruby-on-rails

it's related to Google::Apis::CalendarV3::Event date format error
while I execute my code to add an event to a google calendar i get this error message
invalid: Start and end times must either both be date or both be dateTime.
I tried many ways, I post you 2 of them, I can't make it work !
Is their any helper function in Google API consuming a 'DateTime' and returning to correct string ?
thnaks
Gregoire
start = DateTime.new(2017, 12, 9, 12, 0, 0)
ende = DateTime.new(2017, 12, 9, 12, 0, 0)
event = Google::Apis::CalendarV3::Event.new(
summary: 'test',
description: 'desc',
start: { datetime: start },
end: { datetime: ende }
)
# event = Google::Apis::CalendarV3::Event.new(
# summary: 'test',
# description: 'desc',
# start: { datetime: start.strftime("%Y-%m-%dT%l:%M:%S.000-07:00") },
# end: { datetime: ende.strftime("%Y-%m-%dT%l:%M:%S.000-07:00") }
# )
result = calendar.insert_event('primary', event)
puts "Event created: #{result.html_link}"
Ps : I write software for more than 20 years now, and we still have the same date/time problems !

Finaly the solution is :
date_time: start.to_datetime.rfc3339,

Related

How to make working hours in ruby ​on rails?

I'm creating a chat panel with a bot. Every message sent by the bot will depend on working hours. For example, during business hours the customer sends a message to the bot and the bot will reply to the text message with the sentence: Hello, you sent a message during business hours
and for example when outside working hours the customer sends a text message to the bot and the bot will reply to the message with the sentence: Hello, you sent a message outside of working hours. Where working hours apply every Monday-Saturday at 08: 30-17: 00.
I made a configuration using an array like this:
start_on = [
"Monday, 08:30:00",
"Tuesday, 08:30:00",
"Wednesday, 08:30:00",
"Thursday, 08:30:00",
"Friday, 08:30:00",
"Saturday, 08:30:00"
]
end_on = [
"Monday, 17:00:00",
"Tuesday, 17:00:00",
"Wednesday, 17:00:00",
"Thursday, 17:00:00",
"Friday, 17:00:00",
"Saturday, 17:00:00"
]
And what I want to ask is how to set the current time current_time = (Time.now.to_time) by configuring the working hours in the array start_on and end_on using ruby ​​on rails?
This function returns true if the day is not sunday and the time is between 08:30 and 17:00
def is_working_hour?(time)
!time.sunday? && time.to_s(:time).between?('08:30','17:00')
end
is_working_hour?(Time.now)
I would personally structure the start and end times like this using wday as the day of the week (0 is sunday so 1 == monday):
times = [
{ day: 1, start: "08:30:00", end: "17:00:00" },
{ day: 2, start: "08:30:00", end: "17:00:00" },
...
]
Once you've got a structure like this (you can work it work it out from the above too of course), you can use the following to get today's times.
today = times.detect { |time| time[:day] == Time.now.wday }
Then start and end times are just:
start_time = Time.parse(today[:start])
end_time = Time.parse(today[:end])
# start_time = Time.parse("08:30:00")
# end_time = Time.parse("17:00:00")
Then it's simply a matter of seeing if the current time is inside that range:
Time.now > start && Time.now < end_time
=> true
To convert the values in DateTime, you have to strptime the weekday for Date, and the time for the Time. Then combine the resulting date and time to obtain the DateTime.
curr_index=1 #loop index if necessary
#get start datetime
arr=start_on[curr_index].split(",")
d = Date.strptime(arr[0].strip, '%A')
t = Time.strptime(arr[1].strip, ' %T')
startime = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
#get end datetime
arr=end_on[curr_index].split(",")
d = Date.strptime(arr[0].strip, '%A')
t = Time.strptime(arr[1].strip, ' %T')
endtime = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
return (startime..endtime).cover? Time.now #check if within the range

How to compre DateTime with generic times in ruby

I'm working on a ruby on rails app that can automatically schedule events. Every event has a start_time datetime and an end_time datetime.
Here is the problem:
In general, events shouldn't be scheduled before 9:00am or after 10:30pm. This is true for edge cases as well. I.e, even if an event ends after 9:00am, it shouldn't start before it. Similarly, if an event starts before 10:30pm, it should not go on passed 10:30pm.
How can I go about making sure that every event fits this criteria, when some of these events are on different dates?
The problem i'm having is that, yes, a DateTime object has a date and a time associated with it. But so does a Time object.
This means that if I have an event with a start and end datetime between 9am and 10:30pm, my comparison will always return false if I'm comparing a time object with a datetime object of a different date.
Here is what I had in mind:
def event_out_of_bounds?(event_start_time, event_end_time, min_start_time, max_end_time)
if event_start_time.hour > max_end_time.hour then
return true
end
if event_end_time.hour > max_end_time.hour then
return true
end
if event_start_time.hour < min_start_time.hour then
return true
end
if event_end_time.hour < min_start_time.hour then
return true
end
return false
end
Problems with this code:
It doesn't take into account minutes. I.e., if I ever change min_start_time to 9:30 am, then this method won't work anymore.
There are different dates involved. event_start_time and event_end_time, which are DateTimes, obviously have the same date. However, min_start_time and max_end_time, Which are created using Time.new have a different date (I just default it 2020-01-01). This wouldn't be a big problem if I knew beforehand what the dates would be for the events, however I won't in this case.
Any ideas for an algorithm to make this check elegantly?
events shouldn't be scheduled before 9:00am or after 10:30pm.
I might use Range#cover? for this.
require 'active_support/all'
def validate_schedule(s, e)
min = s.time_zone.local(s.year, s.month, s.day, 9, 0, 0)
max = s.time_zone.local(s.year, s.month, s.day, 22, 30, 0)
(min..max).cover?(s..e)
end
# Tests
et = ActiveSupport::TimeZone.new('Eastern Time (US & Canada)')
t = ->(d, h, m) { et.local(2020, 2, d, h, m, 0) }
test = ->(s, e) { puts format('%s %s = %s', s, e, validate_schedule(s, e)) }
test[t[1, 8, 59], t[1, 22, 30]] # false, start too early
test[t[1, 9, 0], t[1, 22, 30]] # true, 10:30 pm ok
test[t[1, 9, 0], t[1, 21, 30]] # true, 9:30 pm also ok
test[t[1, 9, 0], t[1, 22, 31]] # false, end too late
test[t[1, 9, 0], t[2, 22, 30]] # false, no all night parties
If you allow multi-day events, simply calculate max from the end date (e) instead:
max = e.time_zone.local(e.year, e.month, e.day, 22, 30, 0)
Range.cover? works with "any objects that can be compared using the <=> operator." (see Custom Objects in Ranges
Approach
It's curious that the question is framed in terms of DateTime objects, considering that the underlying problem seems to be just determining whether the scheduled time of a sporting match falls within a given window of time. Using DateTime objects is but one way of addressing the problem, but is not the only way. In that sense the question may fall under the heading of X-Y problem.
In fact, I find the use of DateTime objects complicates the problem unnecessarily. It's easier to express each time as simply a two-element array that contains the hour and minute. For example, the time-window that extends from 12:30 (24-hour clock) to 22:00 that evening could be expressed as the range:1
window = [12, 30]..[22, 0]
Event times could be defined similarly:2
event_time = [15, 45]..[19, 30]
For convenience, I will refer to the two-element arrays that are the endpoints of these ranges as simply "times".
An important consideration is that the start and end time of a sporting event does not necessarily fall on the same day. An example would be a tournament at which matches must fall within a window between 12:30 (24 hour clock) and 1:00 (AM) the following day. This is not unusual. This time-window would be expressed:
window = [12, 30]..[1, 0]
I assume:
end times are less than 24 hours after their corresponding start times; and
the time-window is the same every day.
Adjustments could of course be made to the code below if either assumption does not hold.
Code
Let's construct a method valid? that returns true (else false) if a given event falls within a given time-window:
def valid?(event_time, window)
e = time_to_mins(event_time.begin)..time_to_mins(event_time.end)
w = time_to_mins(window.begin)..time_to_mins(window.end)
if w.begin <= w.end
w.cover?(e)
else
(w.begin <= e.begin || e.begin <= w.end) &&
(w.begin <= e.end || e.end <= w.end)
end
end
def time_to_mins(time_arr)
60 * time_arr.first + time_arr.last
end
Examples
time_to_mins [ 0, 10] #=> 10
time_to_mins [22, 30] #=> 1350
window = [ 9, 30]..[22, 30]
valid?([13, 45]..[16, 30], window) #=> true
valid?([ 8, 45]..[11, 30], window) #=> false
valid?([22, 15]..[ 0, 15], window) #=> false
window = [12, 30]..[1, 0]
valid?([16, 15]..[19, 30], window) #=> true
valid?([22, 45]..[ 0, 30], window) #=> true
valid?([ 0, 15]..[ 0, 45], window) #=> true
valid?([22, 45]..[ 1, 30], window) #=> false
valid?([ 0, 15]..[ 1, 45], window) #=> false
1. If desired, one might alternatively express times as hashes, such as { hour: 12, min: 30, sec: 0 }.
2. Information for each event might be held in a larger data structure, such as event = { start_date: [2020, 2, 7], time_block: [[22, 45, 0]..[ 0, 30, 0]], player_1: 'R. Federer', player_2: 'R. Nadal' }, using only event[:time_block] in the above calculation.

Flatten overlapping sequences into a contiguous set

For a Ruby on Rails planning application I am running into an algorithm / combination problem that I have trouble solving efficiently.
In my application I have 2 types of records:
Availabilities (when is someone freely available, on stand-by or explicitly unavailable (sick, vacation))
Plan records (when is someone actually scheduled in).
Both types of records are defined by a start and end time, and availabilities have an additional type (available, stand-by, unavailable).
Now I would like to get a flat list of non-overlapping periods that show me when someone has plan records first, but additionally has availabilities
To give an example:
Time: 0-----------6-----------12-----------18-----------24
Avail: |-----available-----||--standby--|
Plans: |------------------|
Result: |------||------------------||----|
The desired result is 3 non-overlapping periods:
3-6: Available
6-15: Planned
15-18: Standby
Another example, where an Availability needs to be split:
Time: 0-----------6-----------12-----------18-----------24
Avail: |-----available-----||--standby--|
Plans: |-----|
Result: |------||-----||----||-----------|
The desired result is 3 non-overlapping periods:
3-6: Available
6-9: Planned
9-12: Available
12-18: Standby
I already have all (overlapping) periods in an array. What is the best way to achieve what I want efficiently?
I assume that we are given hour ranges for 'plan', 'available' and 'coverage', where 'coverage' is the 'available` range preceded and followed by 'stand-by' ranges. Moreover, 'coverage' contains 'plan' and either or both of its 'stand-by' ranges may be of zero duration.
Code
def categories(avail, plan)
adj_avail = adj_avail(avail)
adj_plan = adj_plan(avail, plan)
arr = []
finish = [adj_avail[:available][:start], adj_plan[:start]].min
add_block(arr, :standby, adj_avail[:coverage][:start], finish)
start = finish
finish = [adj_avail[:available][:finish], adj_plan[:start]].min
add_block(arr, :available, start, finish)
start = finish
add_block(arr, :standby, finish, adj_plan[:start])
arr << [:plan, adj_plan]
finish = [adj_plan[:finish], adj_avail[:available][:finish]].max
add_block(arr, :available, adj_plan[:finish], finish)
add_block(arr, :standby, finish, adj_avail[:coverage][:finish])
restore_times(arr)
end
def adj_avail(avail)
avail.each_with_object({}) do |(k,g),h|
start, finish = g[:start], g[:finish]
h[k] = case k
when :coverage
{ start: start, finish: finish + (finish < start ? 24 : 0) }
else # when :available
{ start: start + (start < h[:coverage][:start] ? 24 : 0),
finish: finish + (finish < start ? 24 : 0) }
end
end
end
def adj_plan(avail, plan)
{ start: plan[:start] + (plan[:start] < avail[:coverage][:start] ? 24 : 0),
finish: plan[:finish] + (plan[:finish] < plan[:start] ? 24 : 0) }
end
def add_block(arr, value, curr_epoch, nxt_epoch)
arr << [value, { start: curr_epoch, finish: nxt_epoch }] if nxt_epoch > curr_epoch
end
def restore_times(arr)
arr.map! do |k,g|
start, finish = g.values_at(:start, :finish)
start -= 24 if start > 24
finish -= 24 if finish > 24
[k, { start: start, finish: finish }]
end
end
Examples
avail = { coverage: { start: 3, finish: 18 },
available: { start: 3, finish: 12 } }
plan = { start: 6, finish: 15 }
categories(avail, plan)
#=> [[:available, {:start=>3, :finish=>6} ],
# [:plan, {:start=>6, :finish=>15} ],
# [:standby, {:start=>15, :finish=>18}]]
avail = { coverage: { start: 22, finish: 11 },
available: { start: 23, finish: 10 } }
plan = { start: 24, finish: 9 }
categories(avail, plan)
#=> [[:standby, {:start=>22, :finish=>23}],
# [:available, {:start=>23, :finish=>24}],
# [:plan, {:start=>24, :finish=>9 }],
# [:available, {:start=>9, :finish=>10}],
# [:standby, {:start=>10, :finish=>11}]]
avail = { coverage: { start: 1, finish: 13 },
available: { start: 2, finish: 3 } }
plan = { start: 4, finish: 12 }
categories(avail, plan)
#=> [[:standby, {:start=>1, :finish=>2 }],
# [:available, {:start=>2, :finish=>3 }],
# [:standby, {:start=>3, :finish=>4 }],
# [:plan, {:start=>4, :finish=>12}],
# [:standby, {:start=>12, :finish=>13}]]
Explanation
The main complication here is when the finish hour for 'coverage' is less than the start hour, meaning that the 'coverage' range contains midnight. When this occurs, the 'available' and 'plan' ranges may also contain midnight. I've dealt with this by adding 24 hours to hours after midnight before computing the ranges and then subtracting 24 hours from all hour values that exceed 24 after computing the ranges.
Consider the second example above.
avail = { coverage: { start: 22, finish: 11 },
available: { start: 23, finish: 10 } }
adj_avail(avail)
#=> {:coverage=> {:start=>22, :finish=>35},
# :available=>{:start=>23, :finish=>34}}
plan = { start: 24, finish: 9 }
adj_plan(avail, plan)
#=> {:start=>24, :finish=>33}
If I execute categories for these values of avail and plan, with the last line commented out, I obtain
a = categories(avail, plan)
#=> [[:standby, {:start=>22, :finish=>23}],
# [:available, {:start=>23, :finish=>24}],
# [:plan, {:start=>24, :finish=>33}],
# [:available, {:start=>33, :finish=>34}],
# [:standby, {:start=>34, :finish=>35}]]
and
restore_times(a)
#=> the return value shown above

Rails effective way of looping through daterange

I have ScheduleDay model as;
=> #<ActiveRecord::Relation [#<ScheduleDay id: 1, from: "2017-01-21 00:00:00", to: "2017-01-30 00:00:00", weekday: 100, weekend: 200, weekly: 90, available: true, check_in: "09:00", check_out: "18:00", min_stay: 2, sevendays: false, check_in_day: nil, created_at: "2017-01-14 11:25:18", updated_at: "2017-01-14 11:27:00">, #<ScheduleDay id: 2, from: "2017-02-05 00:00:00", to: "2017-02-15 00:00:00", weekday: 150, weekend: 200, weekly: 140, available: nil, check_in: "09:00", check_out: "18:00", min_stay: 3, sevendays: false, check_in_day: nil, created_at: "2017-01-14 15:53:21", updated_at: "2017-01-14 15:54:43">, #<ScheduleDay id: 3, from: "2017-03-15 00:00:00", to: "2017-06-15 00:00:00", weekday: nil, weekend: nil, weekly: nil, available: false, check_in: nil, check_out: nil, min_stay: 1, sevendays: false, check_in_day: nil, created_at: "2017-01-14 15:56:01", updated_at: "2017-01-14 15:56:57">, #<ScheduleDay id: 4, from: "2017-08-05 00:00:00", to: "2017-09-30 00:00:00", weekday: 500, weekend: 500, weekly: 500, available: true, check_in: "09:00", check_out: "18:00", min_stay: 7, sevendays: true, check_in_day: "Saturday", boat_id: nil, created_at: "2017-01-14 16:01:00", updated_at: "2017-01-14 16:02:10">]>
I have another date range and I would like to loop over my date range in ScheduleDay table and if record is found get some data. If not found then get data as well. I tried couple different way but I m looking for the most efficient way in ruby.
first = Date.parse("05.2.2017")
last = (Date.parse("16.2.2017"))
schedules = ScheduleDay.where(from: Date.today..(last+1.day))
dates = []
(first..last).each do |date|
flag = 0
catch :date_found do
schedules.each do |s|
(s.from.to_date..s.to.to_date).each do |d|
puts date
puts d
if date == d
flag += 1
puts 'found'
throw :date_found
end
end
end
if flag == 0
puts 'date not found'
end
end
end
This is what I have tried so far. But the problem here is I can not access the record if it is not found. If I take;
if flag == 0
puts 'date not found'
end
inside schedules.each... loop then it prints not found for every record in ScheduleDay. I want to loop through all ScheduleDay record and if not found get some data.
EDIT:
I have database records saved by the user. User can customize any date of the year by selecting date range and pricing. I ask user base pricing as well.
So, lets say user entered 15.01.2017 & 21.01.2017 custom price: 100 and base price 200 (base price will be used if date is not found on database date-range).
I would like to return json array of February. So I need;
01.01.2017 -> custom price exists? if not get base price
02.01.2017 -> custom price exists? if not get base price
...
15.01.2017 -> yes! custom price exists get custom price
16.01.2017 -> yes! custom price exists get custom price
17.01.2017 -> yes! custom price exists get custom price
18.01.2017 -> yes! custom price exists get custom price
...
21.01.2017 -> yes! custom price exists get custom price
...
31.01.2017 -> custom price exists? if not get base price
then create json
EDIT2
What if I use;
first = Date.parse("05.2.2017")
last = Date.parse("16.2.2017")
(first..last).each do |date|
schedules.each do |s|
d2 = (s.from.to_date..s.to.to_date)
puts d2 === date
puts d2
puts date
end
end
=== checks date is in the range.

What is the best way of categorising segments of a time period?

I am developing a staff rota system. For payroll I need to calculate the correct rate of pay depending on the date/time period the shift covers.
How can I check for various date periods (weekend, holidays, weekday) without using a long chain of conditional statements with lengthy, verbose conditions.
Given any time range (a shift):
eg. 2015-01-20 15:00 --> 2015-01-21 17:00
What would be the best (and most efficient way) of categorising segments of the this period?
I would like to know:
The period (if any) between 22:00 and 07:00 on any weekday (Monday -
Friday) evening.
The period (if any) falling between 08:00 on a Saturday and 22:00 on a Sunday.
The period (if any) falling on a public holiday (using the
holidays gem)
So my two questions then are:
1) Knowing that a time period (shift) could span a weekend (although I would prefer a solution that would support a span of many days), how do I calculate which date/time ranges to compare against?
2) Once I have determined the time periods (weekends, holidays etc) to compare against, how do I best determine the intersection of these periods and determine the duration of them?
I don't fully understand your question, so I've put together some code that is based on many assumptions about the problem you face. I hope some of the issues I've addressed, and the way I've dealt with them, may be helpful to you. For example, if a worker is still working when a shift ends, it is necessary to identify the next shift, which may be the next day.
You'll see that my code is very rough and poorly structured. I have many temporary variables that are there just to help you follow what's going on. In a real app, you might want to add some classes, more methods, etc. Also, assuming the data will be stored in a database, you might want to use SQL for some of the calculations.
First, what I've assumed to be the data.
Data
A list of holidays:
holidays = ["2015:04:05", "2015:04:06"]
Information for each employee, including the employee's job classification, with keys being the employee id:
employees = {
123 => { name: 'Bob', job: :worker_bee1 },
221 => { name: 'Sue', job: :worker_bee2 }
}
Groups of days having the same daily periods, with pay rates the same for all days of the group, for each job and period within the day, unless the day falls on a holiday:
day_groups = { weekdays: [1,2,3,4,5], weekends: [6,0] }
Information for each work period:
work_periods = {
weekday_graveyard: {
day_group: :weekdays,
start_time: "00:00",
end_time: "08:00" },
weekday_day: {
day_group: :weekdays,
start_time: "08:00",
end_time: "16:00" },
weekday_swing: {
day_group: :weekdays,
start_time: "16:00",
end_time: "24:00" },
weekend: {
day_group: :weekends,
start_time: "00:00",
end_time: "24:00" } }
A wage schedule by job, dialy period, for non-holidays and holidays:
wage_by_job = {
worker_bee1: {
weekday_graveyard: { standard: 30.00, holiday: 60.00 },
weekday_day: { standard: 20.00, holiday: 40.00 },
weekday_swing: { standard: 25.00, holiday: 50.00 },
weekend: { standard: 22.00, holiday: 44.00 }
},
worker_bee2: {
weekday_graveyard: { standard: 32.00, holiday: 64.00 },
weekday_day: { standard: 22.00, holiday: 44.00 },
weekday_swing: { standard: 27.00, holiday: 54.00 },
weekend: { standard: 24.00, holiday: 48.00 }
}
}
Hours worked by all employees during the pay period:
shifts_worked = [
{ id: 123, date: "2015:04:03", start_time: "15:30", end_time: "00:15" },
{ id: 221, date: "2015:04:04", start_time: "23:30", end_time: "08:30" },
{ id: 123, date: "2015:04:06", start_time: "08:00", end_time: "16:00" },
{ id: 221, date: "2015:04:06", start_time: "23:00", end_time: "07:00" },
{ id: 123, date: "2015:04:07", start_time: "00:00", end_time: "09:00" }
]
Helpers
require 'set'
require 'date'
def date_str_to_obj(date_str)
Date.strptime(date_str, '%Y:%m:%d')
end
date_str_to_obj("2015:04:04")
#=> #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>
def next_day(day)
(day==6) ? 0 : day+1
end
next_day(6)
#=> 0
Convert holidays to date objects and store in a set for fast lookup:
#holiday_set = Set.new(holidays.map { |date_str|
date_str_to_obj(date_str) }.to_set)
#=> #<Set: {#<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>,
# #<Date: 2015-04-06 ((2457119j,0s,0n),+0s,2299161j)>}>
def is_holiday?(date)
#holiday_set.include?(date)
end
is_holiday?(date_str_to_obj("2015:04:04"))
#=> false
is_holiday?(date_str_to_obj("2015:04:05"))
#=> true
Map each day of the week to an element of day_groups:
#day_group_by_dow = day_groups.each_with_object({}) { |(period,days),h|
days.each { |d| h[d] = period } }
#=> {1=>:weekdays, 2=>:weekdays, 3=>:weekdays, 4=>:weekdays,
# 5=>:weekdays, 6=>:weekend, 0=>:weekend}
Map each element of day_groups to an array of work periods:
#work_periods_by_day_group = work_periods.each_with_object({}) { |(k,g),h|
h.update(g[:day_group]=>[k]) { |_,nwp,owp| nwp+owp } }
#=> {:weekdays=>[:weekday_graveyard, :weekday_day, :weekday_swing],
# :weekend=> [:weekends]}
Compute hours worked within a work period:
def start_and_end_times_to_hours(start_time, end_time)
(time_to_minutes_after_midnight(end_time) -
time_to_minutes_after_midnight(start_time))/60.0
end
start_and_end_times_to_hours("10:00", "14:30")
#=> 4.5
A helper for the previous method:
private
def time_to_minutes_after_midnight(time_str)
hrs, mins = time_str.scan(/(\d\d):(\d\d)/).first.map(&:to_i)
60 * hrs + mins
end
public
time_to_minutes_after_midnight("10:00")
#=> 600
time_to_minutes_after_midnight("14:30")
#=> 870
As indicated by the method name:
def date_and_time_to_current_period(date, time, work_periods)
day_grp = #day_group_by_dow[date.wday]
periods = #work_periods_by_day_group[day_grp]
periods.find do |per|
p = work_periods[per]
p[:start_time] <= time && time < p[:end_time]
end
end
date_and_time_to_current_period(date_str_to_obj("2015:04:03"),
#=> :weekday_swing
date_and_time_to_current_period(date_str_to_obj("2015:04:04"),
#=> :weekend
Lastly, another self-explanatory method:
def next_period_and_date_by_period_and_date(work_periods, period, date)
end_time = work_periods[period][:end_time]
if end_time == "24:00" # next_day
day_grp = #day_group_by_dow[next_day(date.wday)]
wp = #work_periods_by_day_group[day_grp]
[wp.find { |period| work_periods[period][:start_time]=="00:00" }, date+1]
else # same day
day_grp = work_periods[period][:day_group]
wp = #work_periods_by_day_group[day_grp]
[wp.find { |period| work_periods[period][:start_time]==end_time }, date]
end
end
next_period_and_date_by_period_and_date(work_periods,
:weekday_day, date_str_to_obj("2015:04:03"))
#=> [:weekday_swing, #<Date: 2015-04-03 ((2457116j,0s,0n),+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:02"))
#=> [:weekday_graveyard, #<Date: 2015-04-03...+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:03"))
#=> [:weekend, #<Date: 2015-04-04 ((2457117j,0s,0n),+0s,2299161j)>]
next_period_and_date_by_period_and_date(work_periods,
:weekday_swing, date_str_to_obj("2015:04:04"))
#=> [:weekend, #<Date: 2015-04-05 ((2457118j,0s,0n),+0s,2299161j)>]
Calculation of payroll
shifts_worked.each_with_object(Hash.new(0.0)) do |shift, payroll|
id = shift[:id]
date = date_str_to_obj(shift[:date])
start_time = shift[:start_time]
end_time = shift[:end_time]
wage_schedule = wage_by_job[employees[id][:job]]
curr_period = date_and_time_to_current_period(date, start_time, work_periods)
while true
end_time_in_period = work_periods[curr_period][:end_time]
end_time_in_period = end_time if
(end_time > start_time && end_time < end_time_in_period)
hours_in_period =
start_and_end_times_to_hours(start_time, end_time_in_period)
wage = wage_schedule[curr_period][is_holiday?(date) ? :holiday : :standard]
payroll[id] += (wage * hours_in_period).round(2)
break if end_time == end_time_in_period
curr_period, date =
next_period_and_date_by_period_and_date(work_periods,
curr_period, date)
start_time = work_periods[curr_period][:start_time]
end
end
#=> {123=>795.5, 221=>698.0}
I've used the following gem:
https://github.com/fnando/recurrence
I haven't done Holidays yet.
Requirement
class Requirement < ActiveRecord::Base
# Model with attributes:
# start - datetime
# end - datetime
# is_sleepin - boolean
def duration
self.end - self.start
end
def to_range
self.start..self.end
end
def spans_multiple_days?
self.end.to_date != self.start.to_date
end
end
Breakdown of duration of requirement (shift)
class Breakdown
attr_reader :requirement,
:standard_total_duration,
:weekend_total_duration,
:wakein_total_duration
def initialize(requirement)
#requirement = requirement
#rules = Array.new
#rules << Breakdown::StandardRule.new(self)
#rules << Breakdown::WeekendRule.new(self)
#standard_total_duration = components[:standard].total_duration
#weekend_total_duration = components[:weekend].total_duration
if #requirement.is_sleepin?
#standard_total_duration = 0
#weekend_total_duration = 0
end
# Following is a special set of rules for certain Locations where staff work
# If a requirement is_sleepin? that means duration is not counted so set to 0
if ['Home 1', 'Home 2'].include?(#requirement.home.name.strip) and
#requirement.spans_multiple_days? and not #requirement.is_sleepin?
#rules << Aspirations::Breakdown::WakeinRule.new(self)
#wakein_total_duration = components[:wakein].total_duration
#standard_total_duration = 0
#weekend_total_duration = 0
end
end
def components
#rules.hmap{ |k,v| [ k.to_sym, k ] }
end
end
Which uses Rules to specify which parts of a shifts duration should be categorised:
module Breakdown
class Rule
def initialize(breakdown)
#requirement = breakdown.requirement
end
def to_sym
# eg 'Breakdown::StandardRule' becomes :standard
self.class.name.split('::').last.gsub("Rule", "").downcase.to_sym
end
def periods
output = []
occurrences = rule.events.map{ |e| rule_time_range(e) }
occurrences.each do |o|
if (o.max > #requirement.start and #requirement.end > o.min)
output << (o & #requirement.to_range)
end
end
return output
end
def total_duration
periods.reduce(0) { |sum, p| sum + (p.max - p.min).to_i }
end
end
end
Example of a Rule (in this case a weekend rule)
module Breakdown
class WeekendRule < Breakdown::Rule
def period_expansion
# This is an attempt to safely ensure that a weekend period
# is detected even though the start date of the requirement
# may be on Sunday evening, maybe could be just 2 days
4.days
end
def period_range
(#requirement.start.to_date - period_expansion)..(#requirement.end.to_date + period_expansion)
end
def rule
Recurrence.new(:every => :week, :on => :saturday, :starts => period_range.min, :until => period_range.max)
end
def rule_time_range(date)
# Saturday 8am
start = date + Time.parse("08:00").seconds_since_midnight.seconds
_end = (date + 1.day) + Time.parse("22:00").seconds_since_midnight.seconds
start.._end
end
end
end
One possible approach might be to break a week up into individual chunks of, say, 15 minutes (depending on how much resolution you need). Instead of ranges of time that are somewhat hard to deal with, you can then work with sets of these chunks, which Ruby supports very nicely.
Number the chunks of time sequentially:
Monday 12:00 AM-12:15 AM = 0
Monday 12:15 AM-12:30 AM = 1
...
Sunday 11:45 PM-12:00 AM = 671
Then prepare sets of integers for holidays, weekends, each shift, etc., whatever you need. Except for the holidays, those are probably all constants.
For instance, for your weekend from Saturday 8 AM to Sunday 10 PM:
weekend = [ 512..663 ]
Similarly, represent each employee's attendance as a set. For instance, somebody who worked from 8 AM to noon on Monday and from 10 AM to 11 AM on Saturday would be:
attendance = [ 32..47, 520..523 ]
With this approach, you can use set intersections to figure out how many hours were on weekends:
weekendAttendance = weekend & attendance

Resources