I need to work out how many different instances occur on a different day, from many different ranges. Probably best to explain it with an example.
18-JAN-09 to 21-JAN-09
19-JAN09 to 20-JAN-09
20-JAN-09 to 20-JAN-09
Using the three examples above, I need it to collect this information and display something a little like...
18th Jan: 1
19th Jan: 2
20th Jan: 3
21st Jan: 1
... I'll be grabbing the information from an Oracle database fwiw (hence the format above ^) and there will be hundreds, maybe thousands of records, so my lame attempt to do all sorts of loops and if statements would take forever to run.
Is there any fairly simple and efficient way of doing this? I'm really not too sure where to start unfortunately...
Thanks
You could use the method described in another SO:
SQL> WITH DATA AS (
2 SELECT to_date('18-JAN-09', 'dd-mon-rr') begin_date,
3 to_date('21-JAN-09', 'dd-mon-rr') end_date FROM dual UNION ALL
4 SELECT to_date('19-JAN-09', 'dd-mon-rr'),
5 to_date('20-JAN-09', 'dd-mon-rr') FROM dual UNION ALL
6 SELECT to_date('20-JAN-09', 'dd-mon-rr'),
7 to_date('20-JAN-09', 'dd-mon-rr') FROM dual
8 ),calendar AS (
9 SELECT to_date(:begin_date, 'dd-mon-rr') + ROWNUM - 1 c_date
10 FROM dual
11 CONNECT BY LEVEL <= to_date(:end_date, 'dd-mon-rr')
12 - to_date(:begin_date, 'dd-mon-rr') + 1
13 )
14 SELECT c.c_date, COUNT(d.begin_date)
15 FROM calendar c
16 LEFT JOIN DATA d ON c.c_date BETWEEN d.begin_date AND d.end_date
17 GROUP BY c.c_date
18 ORDER BY c.c_date;
C_DATE COUNT(D.BEGIN_DATE)
----------- -------------------
18/01/2009 1
19/01/2009 2
20/01/2009 3
21/01/2009 1
Either your DB engine or your PHP code is going to have to loop over the date range.
Here's some PHP code to do the summation. The day counts are stored by year-month to avoid having a huge array for a wide date range.
<?php
// Get the date ranges from the database, hardcoded for example
$dateRanges[0][0] = mktime(0, 0, 0, 1, 18, 2009);
$dateRanges[0][1] = mktime(0, 0, 0, 1, 21, 2009);
$dateRanges[1][0] = mktime(0, 0, 0, 1, 19, 2009);
$dateRanges[1][1] = mktime(0, 0, 0, 1, 20, 2009);
$dateRanges[2][0] = mktime(0, 0, 0, 1, 20, 2009);
$dateRanges[2][1] = mktime(0, 0, 0, 1, 20, 2009);
for ($rangeIndex = 0; $rangeIndex < sizeof($dateRanges); $rangeIndex++)
{
$startDate = $dateRanges[$rangeIndex][0];
$endDate = $dateRanges[$rangeIndex][1];
// Add 60 x 60 x 24 = 86400 seconds for each day
for ($thisDate = $startDate; $thisDate <= $endDate; $thisDate += 86400)
{
$yearMonth = date("Y-m", $thisDate);
$day = date("d", $thisDate);
// Store the count by year-month, then by day
$months[$yearMonth][$day]++;
}
}
foreach ($months as $yearMonth => $dayCounts)
{
foreach ($dayCounts as $dayNumber => $dayCount)
{
echo $yearMonth . "-" . $dayNumber . ": " . $dayCount . "<br>";
}
}
?>
You need a table with one row for each day
test_calendar:
Day
16.01.2009
17.01.2009
18.01.2009
19.01.2009
20.01.2009
21.01.2009
22.01.2009
23.01.2009
24.01.2009
25.01.2009
26.01.2009
table test contains bagin and finish of inctance:
DFROM DTILL
18.01.2009 21.01.2009
19.01.2009 20.01.2009
20.01.2009 20.01.2009
Here is a query you need:
select day, (select count(1)
from test where dfrom<=t.day and dtill>=t.day) from test_calendar t
where day>to_date('15.01.2009','DD.MM.YYYY')
order by day
Huge thanks for the solutions guys - managed to get it working using some of the SQL from above and also bits of PHP from the second solution.
Really appreciate it, cheers :)
Related
I'm trying to write an algorithm which calculate the week (saturday to saturday) of interest based on a range of date.
For example I have this range:
2018-01-04 to 2018-01-13
In this case I have two weeks of interest, it is: "week 1" From 01 to 07 of January and "week 2" From 08 to 14 of the same January.
In this case the algorithm will respond to me that the week of interest is the "Week 2" because the number of days in that week is higher than the number of days in the "week 1".
How can I do this in Carbon?
Assuming you have the start and end date as Carbon objects $s and $e
make sure the difference is less than 14 days
calculate the overlaps:
$s->diffInDays($s->copy()->startOfWeek()) "offset" into first week
$e->copy()->endOfWeek()->diffInDays($e) "remainder" of last week
if $offset > $remainder select $e; else select $s
output $selectedWeek->copy()->startOfWeek() and $selectedWeek->copy()->endOfWeek()
Apparently, startOfWeek() and endOfWeek() alter the object; so make sure to copy() before using these methods!
Implementation:
$s = new Carbon\Carbon('2018-01-04');
$e = new Carbon\Carbon('2018-01-13');
$diff = $e->diffInDays($s);
if ($diff > 14) die("uh-oh!");
$offset = $s->diffInDays($s->copy()->startOfWeek());
$remainder = $e->copy()->endOfWeek()->diffInDays($e);
$selectedWeek = ($offset > $remainder ? $e : $s)->copy()->startOfWeek();
echo "selected week {$selectedWeek->weekOfYear} ({$selectedWeek} - {$selectedWeek->copy()->endOfWeek()})";
Output:
selected week 2 (2018-01-08 00:00:00 - 2018-01-14 23:59:59)
I guess you could do something like this. I haven't tested it but you can get some idea. Also I would like to point to the documentation https://carbon.nesbot.com/docs/
// Create dates
$date1 = Carbon::create(2018, 1, 4, 0, 0, 0);
$date2 = Carbon::create(2018, 1, 13, 0, 0, 0);
// Get The week numbers
$week_date_1 = $date1->weekOfYear;
$week_date_2 = $date2->weekOfYear;
// Count number of days for each week
$number_of_days_week_1 = $date1->diffInDays($date1->startOfWeek);
$number_of_days_week_2 = $date2->diffInDays($date2->startOfWeek);
// Highest number of days per week wins
if($number_of_days_week_1 > $number_of_days_week_2){
return $week_date_1;
} else {
return $week_date_2;
}
I have some code that loops through each day within a 10 year range starting on a specific date. The date should only be added if it meets the selected criteria from a form.
The form has fields for months and weekdays to be selected. Within each weekday there are options for every, first, second, etc. I'm adding an 'every other' option to each weekday along with a start date field.
I am trying to determine the best way to check if the current date is within the 'every other' criteria based on the start date.
After looking at other questions, I couldn't find an ideal answer that took into account year that have 53 weeks. I was able to come up with some test code that seems to be giving me accurate results, but I'm wondering if there is a simpler way to perform this check.
Test Code:
// set start date
$date = new \DateTime('2019-12-15');
// get start date week of year and modulus
$start_week = date('W', $date->getTimestamp());
$start_mod = $start_week % 2;
// set end time (10 years from start date)
$end_time = strtotime('+10 years', $date->getTimestamp());
// init previous year and week modifier
$prev_year = false;
$week_modifier = 1;
// each day in range
while($date->getTimestamp() <= $end_time){
// get year
$y = $date->format('Y');
// previous year doesn't match current year
if($prev_year != $y){
// previous year set
if($prev_year){
// get number of weeks in year
$weeks_in_year = date('W', mktime(0, 0, 0, 12, 28, $prev_year));
// year has odd number of weeks
if($weeks_in_year % 2){
// increment week modifier
$week_modifier++;
}
}
// update previous year
$prev_year = $y;
}
// get week of year
$w = $date->format('W') + $week_modifier;
// check if meets every other criteria (based on start date)
$every_other = false;
if( ($w % 2) == $start_mod ){
$every_other = true;
}
// print date if it is part of every other Tuesday
if($date->format('w') == 2 && $every_other){
echo $date->format('Y-m-d');
echo '<br/>';
}
// increment day
$date->modify('+1 day');
}
Note 1: 2020 is the next year in which there are 53 weeks.
Note 2: I had a typo in this test code that was incrementing the week modifier instead of initializing it to 0. It would make more sense to me that this code would work if the modifier was initialized to be 0, but instead it only works when initialized to an odd number.
Since the "every other" is evaluated in a continuous cycle, you might just keep track of the days:
$odd = [ true, true, true, true, true, true, true ];
...
// Flip the appropriate day of the week.
$odd[date('w')] = !$odd[date('w')];
// Or start with all 1's, then $odd[date('w')] ^= 1;
if ($odd[date('w')]) {
// This is the "not-other" day
}
Modular arithmetic
This day is $w and we mark it:
$odd[$w] = !$odd[$w];
Now we advance by an unknown number of days $d. We need to properly flip all the days in this interval.
One way to do that is to cycle through all the days. But it's clear that this can't be necessary - we have seven days in a week, and they are either odd or even; even if we flipped them all, it would be seven updates. Cycling through one year would be 365 or 366 updates.
On the one hand, the 365-cycle solution has the advantage of simplicity. Is running 7 flips instead of 3652 really worth our while? If it is not, we're done. So let's suppose it is; but this evaluation should be re-done for every project.
So note that if we advanced by 1 day, we need do nothing. If we advance by 2 days, day[w+1] must be flipped. If we advance by 5 days, days from w+1 to w+4 need to be flipped. In general, days from w+1 to w+d-1 need to be flipped:
for ($i = 1; $i < $w+$d; $i++) {
$odd[$i % 7] = !$odd[$i % 7];
}
But now notice that if we advanced by 15 days, we would again need do nothing, as if we had advanced of only 1 day, since every day of the week would find itself being flipped twice:
d need to flip w+...
1 (none)
2 1
3 1, 2
4 1, 2, 3
5 1, 2, 3, 4
6 1, 2, 3, 4, 5
7 1, 2, 3, 4, 5, 6
8 1, 2, 3, 4, 5, 6, 0 (or 7)
9 2, 3, 4, 5, 6, 0
10 3, 4, 5, 6, 0
11 4, 5, 6, 0
12 5, 6, 0
13 6, 0
14 0
15 (none)
So here's a very reasonable compromise: if we need to advance by X days, treat it as if we had to advance by (X % 14) days. So now we will run at most 13 updates. Stopping now means that our code is the trivial version, enhanced by a strategically placed "% 14". We went from 3652 to 14 updates for a ten-year interval, and the best we could hope for was 7 updates. We got almost all of the bang, for very little buck.
If we want to settle for nothing but the best, we go on (but note that the additional arithmetic might end up being more expensive than the saving in updates from the worst value of 13 to, at best, zero. In other words, doing additional checks means we will save at best 13 updates; if those checks cost more than 13 updates, we're better off not checking and going through blindly).
So we start flipping at dIndex 1 if (1 < d%14 < 9), or (d%7) if (d%14 >=9 ). And we end at (d%14)-1 if (d%14)<8, 0 otherwise. If d%14 is 1, the start (using a simplified rule 1: d%14 < 9) is 1, the end is at 0, and since 0 is less than 1, the cycle would not even start. This means that the simplified rule should work:
// increase by d days
d1 = (d%14) < 9 ? 1 : (d%7);
d2 = (d%14) < 8 ? (d%14-1) : 7;
for (dd = d1; dd <= d2; dd++) {
odd[(w+dd)%7)] = !odd[(w+dd)%7)];
}
The above should flip correctly the "every other XXX" bit doing at most 7 writes, whatever the value of d. The approximate cost is about 6-7 updates, so if we're doing this in memory, it's not really that worth it, on average, if compared with the "% 14" shortcut. If we're flipping with separated SQL queries to a persistency layer, on the other hand...
(And you really want to check out I didn't make mistakes...)
I am trying to organize an output of DATA in the following way:
Today, Yesterday and everything before Yesterday is present with a respective full DateTime minus the clock time of course. For a better understanding, have a look at the screenshot below:
I have written this code:
try{
$db = new PDO("mysql:host=" .$hostname. ";dbname=" .$database. "", "" .$user. "", "" .$pass. "");
$db->setAttribute(PDO::ATTR_PERSISTENT, true);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$notifications = $db->prepare("SELECT * FROM reports
ORDER BY timestamp DESC");
$notifications->execute();
$result = (object)$notifications->fetchAll(PDO::FETCH_OBJ);
echo "<pre>";
print_r($result);
echo "</pre>";
}
catch(PDOException $e)
{
echo $e->getMessage();
}
I am trying to figure out a way to split things up, for instance, "Today", "Yesterday", "Day before Yesterday" and so forth for as long as considered normal e.g. an entire month maybe.
*How would I structure this up correctly with a PDO prepared statement? *
[Today] => stdClass Object
(
[id] => 1
[timestamp] => 2015-04-09 13:20:05
[seen] => 0
)
[Yesterday] => stdClass Object
(
[id] => 2
[timestamp] => 2015-04-08 15:30:50
[seen] => 0
)
Clearly: I want to print everything with timestamp of TODAY. Next, everything with YESTERDAYS timestamp. And so forth.
SQL:
// Today
AND DATE(from_unixtime(comment_date)) = CURRENT_DATE
// Yesterday
AND DATE(from_unixtime(comment_date)) = DATE_SUB(CURRENT_DATE,INTERVAL 1 DAY)
// This week
AND YEARWEEK(from_unixtime(comment_date), 1) = YEARWEEK(CURRENT_DATE, 1)
// This month
AND YEAR(from_unixtime(comment_date)) = YEAR(CURRENT_DATE)
AND MONTH(from_unixtime(comment_date)) = MONTH(CURRENT_DATE)
Here's an example of how I would handle this. I wouldn't change your query - it's fine as is. Assuming you want to show everything in the database sorted from latest to earliest post. Let PHP handle the heavy lifting. I've purposely broken some things in to multiple lines instead of nesting the functions to make it easier to read. Condense as you see fit.
I'm not claiming this is the BEST way to do it. Only what I use.
//MOCK UP SOME DISPLAY DATA - YOU WOULD USE YOUR QUERY RESULT INSTEAD
$rows = array();
$rows[] = date('Y-m-d H:i:s');
$rows[] = date('Y-m-d H:i:s');
$rows[] = date('Y-m-d H:i:s');
$rows[] = date('Y-m-d H:i:s', mktime(0, 0, 0, date('n'), date('j') - 1, date('Y')));
$rows[] = date('Y-m-d H:i:s', mktime(0, 0, 0, 12, 24, 2014));
$rows[] = date('Y-m-d H:i:s', mktime(0, 0, 0, 12, 25, 2014));
$rows[] = date('Y-m-d H:i:s', mktime(0, 0, 0, 12, 26, 2014));
$rows[] = date('Y-m-d H:i:s', mktime(0, 0, 0, 3, 2, 2001));
//CREATE AN ARRAY OF THE REPLACEMENTS YOU WANT
$aArray = array();
$aArray[date('Y-m-d')] = 'Today';
$aArray[date('Y-m-d', mktime(0, 0, 0, date('n'), date('j') - 1, date('Y')))] = 'Yesterday';
$aArray[date('Y-m-d', mktime(0, 0, 0, date('n'), date('j') - 2, date('Y')))] = 'Day before Yesterday';
$aArray['2014-12-25'] = 'Christmas 2014';
//INITIALIZE SOME VARIABLES
$cLastHeader = '';
$cCurrHeader = '';
//THIS WOULD BE YOUR QUERY RESULTS LOOP
foreach ($rows AS $nNull => $cDate) {
$cLookup = substr($cDate, 0, 10); //TRIM OUT THE TIME FROM CURRENT RECORD
//IS DATE IN ARRAY? IF NOT, FORMAT
if (isset($aArray[$cLookup])) {
$cCurrHeader = $aArray[$cLookup];
} else {
$cCurrHeader = $cLookup; //WOULD SHOW 'YYYY-MM-DD'
$cCurrHeader = date('F Y', strtotime($cLookup)); //WOULD SHOW 'MONTH YYYY'
}
//HAS HEADER CHANGED? IF SO PRINT
if ($cCurrHeader != $cLastHeader) {
$cLastHeader = $cCurrHeader;
print($cCurrHeader . "\n");
}
//PRINT RECORD
print("\t" . $cDate . "\n");
}
The output from this is :
Today
2015-05-28 18:40:35
2015-05-28 18:40:35
2015-05-28 18:40:35
Yesterday
2015-05-27 00:00:00
December 2014
2014-12-24 00:00:00
Christmas 2014
2014-12-25 00:00:00
December 2014
2014-12-26 00:00:00
March 2001
2001-03-02 00:00:00
SELECT IF( DATEDIFF( NOW( ) , `timestamp` ) =0, "Today", if( DATEDIFF( NOW( ) , `timestamp` ) =1, "Yesterday", DATE_FORMAT( `timestamp`, '%M %d' ) ) ) AS day,
id,timestamp,seen
FROM reports
ORDER BY timestamp DESC
Trying to solve your issue through query,check if it is helpful for you.
As per given screenshot, after today and yesterday this query will give you month and date.
What about get all the rows and checking the date in PHP then group the results which exists at the same time-frame in separate array like the following :
$notifications = $db->prepare("SELECT * FROM reports
ORDER BY timestamp DESC");
$notifications->execute();
$rows = $notifications->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $row ){
if ( date('Ymd') == date('Ymd', strtotime($row['timestamp'])) ){
$today_rows[] = $row;
}
else if (date('Ymd', strtotime('yesterday')) == date('Ymd', strtotime($row['timestamp'])) ){
$yesterday_rows[] = $row;
}
else if () ... etc
}
Now you can use $today_row , $yesterday_rows ... etc in your view and display them as you want.
There are multiple ways to approach this.
First, you can create separate calls and merge the data on php side.
Second, you can use subquery but I don't recommend subqueries as they are kind of tricky and can hog up a lot of resources.
Third, you can use unions. You create 3 separate queries to select based on different scenarios and then use union. This way, you will get all the results in one call.
SELECT *, 'today' as day FROM table WHERE DATE(from_unixtime(comment_date)) = CURRENT_DATE
UNION
SELECT *, 'yesterday' as day FROM table WHERE MONTH(from_unix_MONTH(CURRENT_DATE)) = MONTH(CURRENT_DATE)
So basically, unions are different select statements put together into one query. You can union as many select statements as you like. HOWEVER, note that you want to have all select statements return the SAME COLUMNS or it will error. You also might wanna read up on UNION ALL. There are some minor differences between UNION and UNION ALL but not very.
You should alter your MySQL query to get date difference as well.
SELECT *,DATEDIFF(NOW(),`timestamp`) as `dateDifference` FROM `reports` ORDER BY `dateDifference`
an example is provided here http://sqlfiddle.com/#!9/ecad0/6
Then run an if statement or a switch-case on dateDifference to organize your results the way you want.
Example of a suitable switch-case statement is given below
switch (true) {
case ($dateDifference===0):
echo "Today's reports!";
break;
case ($dateDifference===1):
echo "Yesterday's reports!";
break;
case ($dateDifference>1):
echo "Even older reports!";
break;
case ($dateDifference<0):
echo "An unexpected error occurred. Please contact your system administrator!";
}
I have a page that needs to show a rolling payroll type date. The payroll in question runs 1-15th and 16th - End of month. I have a page that shows the current payroll with a button to go forward or backward. I have the following code which works most of the time, but occasionally skips pay periods. Is there a better way to do this?
//find out what the offset is for the pay period we want to see
$offset = $_GET['offset'];
$offsetdays = $offset * 15;
//Find out what day it is
$dayofmonth = date('j', mktime( 0, 0, 0,date("m"), -$offsetdays, date("Y")));
//if it is the second half of the month set the pay period to be the 1-15th of this month
if($dayofmonth <= 15){
//Echo display dates in HTML
Echo "<div style='width:15%; float:right; text-align:center'>Pay Period End Date <BR>";
echo date("m/d/Y", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), -$offsetdays, date("2014"))), 15, date("2014"))); //End Date
Echo "</div> <div style='width:1%; float:right; text-align:center'>|<BR>|</div>";
Echo "<div style='width:15%; float:right; text-align:center'>Pay Period Start Date <BR>";
Echo date("m/d/Y", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), -$offsetdays, date("2014"))), 1, date("2014"))); //Start Date
Echo "</div>";
Echo "<BR>";
//Set variable dates for SQL
$checkoffset = offsetdays - 15;
$payrollstartdate = date("Y-m-d", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), - $offsetdays, date("2014"))), 1, date("Y")));
$payrollenddate = date("Y-m-d", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), - $offsetdays, date("2014"))), 15, date("Y")));
$checkdayte = date("m/d/Y", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), - $checkoffset, date("2014")))+1, 0, date("Y")));
}
//if it is the first half of the month set the pay period to be the 16- end of this month
if($dayofmonth > 15){
//Echo display dates in HTML
Echo "<div style='width:15%; float:right; text-align:center'>Pay Period End Date <BR>";
echo date("m/d/Y", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), -$offsetdays, date("2014")))+1, 0, date("2014"))); //End Date
Echo "</div> <div style='width:1%; float:right; text-align:center'>|<BR>|</div>";
Echo "<div style='width:15%; float:right; text-align:center'>Pay Period Start Date <BR>";
echo date("m/d/Y", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), -$offsetdays, date("2014"))), 16, date("2014"))); //Start Date
Echo "</div>";
Echo "<BR>";
//Set variable dates for SQL
$checkoffset = offsetdays - 15;
$payrollstartdate = date("Y-m-d", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), - $offsetdays, date("2014"))), 16, date("2014")));
$payrollenddate = date("Y-m-d", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), - $offsetdays, date("2014")))+1, 0, date("2014")));
$checkdayte = date("m/d/Y", mktime(0, 0, 0, date("m", mktime( 0, 0, 0,date("m"), -$checkoffset, date("2014"))), 15, date("2014")));
}
Increasing the offset by 1 will show the next pay period, while decreasing it by one will show the last.
I have also attempted to do this in MySQL without much luck.
In a little more detail here is what I would like to happen:
Today(2/27) when I go to the the page I need the start date to be 2/15/2014, and the end date to be 2/28/2014. When I increase the offset variable I need the start date to be 3/1/2014, and the end date to be 3/15/2014.
Next Tuesday(3/4) when I go to this page I need the start date to be 3/1/2014, and the end date to be 2/15/2014. When I increase the offset variable I need the start date to be 3/15/2014, and the end date to be 3/31/2014.
Here is a MySQL-centered way of solving this problem. It's based on the idea that date intervals are efficiently described using the starting date of each interval.
The time intervals you need for your payroll business rules are pay periods -- "half" months -- defined as starting on YYYY-mm-01 and YYYY-mm-16.
So, given an arbitrary timestamp, for example NOW() or 1941-12-07 08:00, we need a way to determine the starting day of the pay period. It would be handy to have a MySQL stored function to do that, in such a way that
BIMONTHLY_PAY_PERIOD_START('1941-12-07 08:00') produces '1941-12-01'
and
BIMONTHLY_PAY_PERIOD_START('2014-03-20 20:50') produces '2014-03-16'
Here's a definition of that stored function.
DELIMITER $$
DROP FUNCTION IF EXISTS `BIMONTHLY_PAY_PERIOD_START`$$
CREATE FUNCTION `BIMONTHLY_PAY_PERIOD_START` (datestamp DATETIME)
RETURNS DATE
NO SQL
DETERMINISTIC
COMMENT 'returns first day of bimonthly pay period: 1,16 of month'
BEGIN
DECLARE dy INT;
SET dy = DAY(datestamp);
IF dy<=15
THEN SET dy=0;
ELSE SET dy=15;
END IF;
RETURN (DATE(DATE_FORMAT(datestamp, '%Y-%m-01')) + INTERVAL dy DAY);
END$$
DELIMITER ;
It does it, basically, by finding the first day of the month and then offsetting it by 15 days if necessary.
Next, we need a function, given a pay period (remember, it's defined by its first day), to move forward or backward a particular number of pay periods. For example
BIMONTHLY_PAY_PERIOD_ADD('1941-12-01', -1) goes back one period to '1941-11-16'
and
BIMONTHLY_PAY_PERIOD_ADD('2014-03-16', 3) goes forward three to '2014-05-01'
Here is a definition of that function:
DELIMITER $$
DROP FUNCTION IF EXISTS `BIMONTHLY_PAY_PERIOD_ADD`$$
CREATE FUNCTION `BIMONTHLY_PAY_PERIOD_ADD`(datestamp DATETIME, incr INT)
RETURNS DATE
NO SQL
DETERMINISTIC
COMMENT 'returns first day of a pay period in the future or past'
BEGIN
DECLARE pp DATE;
SET pp = BIMONTHLY_PAY_PERIOD_START(datestamp);
IF ABS(incr) MOD 2 = 1
THEN
SET pp = pp + INTERVAL 16 DAY;
SET incr = incr - 1;
END IF;
SET incr = incr DIV 2;
SET pp = pp + INTERVAL incr MONTH;
RETURN BIMONTHLY_PAY_PERIOD_START(pp);
END$$
DELIMITER ;
It works stably, without skipping pay periods, by using the insight that there are two pay periods in each month.
Finally, you need a way to use these pay period dates in SQL logic. For example, suppose you have a table of payroll records containing
employee_id INT identifies the employee
clock_in DATETIME the date and time the employee clocked in
clock_out DATETIME the date and time the employee clocked out
and you want to summarize minutes-on-the-job by employee and pay period for the last six complete pay periods. That is, you want to exclude minutes-on-the-job for the present, incomplete, pay period.
You do this:
SELECT employee_id,
BIMONTHLY_PAY_PERIOD_START(clock_out) AS pay_period,
SUM(TIMESTAMPDIFF(MINUTE,clock_in, clock_out)) AS minutes_worked
FROM payroll_data
WHERE clock_out >= BIMONTHLY_PAY_PERIOD_ADD(NOW(), -6)
AND clock_out < BIMONTLY_PAY_PERIOD_START(NOW())
GROUP BY employee_id, BIMONTHLY_PAY_PERIOD_START(clock_out)
ORDER BY employee_id, BIMONTHLY_PAY_PERIOD_START(clock_out)
This query uses the functions we've defined to figure out which pay period each day's work is in. Notice that it uses the >= comparison operator for the start of the date range it needs, and the < operator combined with the day after the date range to handle the end of the date range.
If you need the last day of the present pay period, that's easy. It's the day before the start of the next pay period.
BIMONTHLY_PAY_PERIOD_ADD(NOW(),1) - INTERVAL 1 DAY
This way of handling date arithmetic is robust and reasonably fast. It exploits useful features of MySQL like date-range index searches and aggregate (GROUP BY) queries. You should be able to combine it with client code (php) to make nice presentations of your information.
One of the resons that could cause skipping periods is your way of computing $offsetdays. All months aren't 30 days long, so multiplying the period offset by 15 won't always work, especially with big offsets.
<?php
// Set default timezone to avoid warnings
date_default_timezone_set("UTC");
function get_period_bounds($offset = 0) {
$secondhalf = ($offset % 2) == 0 xor (int) date('j') >= 15;
$monthnumber = ceil((int) date('n') + $offset / 2);;
$period_begin = mktime(0, 0, 0, // 00:00:00
$monthnumber,
$secondhalf ? 16 : 1);
$period_end = mktime(0, 0, 0, // 00:00:00
$secondhalf ? $monthnumber + 1 : $monthnumber,
$secondhalf ? 0 : 15);
return array($period_begin, $period_end);
}
The magic
Let's explain the tiny bits of magic involved here :
Month half
To determine if the target period will be in the second half of the month, we first need to know whether today is on the first or second half of the month. That's done with (int) date('j') >= 15. We simply convert the day number to an integer and compare it to the threshold (15).
If today is on the second half, the next period will be on the first half, the one after on the second, the one after on the first, etc. So if I skip an even number of periods (i.e. a round number of months), the target period is still on the second half. This applies with a negative offset (go backward) and easily adapts if today is on the first half.
Hence the target period is the second half if :
Today is in the first half and we skip an odd number of periods ;
or today is in the second half and we skip an even number of periods.
We can combine these two conditions into one using the XOR (eXclusive OR) operator :
target_in_second_half = (today in second half) XOR (even offset)
Month
Now that we know in which half of the month the target period is, we need to know in which month is is. Since there are exactly two periods in a month, half of the offset is the month offset. So we add half the offset to the current month and ceil the result. Most of the magic happens in the mktime function that understands for instance than month #0 is the last month of the previous year.
Begin date
The beginning date is pretty straightforward : we simply use the relative month number with the current year (used by default in mktime). If the target period is on the first period, the first day of the period is the 1st, and 16th otherwise.
I used the ternary operator for readability :
(BOOLEAN TEST) ? VALUE_IF_TRUE : VALUE_IF_FALSE
End date
The case of the first half is straightforward, we use the relative month index and the end day is the 15th. For the second half, the last little trick is to get the number of the last day of the month. To do so, we rely (again) on mktime's ability to transform relative second/minute/hour/day/month/year by adding one to the month number and setting the day number to 0, i.e. the last day of the previous month. To sum up, we get the last day of the month before the month after the period month, i.e. the last day of the month of the period.
Check
You may want to check if this formula doesn't skip a period once in a while. This little loop will help you do that :
<?php
$len = 500; // Change to your needs
$last = 0;
for ($i = -$len; $i <= $len; $i++) {
$a = get_period_bounds($i);
printf("Period %d is from %s to %s [%s]<br />",
$i, // Period number
date('Y-m-d', $a[0]), // Begin date
date('Y-m-d', $a[1]), // End date
$last + 3600 * 24 == $a[0] ? 'x' : ' ' // Prints a x if the last day of the previous period is the day before the first day of this period
);
$last = $a[1];
}
This snippet will print the period dates between -$len and $len, with a checkmark [x] if the last tay of the period before is really the day before the first day of the current period, i.e. whether or not the periods really are adjacent.
(note : there won't be a check on the first line because there's no previous data to compare)
This sounds like a silly question but I am struggling to think of how best to tackle it.
I have 2 dates, lets say 15-03-2012 to 19-03-2012. I need to extract all combinations working down from all 4 days together to 4 individual days, output like this (formatting change for simplicity):
15, 16, 17, 18
15,16, 17
16, 17, 18
15, 16
16, 17
17, 18
15
16
17
18
I am looking for the best looping method, the date range will vary in length, the format isn't relevant but it needs to be php dates from which I will extract the exact format required. I will be using each date set to execute code at each iteration so has to be dates/loops.
Create two Date objects of the values you have, write a while loop that runs until the start variable (your start date) is equal to the end variable (your end date) and you should be done.
The general idea:
$day = 24 * 60 * 60;
for($startDate = $dateA; $startDate <= $dateB; $startDate += day)
{
for($endDate = $startDate; $endDate <= $dateB; $endDate += day)
{
for($dateIndex = startDate; $dateIndex <= $endDate; $dateIndex += day)
{
$output = getDate($dateIndex);
echo($output['month'] . $output['day'] . ',');
}
echo '</br>';
}
}