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)
Related
Basically, I have a date set to today. From that date, I want to get the next and previous 20th day of the month. Basically, starting with 07/10/2020 (date of the post in d/m/Y format), I should end up with a date/timestamp to 20/09/2020 (previous 20th) and 20/10/2020 (next 20th).
I tried a few things and ended up with:
$ld_next_str = date("d/m/Y", strtotime("20 day this month"));
$ld_next_dt = DateTime::createFromFormat("d/m/Y", $ld_next_str);
$ld_prev_str = date("d/m/Y", strtotime("20 day last month"));
$ld_prev_dt = DateTime::createFromFormat("d/m/Y", $ld_prev_str);
But turns out this : "20 day this month" just adds 20 days to the current day so right now (on the seventh) it returns the 27th.
Knowing this, I figured I could probably just do something like subtracting the current day to that date (27 - 7 = 20) but I feel like there's probably a simpler way to do it, using strtotime without any extra steps.
Thanks in advance!
PS : I know you can get some days using "first" / "second" / "third" etc... but (I think) it doesn't go any higher than "twelfth", because I tried "twentieth" and that didn't work...
The formula for the previous 20th is:
mktime(0, 0, 0, $month - (1 - floor($day / 20)), 20, $year))
The formula for the next 20th is:
mktime(0, 0, 0, $month + floor($day / 20), 20, $year))
As a demo:
for ($month = 1; $month <= 12; $month++) {
for ($day = 1; $day <= 30; $day += 5) {
$prev = mktime(0, 0, 0, $month - (1 - floor($day / 20)), 20, 2020);
$next = mktime(0, 0, 0, $month + floor($day / 20), 20, 2020);
echo "$day/$month/2020: ", date('d/m/Y', $prev), ', ', date('d/m/Y', $next), PHP_EOL;
}
}
Outputs:
1/1/2020: 20/12/2019, 20/01/2020
6/1/2020: 20/12/2019, 20/01/2020
11/1/2020: 20/12/2019, 20/01/2020
16/1/2020: 20/12/2019, 20/01/2020
21/1/2020: 20/01/2020, 20/02/2020
26/1/2020: 20/01/2020, 20/02/2020
1/2/2020: 20/01/2020, 20/02/2020
6/2/2020: 20/01/2020, 20/02/2020
11/2/2020: 20/01/2020, 20/02/2020
16/2/2020: 20/01/2020, 20/02/2020
21/2/2020: 20/02/2020, 20/03/2020
...
Solution with an external class (a DateTime extension) that supports the cron syntax.
The string "0 0 20 * *" specifies the 20th of every month and every weekday at 00:00.
The nextCron() and previousCron() methods are used to determine the previous/next point in time based on a specific date.
$cronStr = "0 0 20 * *"; //every 20th 00:00
//source on https://github.com/jspit-de/dt
$next20 = dt::create('now')->nextCron($cronStr);
echo $next20; //2020-10-20 00:00:00
$prev20 = dt::create('now')->previousCron($cronStr);
echo $prev20; //2020-09-20 00:00:00
Outputs for the execution on October 8, 2020.
Here a better solution by using DateTime
$lastMonth = (new DateTime("last month"))->format("Y-m-20");
$thisMonth = (new DateTime("this month"))->format("Y-m-20");
$nextMonth = (new DateTime("next month"))->format("Y-m-20");
echo "$lastMonth - $thisMonth - $nextMonth";
I need to get a next month payment date right. So I'm taking the last due date
and adding a month to it. How can I do it?
My example is: last payment was 31-st of Januarry, I'm doing
Carbon::create(2018, 1, 31, 0, 0, 0)->addMonthsNoOverflow(1)
and it works, it gives 2018-02-28 but after that next payment will be due on 28-th again. So I need to set a date after I'm adding a month.
Carbon::create(2018, 2, 28, 0, 0, 0)->addMonthsNoOverflow(1)->day(31)
it gives me 2018-03-31 which is good but if I use this formula with the first example
Carbon::create(2018, 1, 31, 0, 0, 0)->addMonthsNoOverflow(1)->day(31)
it gives me overflow 2018-03-03. And this is my problem. What should I do? Is there something like ->setDayNoOverflow(31) ?
You should not use last payment date, but keep the first date and calculate all the other date from the first, not the previous one:
Carbon::create(2018, 1, 31, 0, 0, 0)
Carbon::create(2018, 1, 31, 0, 0, 0)->addMonthsNoOverflow(1)
Carbon::create(2018, 1, 31, 0, 0, 0)->addMonthsNoOverflow(2)
Carbon::create(2018, 1, 31, 0, 0, 0)->addMonthsNoOverflow(3)
Supposing you don't have this data, you still can:
$day = 31;
$date = Carbon::create(2018, 1, 28, 0, 0, 0);
$date->addMonthsNoOverflow(1);
$date->day(min($day, $date->daysInMonth));
Seems like there is no such function in Carbon, so why don't make it yourself?
public function setDaysNoOverflow($value)
{
$year = $this->year;
$month = $this->month;
$this->day((int)$value);
if ($month !== $this->month) {
$this->year = $year;
$this->month = $month;
$this->modify('last day of this month');
}
return $this;
}
Just add it to Carbon/Carbon.php and it should do the job.
It's based on addMonthsNoOverflow function source.
Warning: it's not tested, only for inspiration purposes.
I think the actual problem here is about the business logic or database schema, not date manipulation.
Despite I'm not sure what your subscription model is, it seems that you want to keep track of two separate pieces of information:
date until which service has been paid for,
day of month on which the payment is due.
By trying to store both in a single field you may lose some information, just as in case of January/February/March transition. By using separate fields you could prevent it and build a due date for payment on each month, for example as proposed by KyleK:
$lastPayment->addMonthsNoOverflow(1)->day(
min($paymentDue->day, $lastPayment->daysInMonth)
);
In my opinion this is a bit complicated way to do this and could by greatly simplified by:
using business months (30 days) instead of calendar months,
letting go of the original payment day and just adding a month to the last payment.
However I understand that it may not be up to you to decide.
You can use it as a loop
// from month
$min = 3;
// to mont
$max = 9;
// all dates
$paymentDates = [];
$date2 = \Carbon\Carbon::now();
// next day (or today)
$day = $date2->addDay(1)->format('d');
$currentMonth = $date2->format('m');
$year = $date2->format('Y');;
$date = \Carbon\Carbon::now();
$nextMonth = $currentMonth;
for($i = $min; $i <= $max; $i++) {
$date = \Carbon\Carbon::create($year, $currentMonth, $day)->addMonthsNoOverflow($nextMonth)->format('d/m/Y');
// add to array
array_push($paymentDates, $date);
// next month
$nextMonth++;
}
You can use addMonth() function available in Carbon Library like below
Carbon::create(2018, 1, 31, 0, 0, 0)->addMonth();
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 just want to know, how to check the timestamp(any month, any year, any date) is present in the current month or not in PHP?
For.eg 1456132659 This is an example timestamp which indicates the last month date. I want a condition like
if(1456132659 is in current month) {
//true
}
else {
//false
}
How can I do this?
Simply compare the month -
if(date('m', 1456132659) === date('m')) {
//your code goes here
}
You first need to fetch the timestamp of first and last date of current month by using below PHP code-
$first_minute = mktime(0, 0, 0, date("n"), 1);
$last_minute = mktime(23, 59, 0, date("n"), date("t"));
// Now just compare the timestamp for above two values by using below condition
if($timestamp >= $first_minute AND $timestamp <= $last_minute) {
// Your line of code if condtion fullfill.
}
I am trying to get stripe to set a end_trial date on the next occurrence of whatever day of the month the user chooses. i.e. If today is the 16th and the user chooses the 15th I need the unix timestamp for the 15th of the next month. However if today was the 14th I need the timestamp for tomorrow.
I tried the solution found on this SO question Find the date for next 15th using php .
When i ran the code suggested in that question and substituted 15 for 31
$nextnth = mktime(0, 0, 0, date('n') + (date('j') >= 31), 31);
echo date('Y-m-d', $nextnth);
The result is 2013-03-03
I also tried this one Get the date of the next occurrence of the 18th .
The second one would actually give me 2013-03-31 when i ran it one 2013-1-31.
Both had unexpected results. Is february the problem? Any guidance will be much appreciated.
Here is a way to do it.
function nextDate($userDay){
$today = date('d'); // today
$target = date('Y-m-'.$userDay); // target day
if($today <= $userDay){
$return = strtotime($target);
}
else{
$thisMonth = date('m') + 1;
$thisYear = date('Y');
if($userDay >= 28 && $thisMonth == 2){
$userDay = 28;
}
while(!checkdate($thisMonth,$userDay,$thisYear)){
$thisMonth++;
if($thisMonth == 13){
$thisMonth = 1;
$thisYear++;
}
}
$return = strtotime($thisYear.'-'.$thisMonth.'-'.$userDay);
}
return $return;
}
// usage
echo date('Y-m-d',nextDate(29));
We get the user's choice and compare it today.
If today is less than or equal to user choice, we return the timestamp for this month.
If today is greater than user choice, we loop through dates, adding a month (or a year if it's $thisMonth hits 13). Once this date does exist again, we have our answer.
We check the dates using php's checkdate function, strtotime and date.
I really don't understand the question completely. You can easily determine the date for next 30 days for example
$next_ts = time() + 30 * 86400; // add 30 days to current timestamp
$next = date('Y-m-d', $next_ts); // format string as Y-m-d
echo $next;
If that is not what you need, please explain the problem.