Time difference between dates, including business hour and excluding holidays - mysql

How can I calculate the time difference between two date, considering:
Only Monday to Friday
Time between 9am to 5:30pm;
Exclude Holidays.
Example:
d1 = 2012-10-05 17:00:00
d2 = 2012-14-09 12:00:00
Calculation Steps:
2012-10-05 = 00:30:00
2012-10-06 = 00:00:00
2012-10-07 = 00:00:00
2012-10-08 = 07:30:00
2012-10-09 = 04:00:00
ddiff(d2,d1) = 12:00:00
I know how to do it using only mon-fri, as described here. And I am talking about MySQL.

I've come up with a solution that's relatively straightforward for calculating the time difference for the full interim dates. However it's a bit messy to use mysql for calculating the time difference for the start & end dates. I have included them in my solution, but with a number of assumptions.
In any case, here's the sql
SET #startdate:='2012-12-24 17:00:00';
SET #enddate:='2013-01-02 12:00:00';
SELECT
TIME_TO_SEC(TIMEDIFF(CONCAT(DATE(#startdate),' 17:30:00'), #startdate))/3600 as startday_time,
TIME_TO_SEC(TIMEDIFF(#enddate, CONCAT(DATE(#enddate),' 9:00:00')))/3600 as endday_time,
SUM(daily_hours) as otherdays_time from
(
SELECT 7.5 as daily_hours, id, DATE_ADD(DATE(#startdate),INTERVAL id-1 DAY) as idate from numbers
) dates
LEFT JOIN holidays on DATE(dates.idate) = DATE(holidays.date)
WHERE
idate BETWEEN #startdate AND #enddate
AND holidays.date IS NULL
AND DAYOFWEEK(idate) <> 7 AND DAYOFWEEK(idate) <> 1;
sqlfiddle here:
http://sqlfiddle.com/#!2/ff3f3/1/2
To get the valid interim dates, we'll need two tables - a holidays table listing all the holiday dates and a numbers table that contains a series of integers which is very useful for joining against to get a sequential series of dates (with no gaps).
Note: In the sqlfiddle, I've populated the numbers table only up to 12 to cover the dates used in my example - it will probably need to be populated to a higher number depending on the range of dates you'll be working with.
For the start day time & end day time, I've made the following assumptions:
that start date & end date are both valid dates that should be counted towards the total time
that the time on the start date is between lunch and 17.30
that the time on the end date is between lunch and 17.30
if these assumptions are wrong, you're getting into serious conditional territory (with lots of ifs) and might be best doing this in the php (or whatever).
note: I've left the times (which are in hours) un-added for illustration purposes.

Related

Finding free timeslots between appointments with preferred beginning

I have 2 tables...
appointments:
id (int)
start (datetime)
end (datetime)
timeslots:
id (int)
timeslot (time)
The table timeslots has records like this...
00:00:00
00:05:00
00:10:00
...
23:55:00
The table appointments has one record...
start: 2017-05-15 01:30:00
end: 2017-05-15 02:00:00
With the following simplified query I try to get the first free starting timeslot each hour if there is no prefered hourly begin time while respecting existing appointments...
SET #mandatory=true, #parallel=1, #targetdate="2017-05-15", #begin='30', #duration='00:30:00', #time_start="00:00:00", #time_end="03:00:00";
SELECT MIN(timeslot) AS free_slot FROM timeslots
INNER JOIN
appointments
ON 1=1
WHERE timeslots.timeslot BETWEEN #time_start AND #time_end
AND (ADDTIME(timeslot, #duration) BETWEEN #time_start AND #time_end)
AND ((#mandatory AND MINUTE(timeslot)=#begin) OR (NOT #mandatory))
AND ((
SELECT COUNT(*) AS anzahl
FROM appointments
WHERE (concat(date(#targetdate)," ",timeslots.timeslot) BETWEEN appointments.start AND appointments.end - INTERVAL 1 SECOND
OR concat(date(#targetdate)," ", ADDTIME(timeslot, #duration) ) BETWEEN appointments.start AND appointments.end - INTERVAL 1 SECOND)
) < #parallel)
GROUP BY HOUR(timeslot)
Explanation of parameters:
#mandatory:
TRUE means, I only allow timeslots matching #begin. FALSE means, I prefer(!) timeslots matching #begin, but if there is no match, I would like to get the first free timeslot each hour.
#parallel:
Number of parallel appointments allowed. The value 1 means, that there should be no overlapping appointments. The value 2 means, that there should be only 2 appointments at the same time.
#targetdate:
The query should return timeslots only for the targetdate.
#begin:
The prefered beginning of the free timeslots. '30' means, that I would like to get timeslots starting "half past ..." every hour.
#duration: The needed duration of consecutive free timeslots.
#time_start and #time_end:
The result should only contain records within this time range.
My problem is, that I get either free timeslots only starting at half past ... or the first free timeslot for every full hour - depending on setting #mandatory to TRUE or FALSE.
When setting #mandatory to TRUE I think I get the desired result (only timeslots starting at the defined begin time). But I want to set #mandatory to FALSE and get first free timeslot every hour ONLY when there is no free timeslot beginning with half past (I would like to PREFER the matching begin timeslots, but offer alternative timeslots...
This is what I would like to get:
00:30:00
01:00:00 (this row respects the existing appointment at half past one and returns the first free timeslot for this hour only when the duration of the remaining timeslots fits)
02:30:00
Is there an easy way to get these results?
Best regards

MySQL - How to combine the column data into one row

I am making an employee attendance record and I have trouble merging the
raw data into time in and time out combination format.
From the given "Raw Data" Table below
I need to combine the time in and time out of the employee into one row
like the "Merge Time in/out" sample below.
Also consider that the employee has two shifting schedule the day shift and night shift.
Take note that if the employee is in night shift schedule
the time out date is different to time in date.
Day shift empid(ID001,ID002)
Night shift empid(ID003)
Raw Data Table
--------------------------------------------
empid date time[in/out] in_out
--------------------------------------------
ID001 2014-08-01 7:00am IN
ID002 2014-08-01 7:01am IN
ID003 2014-08-01 8:05pm IN <--Night Shift
ID001 2014-08-01 5:00pm OUT
ID002 2014-08-01 5:01pm OUT
ID003 2014-08-02 6:01am OUT <--take note of date
Merge Time in/out Table
--------------------------------------------
empid date time_in time_out
--------------------------------------------
ID001 2014-08-01 7:00am 5:00pm
ID002 2014-08-01 7:01am 5:01pm
ID003 2014-08-01 8:05pm 6:01am
select r1.empid,
r1.date,
r1.time as time_in,
r2.time as time_out
from raw_Data r1
inner join raw_data r2 on r1.empid = r2.empid
where r1.in_out = 'IN'
and r2.in_out = 'OUT';
Ok, so you can tell if the employee worked the night shift when his time_out was AM. In this case, it's the last row's case.
What I did was determinate a real date field. It is the day before when you're out from the night shift, and the current date in any other case
select empid,
IF(RIGHT(timeinout,2)='am' AND in_out='OUT',
DATE_ADD(date, INTERVAL -1 DAY),
date) as realdate,
MAX(if(in_out='IN',timeinout,null)) as time_in,
MAX(if(in_out='OUT',timeinout,null)) as time_out
from shifts
group by empid, realdate
Outputs
depending on the table size it might be worth using this way just for saving yourself a join. In almost any other case, a join is cleaner.
I guess you have no control over the format of the input, so you'll have to stick to times as text and make a comparison for the am/pm suffix in the last 2 characters. I find that rather error prone, but let's pray the raw data will stick to that format.
This solution makes a few assumptions that I rather explain here to avoid further misunderstandings
The workers can't work the night shift if they worked the day shift (since we're grouping by date, you would need an extra field to distinguish day shift and night shift for a given day)
Input will never list an OUT time earlier than a IN time for a given day/employee tuple (if it happens, this would need an extra verification step to guarantee consistent output)
Input will always include timein and timeout for a given shift (if it didn't, you would need an extra step to discard orfan timeentries).
Try this query and tell me if it works
SELECT empid,
date,
MAX(CASE WHEN in_out = 'IN' THEN time ELSE '' END) time_in,
MAX(CASE WHEN in_out = 'OUT' THEN time ELSE '' END) time_out
FROM Raw Data
GROUP BY empid, date

Finding available timeslots in a day based on existing records in MySQL

I have a table that has these fields:
How would I find all available time slots for that day (based on the day starting at 7am and ending at 10pm) that aren't currently in this table? For example, on one particular day, if all timeslots from 7am till 10pm were taken bar one at 6pm till 7pm, that would be the one result.
The duration of each time slot does not vary - they all last one hour.
I have tried many different things, but I have a feeling I am so far off, it is hardly worth posting what I've tried.
Any help would be greatly appreciated!
One thing I would think about would be having a "hours table" with all the values you wanna check. Then, with a left join and selecting only null values, you'd get only the values you haven't assigned. I built a SQLfiddle http://sqlfiddle.com/#!2/66b441/6 to check it with some dummy values to show how this works:
SELECT h.slot
FROM hours h
LEFT JOIN deliveries d
ON ( h.slot = d.start_time AND date_stamp = '2014-04-04' )
WHERE start_time IS NULL
Check the data in the SQLfiddle, if you know the slots and they don't overlap, with that you will get for the date the values
SLOT
January, 01 1970 07:00:00+0000
January, 01 1970 10:00:00+0000

Get stats for each day in a month without ignoring days with no data

I want to get stats for each day in a given month. However, if a day has no rows in the table, it doesn't show up in the results. How can I include days with no data, and show all days until the current date?
This is the query I have now:
SELECT DATE_FORMAT(FROM_UNIXTIME(timestamp), '%d'), COUNT(*)
FROM data
WHERE EXTRACT(MONTH FROM FROM_UNIXTIME(timestamp)) = 6
GROUP BY EXTRACT(DAY FROM FROM_UNIXTIME(timestamp))
So if I have
Row 1 | 01-06
Row 2 | 02-06
Row 3 | 03-06
Row 4 | 05-06
Row 5 | 05-06
(i changed timestamp values to a day/month date just to explain)
It should output
01 | 1
02 | 1
03 | 1
04 | 0
05 | 2
06 | 0
...Instead of ignoring day 4 and today (day 6).
You will need a calendar table to do something in the form
SELECT `date`, count(*)
FROM Input_Calendar c
LEFT JOIN Data d on c.date=d.date
GROUP BY `date`
I keep a full copy of a calendar table in my database and used a WHILE loop to fill it but you can populate one on the fly for use based on the different solutions out there like http://crazycoders.net/2012/03/using-a-calendar-table-in-mysql/
In MySQL, you can use MySQL variables (act like in-line programming values). You set and can manipulate as needed.
select
dayofmonth( DynamicCalendar.CalendarDay ) as `Day`,
count(*) as Entries
from
( select
#startDate := date_add( #startDate, interval 1 day ) CalendarDay
from
( select #startDate := '2013-05-31' ) sqlvars,
AnyTableThatHasAsManyDaysYouExpectToReport
limit
6 ) DynamicCalendar
LEFT JOIN Input_Calendar c
on DynamicCalendar.CalendarDay = date( from_unixtime( c.date ))
group by
DynamicCalendar.CalendarDay
In the above sample, the inner query can join against as the name implies "Any Table" in your database that has at least X number of records you are trying to generate for... in this case, you are dealing with only the current month of June and only need 6 records worth... But if you wanted to do an entire year, just make sure the "Any Table" has 365 records(or more).
The inner query will start by setting the "#startDate" to the day BEFORE June 1st (May 31). Then, by just having the other table, will result in every record joined to this variable (creates a simulated for/next loop) via a limit of 6 records (days you are generating the report for). So now, as the records are being queried, the Start Date keeps adding 1 day... first record results in June 1st, next record June 2nd, etc.
So now, you have a simulated calendar with 6 records dated from June 1 to June 6. Take that and join to your "data" table and you are already qualifying your dates via the join and get only those dates of activity. I'm joining on the DATE() of the from unix time since you care about anything that happend on June 1, and June 1 # 12:00:00AM is different than June 1 # 8:45am, so matching on the date only portion, they should remain in proper grouping.
You could expand this answer by changing the inner '2013-05-31' to some MySQL Date function to get the last day of the prior month, and the limit based on whatever day in the current month you are doing so these are not hard-coded.
Create a Time dimension. This is a standard OLAP reporting trick. You don't need a cube in order to do OLAP tricks, though. Simply find a script on the internet to generate a Calendar table and join to that table.
Also, I think your query is missing a WHERE clause.
Other useful tricks include creating a "Tally" table that is a list of numbers from 1 to N where N is usually the max of the bigint on your database management system.
No code provided here, as I am not a MySQL guru.
Pseudo-code is:
Select * from Data left join TimeDimension on data.date = timedimension.date

Bringing in Schedules for Access calculations

I really don't know how to ask this question or title it but here I go. I work in a school system and I have created a database for some psychologists to use to track their referrals. By state rules,they have 60 days from the date of their first meeting to finish the process. Weekends still count but HOLIDAYS DO NOT. I do not really know how to use the calender we have so that we have an accurate Calculation. For instance, with Holidays accounted for, if a kid was started today, he would need to have everything finished on 1/18/2013 That is 60 days from now based on our schedule. Does anyone have any idea where I should start?
Edit
Ok, so I now have a Calender table. Here is my issue. I have my column that I used to indicate which days are used in calculating my 60 days. Weekends can be used in that calculation. HOWEVER, they cannot be used in the result. If the 60th day lies on a Sunday or Saturday, then the date would need to go to the Friday before. I guess my first issue is really, how do I limit my calculation to the dates in my calender table?
This can be easy with a calendar table.
PARAMETERS start_date DateTime;
SELECT TOP 1 sub.the_date
FROM
(
SELECT TOP 60 the_date
FROM tblCalendar
WHERE
the_date>=[start_date]
AND work_day=True
ORDER BY the_date
) AS sub
ORDER BY sub.the_date DESC;
That query is based on the assumption you have set work_day to True for the dates you want evaluated. IOW, work_day will be False only for your organization's holidays.
For sample code to create and load your calendar table, see the CreateTable_calendar() and LoadCalendar() procedures at Using Start Date and End date in Access query. To initially assign all dates including weekend days as work days, make this change in LoadCalendar().
'rs!work_day = Not (Weekday(dte) = vbSunday Or _
' Weekday(dte) = vbSaturday)
rs!work_day = True
Finally, manually edit the table to change work_day to False for your holidays.
You can check the weekday to ensure you have not chosen a weekend:
SELECT TOP 1 CalDate, WDay
FROM (SELECT Top 60 c.CalDate,Weekday([Caldate]) AS WDay
FROM Calendar c
WHERE c.Holiday=False) a
WHERE WDay Not In (1,7)
ORDER BY CalDate DESC