I had this problem some years ago and back then I implemented a "different logic" in order to deliver the project but the doubt remains in my mind and hopefully with your help I'll be able to understand it now.
Suppose I have some scheduled events on my database that may or may not spawn over several days:
id event start end
-----------------------------------------------
1 fishing trip 2009-12-15 2009-12-15
2 fishCON 2009-12-18 2009-12-20
3 fishXMAS 2009-12-24 2009-12-25
Now I wish to display the events in a calendar, lets take the month of December:
for ($day = 1; $day <= 31; $day++)
{
if (dayHasEvents('2009-12-' . $day) === true)
{
// display the day number w/ a link
}
else
{
// display the day number
}
}
What query should the dayHasEvents() function do to check if there are (or not) events for the day? I'm guessing SELECT .. WHERE .. BETWEEN makes the most sense here but I've no idea how to implement it. Am I in the right direction?
Thanks in advance!
#James:
Lets say we're on December 19th:
SELECT *
FROM events
WHERE start >= '2009-12-19 00:00:00'
AND end <= '2009-12-19 23:59:59'
Should return the event #2, but returns nothing. =\
You should scratch that approach and grab all events for the given month up front so you only need to perform a single query as opposed to N queries where N is the number of days in the month.
You could then store the returned results in a multidimensional array like so:
// assume event results are in an array of objects in $result
$events = array();
foreach ($result as $r) {
// add event month and day as they key index
$key = (int) date('j', strtotime($r->start));
// store entire returned result in array referenced by key
$events[$key][] = $r;
}
Now you'll have a multidimensional array of events for the given month with the key being the day. You can easily check if any events exist on a given day by doing:
$day = 21;
if (!empty($events[$day])) {
// events found, iterate over all events
foreach ($events[$day] as $event) {
// output event result as an example
var_dump($event);
}
}
You're definitely on the right track. Here is how I would go about doing it:
SELECT *
FROM events
WHERE start <= '2009-12-01 00:00:00'
AND end >= '2009-12-01 23:59:59'
And you obviously just replace those date values with the day you're checking on.
James has the right idea on the SQL statement. You definitely don't want to run multiple MySQL SELECTs from within a for loop. If daysHasEvents runs a SELECT that's 31 separate SQL queries. Ouch! What a performance killer.
Instead, load the days of the month that have events into an array (using one SQL query) and then iterate through the days. Something like this:
$sql= "SELECT start, end FROM events WHERE start >= '2009-12-01' AND end <= '2009-12-31'";
$r= mysql_query($sql);
$dates= array();
while ($row = mysql_fetch_assoc($r)) {
// process the entry into a lookup
$start= date('Y-m-d', strtotime($row['start']));
if (!isset($dates[$start])) $dates[$start]= array();
$dates[$start][]= $row;
$end= date('Y-m-d', strtotime($row['end']));
if ($end != $start) {
if (!isset($dates[$end])) $dates[$end]= array();
$dates[$end][]= $row;
}
}
// Then step through the days of the month and check for entries for each day:
for ($day = 1; $day <= 31; $day++)
{
$d= sprintf('2009-12-%02d', $day);
if (isset($dates[$d])) {
// display the day number w/ a link
} else {
// display the day number
}
}
For your purposes a better SQL statement would be one that grabs the start date and the number of events on each day. This statement will only work properly if the start column is date column with no time component:
$sql= "SELECT start, end, COUNT(*) events_count FROM events WHERE start >= '2009-12-01' AND end <= '2009-12-31' GROUP BY start, end";
Related
the following situiation:
We have 3 things available at the same time.
The first is booked from 09:00 to 11:00.
The second is booked from 11:00 to 13:00
I want to calculate how much things are available from 10:00 to 12:00.
I've done it calculating how much things are booked in the range from 10:00 to 12:00.
It was 2. So the available things are 1.
But the first and the second could be the same. So the available things in the designated time are 2 and my calculation was wrong.
So I created the following algorithm to calculate the occupied things in a time range. the results came from a database query and contains the booking of things which are in my time range (result is an array and contains start,end and cnt which is the amount of booked things):
$blocks = array();
$times = array();
foreach($results as $result){
$block = array();
$block['start'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['start'])->getTimestamp();
$block['end'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['end'])->getTimestamp();
$block['things'] = $result['cnt'];
$blocks[] = $block;
$times[] = $block['start'];
$times[] = $block['end'];
}
$times = array_unique($times);
$times = array_values($times);
$peak = 0;
foreach($times as $time){
$timePeak = 0;
foreach($blocks as $block){
if($time >= $block['start'] && $time <= $block['end']){
$timePeak += $block['things'];
}
}
if($timePeak > $peak){
$peak = $timePeak;
}
}
return $peak;
}
whith this method, I am creating timestamps for every start and endtime of every booking in this range.
Then I have calculated the sum of all bookings of every timestamp. The maximum of the calculations (peak) was the maximum amount of bookings. So the available things are max - peak.
It works.
But is there a more elegant way to calculate it?
Sounds similar to this common coding interview problem: https://www.geeksforgeeks.org/find-the-point-where-maximum-intervals-overlap/
If your "max things" is hardcoded then yeah I think you find the maximum booking of things during the given time and subtract from the max to get minimum availability over the range. To do this, you need to consider all bookings that either start or end in your range. As the efficient solution on that link suggests, you sort the starts and ends and then go through them to see your running utilization.
To handle the boundary case you talked about where a booking ending lines up with a booking start, make sure your sorting of "starts" and "ends" always sorts an "end" before a "start" when the time is the same so that you don't get the appearance of an overlap where one doesn't really exist.
I'm assuming you are working on a table like this:
CREATE TABLE `bookings` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`start` datetime NOT NULL,
`end` datetime NOT NULL,
PRIMARY KEY (`id`)
);
And, you have data like this:
INSERT INTO `bookings` (`id`, `start`, `end`)
VALUES
(1, '2019-05-05 09:00:00', '2019-05-05 11:00:00'),
(2, '2019-05-05 11:00:00', '2019-05-05 13:00:00'),
(3, '2019-05-05 07:00:00', '2019-05-05 10:00:00'),
(4, '2019-05-05 12:00:00', '2019-05-05 14:00:00'),
(5, '2019-05-05 14:00:00', '2019-05-05 17:00:00'),
(6, '2019-05-05 10:30:00', '2019-05-05 11:30:00');
This data has all the relevant cases you need to consider:
Bookings that end before your range
Bookings that start after your range
Bookings that intersect the start of your range
Bookings that intersect the end of your range
Bookings contained entirely within your range
Bookings that "hand-off" within your range
I looked at making a SQL query for this but stopped when it got more complicated than you would actually want to code in SQL. I think it's probably do-able but requires really ugly SQL. Definitely do this in code. I think the algorithm is to load all bookings relevant to your range with a query like:
SELECT * FROM bookings
WHERE start BETWEEN :start AND :end
OR end BETWEEN :start AND :end
That's all the bookings that will possibly matter. Then you sort the start and end events as distinct events as described earlier and loop over the list of events keeping a running counter going as well as a max value seen so far. The running count will first go up and at the end it will go back down to zero, but may have multiple peaks in the middle. So, you can't just stop the first time it goes down, you have to keep scanning until you get through all the events (or at least to your end time, but simpler and no real cost to just go all the way through the list). Once done, you've got the maximum number of concurrent bookings.
I haven't tested or compiled this, so take it as psuedo-code. I'd change your code to something like this:
//assumedly, prior to this point you have a
//$startTime and $endTime that you used to do the query mentioned above
//and the results of the query are in $results
$times = array();
foreach($results as $result){
$time = array();
$time['time'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['start'])->getTimestamp();
$time['change'] = $result['cnt'];
$times[] = $time;
$time['time'] = DateTime::createFromFormat("Y-m-d H:i:s", $result['end'])->getTimestamp();
$time['change'] = -1 * $result['cnt'];
$times[] = $time;
}
usort($times, function($lh, $rh) {
if ($lh['time'] === $rh['time']) {
return $lh['change'] - $rh['change']
} else {
return $lh['time'] < $rh['time'] ? -1 : 1;
}
}
$maxPeak = 0;
$curVal = 0;
foreach($times as $time){
//breaking early here isn't so much about optimization as it is about
//dealing with the instantaneous overlap problem where something starts
//right at the end time. You don't want that to look like a utilization
//that counts against you.
if ($time['time'] === $endTime) {
break;
}
$curVal += $time['change'];
if ($curVal > $maxPeak) {
$maxPeak = $curVal;
}
}
//$maxPeak is the max number of things in use during the period
I have a cron job that gets results from the DB to check it the interval set by user falls on today's date. I am currently thinking of doing it as below :
Get the time column for the row. Ex:2017-05-25 00:00:00
Get the frequency set. Ex:Every 2 weeks.
Get the current date in above format. Ex:2017-05-31 00:00:00
Get the difference in days. Ex:6 days.
Convert the frequency set to days. Ex:2 weeks = 14 days;
Divide (difference in time(days)) by (frequency in days). Ex:6/14
This way I will only get the result to be true when 2 weeks have passed since the time set. I.e., 14/14, 28/14, 42/14,...
If the frequency is in months, I can start dividing by 30. But somehow this feels like a hacky way of doing it. So my question is if there is better way of doing this calculation to check the difference.
This is what I have done as explained by above example.
` $frequency = ; // Get the relevant fields from db
$today = date(Y-m-d H:i:s);
foreach ($frequency as $key => $value) {
$frequency_in_days;
$frequency_type = $value->type;
$frequency_repeat = $value->repeat;
if($frequency_type == 1){
$frequency_in_days = $frequency_repeat;
} elseif($frequency_type == 2) {
$frequency_in_days = $frequency_repeat * 7;
} elseif($frequency_type == 3) {
$frequency_in_days = $frequency_repeat * 30;
} elseif($frequency_type == 4) {
$frequency_in_days = $frequency_repeat * 365;
}
// Get number of days spent between start_date and today in days.
$interval = date_diff($value->start_date, $today)->format('%a');
$result = $interval % $frequency_in_days;
if ($result == 0) {
// Frequency falls today! Do the job.
}
}`
Note: The cron job runs this script. The script again needs to check if the today falls under the frequency set.
Also for argument's sake, is this the best logic to calculate the difference?
Thank you.
This will work
Table "schedule"
`last_run` timestamp,
`frequency_seconds` int
example query for tasks that should go every two weeks:
SELECT *
FROM schedule
WHERE TIMESTAMPDIFF(last_run, NOW()) >= frequency_seconds
after fetching rows update last_run to NOW()
I am generating reference no REF-082013-001 (REF-mmyyyy-001) and incrementing the last part for every entry. REF-082013-001 for first record and REF-082013-002 for second and so on.
My Question is How can i reset the last number by php for every new month. Let say September 2013 I want it to be REF-09-2013-001 and auto increment it till the end of September and then in November reset it again.
Is there a ways to do this in php. Your help is much appreciated. Thank you
Update
Currently 'REF-'.date('m-Y').$maxnumberfromdb in single column called reference_no and Now thinking to store the mm-yyyy ref1 and last number in ref2 separately and start ref2 every first day of the month.
You can probably go with a single table, with an AUTO_INCREMENT field to handle the last part of the reference number, and a date/time field to track when it was last reset.
CREATE TABLE track_reference
ref_number INT(11) AUTO_INCREMENT,
last_reset DATETIME;
Then you write a function in PHP to get a new reference number, which (pseudo-code):
if (MONTH(time()) > MONTH(last_reset)) {
reset ref_number to 0 (use ALTER TABLE);
}
select (ref_number) into $variable;
return 'REF_<month><year>_$variable;
}
It's rough, but you get the idea. I'd also have YEAR appear before MONTH for making sorting by reference number easier later.
This is my code for reset counter and update last_reset, this is effective for me
<pre>
$date2 = new DateTime("now", new DateTimeZone('America/New_York') );
$month_end = strtotime('last day of this month', time());
$count = $data['sim_tarif_count'];
$date3=$date2->format('Y-m-d');
foreach ( $count as $r2 ){
$counts[] = $r2['count'];
$last_reset[] = $r2['last_reset'];}
$paddedNum = sprintf("%04d", $counts[0]);
$reg_id = 'SEQ'.$date2->format('Ymd').$paddedNum;
echo " ";
echo date('j', $month_end);
$counter = $counts[0]+1;
//for reset
if($date2->format('Y-m') > $last_reset[0])
{
$counter = 0;
$counting_update=$this->docs_model->update_time_counter($counter,$date3);
echo $counter = 0;
} else
{
$counting_update=$this->docs_model->update_time_count($counter);
}
</pre>
I wish to print the totals of a column in my database for each month in the last year. The code I have so far to do this is:
$month = date("n");
$year = date("Y");
$loop = 12;
while($loop>1) {
$first = mktime(0,0,0,$month,1,$year);
$last = mktime(23,59,00,$month+1,0,$year);
$spendingData = mysql_query("SELECT * FROM spending WHERE date BETWEEN $first AND $last") or die(mysql_error());
$totalMonth = 0;
while($spending = mysql_fetch_array($spendingData))
{
$totalMonth = $totalMonth + $spending['amount'];
}
print "£".$totalMonth;
$loop = $loop-1;
print "<br>";
}
My quesiton is how, in the loop, do I adjust the times for each month? I thought about just taking a months worth of seconds away from the timestamps, but as I don't know how many days are in each month I don't think this will work. I also don't think I can just keep taking 1 away from the month figure as this will not account for years. I also don't want to hard code the figures in, as they will change with each new month.
How can I achieve this?
Thank you
You could do this rather trivially in MySQL:
SELECT MONTH(date) AS month, SUM(amount) AS amount
FROM yourtable
WHERE YEAR(date) = $year
GROUP BY MONTH(date)
without ever having to involve PHP for the date manipulation stuff.
PHP code (relevant)
$db = '2011-02-28'; $a = '2011-02-01';
and this is the part of the query
LEFT JOIN
abc
ON
abc.date between '$a' and '$db'
This query show following results :
1 2011-02-08
6 2011-02-09
6 2011-02-11
1 2011-02-13
but what i want is to get 0 as a result if there's n rows for other dates.
You can't do that (not without joining a table containing all dates, at least).
Displaying the 0s for dates with no data is the job of your application, not the database. It's trivial, just write a loop from the start date to the end date, and output the 0 for those dates with no rows in your result set.
SQL's job is to tell you what data is there, not what isn't.
while ($row = mysql_fetch_array($result)) {
$results[$row['date']] = $row['count'];
}
for ($time = strtotime('2011-02-01'); $time <= strtotime('2011-02-28'); $time += 86400) {
$datestr = date('Y-m-d', $time);
if (isset($results[$datestr])) {
echo "Count for date $datestr: " . $results[$datestr];
} else {
echo "Count for date $datestr: 0";
}
}
From the limited information you give about what problem you are trying to solve I would guess you are trying to find "free appointments" or similar.
If so then this should help you out. http://www.artfulsoftware.com/infotree/queries.php?&bw=1280#98
This website has a number of query patterns that will help your thinking and design of your database.