Finding empty time blocks between two dates? - php

I have 2 MySQL tables, 'scheduled_time' and 'appointments'
'scheduled_time' has 2 DateTime fields, 'start' and 'end' - this is a time range of when I am available for appointments.
'appointments' contains appointment details but also a 'start' and 'end' field, this will ultimately be within the range specified in 'scheduled_time'.
What is the best way for me to find empty time blocks when taking into account both tables?
Lets say I have 'scheduled_time' starting 11/9/2010 from 8am to 2pm. and I have one 'appointment' from 8am to 10am and one from 1pm to 2pm. How can I find the next available block of say 1 hour?

I did this a while ago. We had a similar structure:
Available (contained all working hours for an employee, flexible working hours)
Appointments (similar to yours)
What I did was basically this (steps):
Get all start and end datetimes for employee < x >, sorted by startdate
let startAvailable = start of the time search (in your case 11/9/2010 # 8am)
let appointment = first appointment in the list of appointments
get the startdate of the first appointment. If the difference between these is big enough, there's your block
if not, let startAvailable = enddate of appointment
remove appointment from the list, let appointment be the next appointment
repeat the process of checking for an available block

First, create a number table. Here as example named "Numbers" with a column "number".
Then you can do something like
select chour as [Free Hour] from Numbers n
inner join Scheduled s on n.chour >= s.start and n.chour < s.[end]
where chour not in
(
select chour from Numbers n
inner join Appointments a ON n.chour >= q.start and n.chour < a.[end]
)
Numbers is my Numbers table, chour is a computed column defined as
DATE_ADD('2010-11-08', INTERVAL number HOUR)
You can also store it as a persisted column of course.
Sorry, if the syntax isn't completely right, I do T-SQL normally :-)
Edit: This technique only works for fixed blocks of time (hour is just an example, you could do half hours just as easily), but it's quite efficient and readable in this case. A usual application in business is mapping dates, because that's the granularity where most contracts live and you can cover a lot of days with a small number table.

This is a great place to use a dummy integer table, which helps you to create data from nothing. Here is an example:
create table ints(i tinyint);
insert into ints values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
You can then use a few cross joins with this table to generate a table of hour-long windows represented by your scheduled_time table, and left join the result against the appointments table to find windows that do not have something already scheduled.
SELECT
h.HourWindowStart, h.HourWindowEnd
FROM (
SELECT
s.start + INTERVAL t.i*100 + u.i*10 + v.i HOUR AS HourWindowStart,
s.start + INTERVAL t.i*100 + u.i*10 + v.i + 1 HOUR AS HourWindowEnd
FROM scheduled_time s
JOIN ints AS t
JOIN ints AS u
JOIN ints AS v
WHERE s.start + INTERVAL t.i*100 + u.i*10 + v.i HOUR < s.end
ORDER BY HourWindowStart
) as h
LEFT JOIN appointments a ON a.end > h.HourWindowStart AND a.start < h.HourWindowEnd
WHERE a.start IS NULL
You can tweak various parts of this process to calculate larger/smaller availability windows (by the half hour, by the day, etc), use more or less cross joins of the integer table based on the maximum number of availability windows that could be represented in a single start/end range in scheduled_time, pre-create a date calendar and join against the two tables, etc.

Related

Finding ocuppations via SQL and/or PHP

I am making a student web app. Amongst other tables, I have a table in which students enroll and enrollments are between two dates.
This app uses MySQL 5.6 and PHP 7.2
It has the following fields:
IDStudent
StartDate
EndDate
IDCourse
Each course has a maximum capacity in which it cannot be surpassed.
I want to know, given a start date, end date and IDCourse, how many concurrent students are in a course. I get an approxiumate value just counting rows between two dates
SELECT COUNT(*) FROM enrollments
WHERE IDCourse = ?
AND (
(StartDate BETWEEN "<start date>" AND "<end date>")
OR
(EndDate BETWEEN "<start date>" AND "<end date>")
OR
(StartDate <= "<start date>" AND EndDate>= "<end date>")
)
But that doesn't take account non overlapping ranges. It counts every enrollment.
For example, I have this very simple case:
Want to find how many students are enrolled between 01/01/2021 and 05/01/2021 at a specified course
And I have those 3 enrollments on that course:
01/01/2021 - 02/01/2021
03/01/2021 - 04/01/2021
20/12/2020 - 01/02/2021
I should get 2 count and not 3, because 1 and 2 don't overlap while 3 overlaps both.
I tried to search online but I didn't found something similar, maybe I am not using the correct keywords!
I found Determine max number of overlapping DATETIME ranges but that is for MySQL 8
Many thanks for your help
Regards
I think you may need to create a calendar table between the first start date and the last end date, count by date and then select the max between the period you are interested:
select max(stcount)
from
(
select c.dt, count(*) stcount from calendar_table c
join enrollments e on c.dt between e.StartDate and e.EndDate
group by c.dt
) countbydate
where dt between '2021-01-01' and '2021-01-05'
db-fiddle:
https://www.db-fiddle.com/f/dXuKMoRQ2ivLt5qi5AVFcG/0

PHP/MySQL Select the day before and day after a range from the same table

I have two tables, one called check_ins and another called holidays.
check_ins has a datetime_start and datetime_end columns (in addition to other stuff that isn't needed for this question). The holidays table has a date range of two columns for the start and end of the holiday.
I need to figure out who was in the day prior to and the day directly after the holiday range to determine who gets holiday pay. In other words, I need only results from the table that the same employee was in one day before and one day after, ignoring the rest.
I've been racking my brain all day trying to figure out a way to do this and have found nothing. Am I barking up the wrong tree here? Should I do this via PHP?
Thanks!
Edit: this is what I used and though I had it until I realized that their might be multiple check-ins in a single day:
SELECT DISTINCT count(check_ins.Employee_ID), check_ins.ShiftStart_Datetime, check_ins.ShiftEnd_Datetime, holidays.* FROM check_ins, holidays WHERE holidays.ID = 2 AND DATE(DATE_ADD(Datefrom, INTERVAL -1 DAY)) = DATE(ShiftStart_Datetime) GROUP BY Employee_ID HAVING count(check_ins.Employee_ID) >1 UNION SELECT DISTINCT check_ins.Employee_ID, check_ins.ShiftStart_Datetime, check_ins.ShiftEnd_Datetime, holidays.* FROM check_ins, holidays WHERE DATE(DATE_SUB(Dateto, INTERVAL -1 DAY)) = DATE(ShiftStart_Datetime) GROUP BY Employee_ID HAVING count(check_ins.Employee_ID) >1
You can (inner) join the check_ins table twice. Once for the day before the start of the holiday and once for the day after.
If the datetime_start and datetime_end may have different dates, you need to use BETWEEN. Cast both of them to a date instead of a datetime, since you don't care about the time.
Add a GROUP BY in case the employee has multiple check ins on one day.
SELECT holidays.id AS holiday_id, ci_before.employee_id FROM holidays
INNER JOIN check_ins ci_before ON holidays.holiday_start - INTERVAL 1 DAY
BETWEEN DATE(ci_before.datetime_start) AND DATE(ci_before.datetime_end)
INNER JOIN check_ins ci_after ON holidays.holiday_end + INTERVAL 1 DAY
BETWEEN DATE(ci_after.datetime_start) AND DATE(ci_after.datetime_end)
AND ci_before.employee_id = ci_after.employee_id
GROUP BY ci_before.employee_id
See the SQL fiddle here
The used tables are
CREATE TABLE `holidays` (id int, holiday_start date, holiday_end date);
CREATE TABLE `check_ins` (employee_id int, datetime_start datetime, datetime_end datetime);
holidays.id is an ID for a holiday, not an employee.
you can do it via php in 2 queries, but why would you... this'll be fun!
SELECT * FROM check_ins
LEFT JOIN holidays AS holi_before
ON (
datediff(holi_before.datetime_end,check_ins.datetime_end) <= 1
)
LEFT JOIN holidays AS holi_after
ON (
holi_before.id = holi_after.id
AND datediff(holi_after.datetime_end,check_ins.datetime_end) >= 1
)
That should about do it (might need to tweak the datediff comparison a bit to get it exactly right.)
haven't test it, but i hope it gives you the right push :-)

Getting temperature difference between intervals

my question is more "theoretical" than practical - in other words, Im not really looking for a particular code for how to do something, but more like an advice about how to do it. Ive been thinking about it for some time but cannot come up with some feasible solution.
So basically, I have a MySQL database that saves weather information from my weather station.
Column one contains date and time of measurement (Datetime format field), then there is a whole range of various columns like temp, humidity etc. The one I am interested in now is the one with the temperature. The data is sorted by date and time ascending, meaning the most recent value is always inserted to the end.
Now, what I want to do is using a PHP script, connect to the db and find temperature changes within a certain interval and then find the maximum. In other words, for example lets say I choose interval 3h. Then I would like to find the time, from all the values, where there was the most significant temperature change in those 3 h (or 5h, 1 day etc.).
The problem is that I dont really know how to do this. If I just get the values from the db, Im getting the values one by one, but I cant think of a way of getting a value that is lets say 3h from the current in the past. Then it would be easy, just subtracting them and get the date from the datetime field at that time, but how to get the values that are for example those 3 h apart (also, the problem is that it cannot just simply be a particular number of rows to the past as the intervals of data save are not regular and range between 5-10mins, so 3 h in the past could be various number of rows).
Any ideas how this could be done?
Thx alot
Not terribly hard actually. So I would assume it's a two column table with time and temp fields, where time is a DATETIME field
SELECT MAX(temp) FROM records
WHERE time >= "2013-10-14 12:00:00" and time <= "2013-10-14 15:00:00"
SELECT t1.*, ABS(t1.temperature - t2.temperature) as change
FROM tablename t1
JOIN tablename t2
ON t2.timecolumn <= (t1.timecolumn - INTERVAL 3 HOUR)
LEFT JOIN tablename t3
ON t3.timecolumn <= (t1.timecolumn - INTERVAL 3 HOUR)
AND t2.timecolumn > t3.timecolumn
WHERE
t3.some_non_nullable_column IS NULL
ORDER BY ABS(t1.temperature - t2.temperature) DESC
LIMIT 1;
1 table joined 2 times on itself, t2 is the quaranteed direct predecessor of t1 t2 is the closest record with offset 3h before or more. This could with the proper indexes, and a limited amount of data (where limited is in the eye of the beholder) be quite performant. However, if you need a lot of those queries in a big dataset, this is a prime candidate for denormalization, were you create a table which also stores the calculated offsets compared to the previous entry.

Is it good or bad practise to alter start dates in a database to the next occurrence of an event?

I am trying to create an event calendar which whilst initially quite small could turn out to be quite large. To that end, when trying to future proof it as much as possible, all events that occur in the past will be deleted from the database. However, is it bad practise to alter the start date of recurring events once they have happened to indicate when the next event will start? This makes it easier to perform search queries because theoretically no events will start more than say a week in the past, depending on how often the database is updated.
Is there a better way to do this?
My current intention is to have a table listing the event details along with a column for whether it is a yearly, monthly, weekly or daily recurrence. When somebody then searches for events between 2 dates, I simply look at each row and check if (EVENT START <= SEARCH FINISH && EVENT FINISH >= SEARCH START). This then gets all the possible events, and the recurring ones then need to be checked to see if they occur during the time period given. This is where I come a little unstuck, as to how to achieve this specifically. My thoughts are as follows:
Yearly: if EVENT START + 1 YEAR <= SEARCH FINISH || EVENT FINISH + 1 Year >= SEARCH START; repeat for +2 YEARS etc until EVENT START + NO YEARS > SEARCH FINISH.
Monthly: As above but + 1 month each time.
Weekly: As above but EVENT START and EVENT FINISH will be plus 7 DAYS BETWEEN RECURRENCE each iteration until EVENT START + 7 DAYS REPEATED > SEARCH FINISH.
Daily: As above but NO OF DAYS DIFFERENCE instead of 7 days for a week. This could be used to specify things like every 14 days (fortnight), every 10 days. Even every week could use this method.
However, when I think about the query that would have to be built to achieve this, I cannot help think that it will be very cumbersome and probably slow. Is there a better way to achieve the results I want? I have still not found a way to do things like occurs on the first Monday of a month or the last Friday of a month, or the second Saturday of April each year. Are these latter options even possible?
-- Edit: added below:
It might help a bit if I explain a bit more about what I am creating. That way guidance can be given with respect to that.
I am creating a website which allows organisations to add events, whether they are a one-off or recurring (daily, weekly, monthly, first Tuesday of a month etc.). The user of the site will then be able to search for events within a chosen distance (arbitrary 10, 25, 50, 100miles, all of country) on a set date or between 2 given dates which could be from 1 day apart up to a couple of years apart (obviously events that far into the future will be minimal or non-existant depending on the dates used).
The EVENTS table itself currently holds a lot of information about the event, such as location, cost, age group etc. Would it be better to have this in a separate table which is looked up once it has been determined if the event is within the specified search parameters? Clearly not all of this information is needed until the detailed page view, maybe just a name, location, cost and brief description.
I appreciate there are many ways to skin a cat but I am unsure how to skin this one. The biggest thing I am struggling with is how to structure my data so that a query will know if the recursion is within the specified date. Also, given that the mathematics to calculate distance between 2 lat/longs is relatively complex, I need to be able to build this calculation into my query, otherwise I will be doing the calculation in PHP anyway. Granted, there will be less results to process this way, but it still needs to be done.
Any further advice is greatly appreciated.
Creating events for each recurrence is unnecessary. It is much better to store the details that define how the event recurs. This question has been answered many times on SO.
One way to do this is to use a structure like this -
tblEvent
--------
id
name
description
date
tblEventRecurring
-----------------
event_id
date_part
end_date
Then you could use a query like this to retrieve events -
SELECT *
FROM `tblEvent`
LEFT JOIN `tblEventRecurring`
ON `tblEvent`.`id` = `tblEventRecurring`.`event_id`
WHERE (`tblEvent`.`date` = CURRENT_DATE AND `tblEventRecurring`.`event_id` IS NULL)
OR (
CURRENT_DATE BETWEEN `tblEvent`.`date` AND `tblEventRecurring`.`end_date`
AND (
(`tblEventRecurring`.`date_part` = 'D') OR
(`tblEventRecurring`.`date_part` = 'W' AND DAYOFWEEK(`tblEvent`.`date`) = DAYOFWEEK(CURRENT_DATE)) OR
(`tblEventRecurring`.`date_part` = 'M' AND DAYOFMONTH(`tblEvent`.`date`) = DAYOFMONTH(CURRENT_DATE))
)
)
UPDATE Added the following example of returning events for a given date range.
When returning dates for a given date range you can join the above query to a table representing the date range -
SET #start_date = '2012-03-26';
SET #end_date = '2012-04-01';
SELECT *
FROM (
SELECT #start_date + INTERVAL num DAY AS `date`
FROM dummy
WHERE num < (DATEDIFF(#end_date, #start_date) + 1)
) AS `date_list`
INNER JOIN (
SELECT `tblEvent`.`id`, `tblEvent`.`date`, `tblEvent`.`name`, `tblEventRecurring`.`date_part`, `tblEventRecurring`.`end_date`
FROM `tblEvent`
LEFT JOIN `tblEventRecurring`
ON `tblEvent`.`id` = `tblEventRecurring`.`event_id`
WHERE `tblEvent`.`date` BETWEEN #start_date AND #end_date
OR (`tblEvent`.`date` < #end_date AND `tblEventRecurring`.`end_date` > #start_date)
) AS `events`
ON `events`.`date` = `date_list`.`date`
OR (
`date_list`.`date` BETWEEN `events`.`date` AND `events`.`end_date`
AND (
(`events`.`date_part` = 'D') OR
(`events`.`date_part` = 'W' AND DAYOFWEEK(`events`.`date`) = DAYOFWEEK(`date_list`.`date`)) OR
(`events`.`date_part` = 'M' AND DAYOFMONTH(`events`.`date`) = DAYOFMONTH(`date_list`.`date`))
)
)
WHERE `date_list`.`date` BETWEEN #start_date AND #end_date
ORDER BY `date_list`.`date`;
You can replace the SQL variables with PHP vars if you would prefer. To display days without any events you can change the INNER JOIN between the two derived tables, date_list and events, to a LEFT JOIN.
The table dummy consists of a single column with numbers from 0 to whatever you anticipate needing. This example creates the dummy table with enough data to cover one month. You could easily populate it using an INSERT... SELECT... on the AI PK of another table -
CREATE TABLE `dummy` (
`num` SMALLINT UNSIGNED NOT NULL PRIMARY KEY
);
INSERT INTO `dummy` VALUES
(00), (01), (02), (03), (04), (05), (06), (07), (08), (09),
(10), (11), (12), (13), (14), (15), (16), (17), (18), (19),
(20), (21), (22), (23), (24), (25), (26), (27), (28), (29),
(30), (31);
Break it up
Have one table for vents that haven't happened yet with a reccurring event ID. So you can just poke one offs in there with recurring veent id of null. Get rid /archive past ones etc.
Have another for the data about recurring events.
When an event marked as recurring happens, go back to recurring table, check to see if it's enabled (you might want to add a range to them ie do this every wek for three months), and if all is okay, add a new record for the next time it occurs.
One way to do it anyway, and it gets rid of the problem of using event start for two different things which is why your code is getting complicated.
If you want future jobs from this. ie everything needed to do in the next month.
The it would be a union query. One to get all teh "current jobs", unioned with one to get all the jobs that will recur in the next month.
Can't stress this enough, get the data design right the code "just happens". If you data is messed up as in one field "start date" serving two different needs, then every time you go near it, you have to deal with that dual use. Forget it once and you get anything from a painful mess to a disaster.
Adding a Recurring_Start_Date column would be better than your current plan, wouldn't it. You wouldn't be asking this question, beacseu your data would fit your needs.
I assume you'll be searching through events much more frequently than you will be creating new ones. During event creation, I would create records for each occurrence of the event up to so reasonable amount of time (maybe for the next year or two).
It would also make things like "The third thursday of each month" a little easier. If you tried to do any of the calculations in a query it would be difficult and probably slow.

PHP: report table with date gaps

I have a table in DB which contains summaries for days. Some days may not have the values.
I need to display table with results where each column is a day from user selected range.
I've tried to play with timestamp (end_date - start_date / 86400 - how many days in report, then use DATEDIFF(row_date, 'user_entered_start_date') and create array from this indexes), but now I've got whole bunch of workarounds for summer time :( Any examples or ideas how to make this correct?
P.S. I need to do this on PHP side, because DB is highly loaded.
Try the DateTime object:
$reportdate=date_create($user_startdate);
$interval=new DateInterval('P1D');//1 day interval
$query="SELECT s.start_date, s.end_date, s.info
FROM summary s
WHERE s.start_date>='$user_startdate'
AND s.end_date<='$user_enddate'";
$r=mysqli_query($db,$query);
while($row=$r->fetch_assoc()){
$rowdate=create_date($row['start_date']);
while($reportdate < $rowdate) {//won't work if $rowdate is a timestamp!
//output $reportdate and blank row
$reportdate->add($interval); //increment date
}
//output $rowdate and $row[info]
$reportdate->add($interval); //increment date
}
ETA another option:
Based on your comments, it may be easier to dynamically generate the missing dates. For this you'll need an integer table, the number of dates that should appear in your report output, a start date and a date increment.
In your db create a table called numbers and insert the numbers 0 through 9:
CREATE TABLE numbers (
num int(10) unsigned NOT NULL,
PRIMARY KEY (num)
);
The numbers table can be used for making sequences of integers. For instance, to get a sequence from 1 to 20:
SELECT i FROM (
SELECT 10*n1.num + n2.num AS i
FROM numbers n1 CROSS JOIN numbers n2) nums
WHERE i BETWEEN 1 AND 20
ORDER BY i ASC;
If you left join a sequence query like the above to your regular query, you should be able to generate both real and blank rows. e.g.
SELECT alldates.d, mystuff.* FROM
(SELECT date_add($start_date, interval i day) AS d FROM
(SELECT 10*n1.num + n2.num AS i
FROM numbers n1 CROSS JOIN numbers n2
ORDER BY i ASC) nums
WHERE i <= DATEDIFF($end_date,$start_date)) alldates
LEFT JOIN mystuff
ON alldates.d = mystuff.somedate
ORDER BY $whatever;
You could "pre-load" the database with blank values. Then do UPDATE queries instead of inserts. All your days with no data will be pre-populated. You can create a simple script that creates a month's/year's/decade's worth of (blank) data and run it as often as you need. Then you never have to worry about how the days with no data get into the database - you start with your data "zeroed" out.

Categories