Users are allowed to pick a time for delivery up until the closing time of the store which can be 1am.
The list should should show all times up to 1am even if it is after midnight.
example 1: User arrives at 18:00 they see 18:00, 18:30, 19:00 and so on up until 00:30, 01:00
example 2: user arrives at 00:10 should see
00:30, 01:00 not
01:00, 01:30, 02:00, 02:30 etc because after midnight has become a has become a new day/date.
I am getting 30 minute time intervals between two times, it is fine as long as both times are in the same day e.g. 17:00 and 23:00. If the end time is past midnight I am not able to get the intervals so 17:00 to 01:00 doesn't give any intervals.
I understand 01:00 is another day but not quite sure how to fix it using a dynamic date. I keep thinking current day +1 will be fine unless it is after mindight then it will be an extra day if that makes sense.
Here's my code:
$timestamp = time() + 60*60;
$earliest = date("h:i ",$timestamp);
$period = new DatePeriod(
new DateTime($earliest),
new DateInterval('PT30M'),
new DateTime('01:00')
);
foreach ($period as $date) {
echo '<option value="">'.$date->format("H:i").'</option>';
}
Because 24:00 works as 00:00 I tried 25:00 but that didn't work. Any help appreciated!
UPDATE: following the answer below coverted to dynamic date, if I use date like:
$start = date('Y-m-d H:i');
$startdate = date('Y-m-d');
$end = date('Y-m-d', strtotime($startdate . ' +1 day'))." 01:00";
$period = new DatePeriod(
DateTime::createFromFormat('Y-m-d H:i','2020-04-03 17:00'),
new DateInterval('PT30M'),
DateTime::createFromFormat('Y-m-d H:i','2020-04-04 01:00')
);
After midnight it's another day added on which is problematic.
You also have to mention exact dates and not just time as 01:00. Because this way, it assumes the current date and hence you really can't have a time period between 17:00 and 01:00 on the same day. Below is how you would do it:
<?php
$period = new DatePeriod(
DateTime::createFromFormat('Y-m-d H:i','2020-04-03 17:00'),
new DateInterval('PT30M'),
DateTime::createFromFormat('Y-m-d H:i','2020-04-04 01:00')
);
foreach ($period as $date) {
echo $date->format("H:i"),PHP_EOL;
}
Demo: https://3v4l.org/5sj9Z
Update:
Since you have only times and not dates, you create DateTime objects, compare them, add 1 day to end time if it's smaller and then loop over the intervals using DatePeriod
<?php
$times = [
['18:00','01:00'],
['17:00','23:00'],
['00:10','01:00']
];
foreach($times as $time){
$start_time = DateTime::createFromFormat('H:i',$time[0]);
$end_time = DateTime::createFromFormat('H:i',$time[1]);
if($end_time < $start_time){
$end_time->add(new DateInterval('P1D'));
}
$period = new DatePeriod(
$start_time,
new DateInterval('PT30M'),
$end_time
);
echo $start_time->format('Y-m-d H:i:s'),' ',$end_time->format('Y-m-d H:i:s'),PHP_EOL;
foreach ($period as $date) {
echo $date->format("H:i"),PHP_EOL;
}
echo PHP_EOL;
}
Demo: https://3v4l.org/Ldpst
You can lose the whole timestamp calculation and have it all handled by DateTime:
// here we get the next round delivery time
$nextAvailableHalfHour = getNextHalfHourMark();
$todayMidnight = new DateTime('today'); // this creates today with time 00:00:00
$todayOneOclock = new DateTime('today 01:00'); // this creates today with time 01:00:00
// this condition says if we're now between midnight and 1 o'clock, use today
// otherwise use tomorrow
$endDate = $nextAvailableHalfHour >= $todayMidnight && $nextAvailableHalfHour <= $todayOneOclock
? $todayOneOclock
: new DateTime('tomorrow 01:00'); // create specific time tomorrow
$period = new DatePeriod(
$nextAvailableHalfHour,
new DateInterval('PT30M'),
$endDate
);
/**
* Gets the next available time with round half hour (:00 or :30).
*/
function getNextHalfHourMark(): DateTime
{
$now = new DateTime(); // create current date and time
$currentMinutes = (int) $now->format('i');
// if we are between :00 and :30
if ($currentMinutes > 0 && $currentMinutes < 30) {
// set to :30 minutes of current hour
$now->setTime($now->format('H'), 30);
// else if we are between :30 and :00
} elseif ($currentMinutes > 30 && $currentMinutes <= 59) {
// set to :00 minutes of next hour
$now->setTime($now->format('H') + 1, 00);
}
return $now;
}
The tomorrow and today strings are enabled by relative formats.
I'm struggling to to write a PHP function that would calculate time difference between two hours (minus the brake) and the result would be in decimal format. My inputs are strings in 24-hour format (hh:mm):
$start = '07:00'; //started at 7 after midnight
$brake = '01:30'; //1 hour and 30 minutes of brake
$finish = '15:00'; //finished at 3 afternoon
//the desired result is to print out '6.5'
example 2
$start = '19:00'; //started late afternoon
$brake = '00:30'; //30 minutes of brake
$finish = '03:00'; //finished at 3 after midnight
//the desired result is to print out '7.5'
I used to have following formula in MS Excel which worked great:
=IF(D12>=F12,((F12+1)-D12-E12)*24,(F12-D12-E12)*24) '7.5 worked hours
where
D12 - Start time '19:00
F12 - Finish time '03:00
E12 - Brake time '00:30
I tried to play with strtotime() with no luck. My PHP version is 5.4.45. Please help
To provide a solution that doesn't require as much mathematics or parsing of the time values.
Assuming the day is not known, we can also account for the offset of the finish time and start time, when the start time is late at night.
Example: https://3v4l.org/FsRbT
$start = '07:00'; //started at 7 after midnight
$break = '01:30'; //1 hour and 30 minutes of brake
$finish = '15:00'; //finished at 3 afternoon
//create the start and end date objects
$startDate = \DateTime::createFromFormat('H:i', $start);
$endDate = \DateTime::createFromFormat('H:i', $finish);
if ($endDate < $startDate) {
//end date is in the past, adjust to the next day
//this is only needed since the day the time was worked is not known
$endDate->add(new \DateInterval('PT24H'));
}
//determine the number of hours and minutes during the break
$breakPeriod = new \DateInterval(vsprintf('PT%sH%sM', explode(':', $break)));
//increase the start date by the amount of time taken during the break period
$startDate->add($breakPeriod);
//determine how many minutes are between the start and end dates
$minutes = new \DateInterval('PT1M');
$datePeriods = new \DatePeriod($startDate, $minutes, $endDate);
//count the number of minute date periods
$minutesWorked = iterator_count($datePeriods);
//divide the number of minutes worked by 60 to display the fractional hours
var_dump($minutesWorked / 60); //6.5
This will work with any time values within a 24 hour period 00:00 - 23:59. If the day the times were worked are known, the script can be modified to allow for the day to be given and provide more precise timing.
To do this, convert you string times into a unix timestamp. This is an integer number of seconds since the unix epoch (00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, minus the number of leap seconds that have taken place since then). Do your math, then use the Date() function to format it back into your starting format:
<?php
$start = '19:00'; //started late afternoon
$break = '00:30'; //30 minutes of brake
$finish = '03:00'; //finished at 3 after midnight
//get the number of seconds for which we took a $break
//do this by converting break to unix timestamp, then extracting the hour and multiplying by 360
//and do the same extracting minutes and multiplying by 60
$breaktime = date("G",strtotime($break))*60*60 + date("i",strtotime($break))*60;
//get start time
$unixstart=strtotime($start);
//get finish time. Add a day if finish is tomorrow
if (strtotime($finish) < $unixstart) {
$unixfinish = strtotime('+1 day', strtotime($finish));
} else {
$unixfinish = strtotime($finish);
}
//figure out time worked
$timeworked = ($unixfinish - $unixstart - $breaktime) / 3600;
echo $timeworked;
?>
Another way, using DateTime. Basically, create 2 DateTime objects with the times of start and finish. To the start time, subtract the brake time, and the subtract from the result the end time.
You need to split the brake time in order to use modify().
<?php
$start = '07:00'; //started at 7 after midnight
$brake = '01:30'; //1 hour and 30 minutes of brake
$brakeBits = explode(":", $brake);
$finish = '15:00'; //finished at 3 afternoon
$startDate = \DateTime::createFromFormat("!H:i", $start);
$startDate->modify($brakeBits[0]." hour ".$brakeBits[1]." minutes");
$endDate = \DateTime::createFromFormat("!H:i", $finish);
$diff = $startDate->diff($endDate);
echo $diff->format("%r%H:%I"); // 06:30
Demo
I am creating a website that allow deliveries only within certain delivery time frames.
Here is an example of exactly what I'm looking for:
FakeCompany delivers on Wednesday and allows customers to place orders between Friday and Tuesday with a cutoff time of 11 PM on Tuesday night.
I need to figure out when the customer logs in if ordering is allowed (between Friday - Tuesday 11 PM). I also need to know how much longer they have to order.
I know the PHP date('N') function that Friday is 5:
date('N', strtotime('Friday'));
and Tuesday is 1:
date('N', strtotime('Tuesday'));
These time ranges may change, so I need a simple solution.
Here is what I started with, and now I'm lost on how to do this.
//Set today and get from database start / end days and end time
$today = (int)date('N');
$startDay = (int)date('N', strtotime('Friday'));
$endDay = (int)date('N', strtotime('Tuesday'));
$endDayTime = '11:00:00';
//If today is before start date
if($today >= $startDay && $today <= $endDay){
//This works only if the end date is not the following week
//It also needs to be before end day time!
}
I think I need to get the date of the week based on the DAY (Friday) and convert that to this weeks Friday if Friday has not passed or next weeks Friday and do the same with end date.
Then I need to know if today is between those dates / times.
$now = new DateTime();
$tuesday = new DateTime('last Tuesday');
$friday = new DateTime('Friday 11pm');
if ($tuesday < $now && $now < $friday) {
$interval = $friday->diff($now);
echo $interval->format('%d day %h hours %i minutes left to order');
}
else {
echo "you can't order now";
}
See it in action
Here is a function to check that today is an approved day then if its tuesday also make sure it is before 11pm:
/*
Acceptable days:
5 - friday
6 - saturday
7 - sunday
1 - monday
2 - tuesday
*/
//Can they order today?
if(in_array(date('N'),array(1,2,5,6,7))){
//if today is tuesday is it before 11pm?
if(date('N') == 2){
if(date('H')<23){
//23 = 11pm in 24 hour time
//Then can order
}
else{
//Then CANT order
}
}
//Its not tuesday so we dont care what time it is they can order
}
for the end day I think you could do it like this:
$endDay = (int)date('N', strtotime('Friday') + 3 * 24 * 3600 + 23 * 3600);
strtotime('Friday') to get friday and add 3 days of 24 hours to it, and it'll be Tuesday 0 am. Then you add 23 hours time to it as it finish at 11pm.
$today = (int)date('N');
$startDay = (int)date('N', strtotime('Friday'));
$endDay = (int)date('N', strtotime('Friday') + 3 * 24 * 3600 + 23 * 3600);
//If today is before start date
if($today >= $startDay && $today <= $endDay){
//now it works
}
Here is exactly what I am looking for.
The dates provided may not be this week or even this month, so we need to figure out based on the date what the day of the week was and set the date on this week or next week to same day depending on today (kinda confusing).
See It In Action
//IF USING A DATABASE TO STORE DATE/TIME
//Get route times
//When Using Database: $query = "SELECT * FROM routes WHERE id = '".$user['route_one']."'";
//When Using Database: $result = $this->query($query);
//When Using Database: $route = $this->fetchArray($result);
//Set date vaiables
$thisWeek = new DateTime();
$routeStart = new DateTime(date('Y-m-d H:i:s', strtotime('2013-04-21 00:00:00')));
//When Using Database: $routeStart = new DateTime(date('Y-m-d H:i:s', strtotime($route['start_time'])));
$routeEnd = new DateTime(date('Y-m-d H:i:s', strtotime('2013-04-24 00:00:00')));
//When Using Database: $routeEnd = new DateTime(date('Y-m-d H:i:s', strtotime($route['end_time'])));
$interval = $routeStart->diff($routeEnd);
$numDays = abs($interval->format('%d'));
//Check if today is past or on the start date, else start date is next week, and set day of week
if($thisWeek->format('N') >= $routeStart->format('N')){
$startDate = $thisWeek->modify('last '.$routeStart->format('l'));
}
else{
$startDate = $thisWeek->modify($routeStart->format('l'));
}
//Now that we know the start date add the amount of days to the start date to create the end date
$endDate = new DateTime($startDate->format('Y-m-d H:s:i'));
$endDate->modify('+'.$numDays.' days '.$routeEnd->format('H').' hours');
//Check to see if user is within the time range to order or not
$today = new DateTime();
if($startDate <= $today && $today <= $endDate){
//Find out how much longer ordering can take place
$interval = $endDate->diff($today);
$output = 'Allowed to order!<br>';
$output .= '<div id="orderTimeCounter">'.$interval->format('%d days %h hours %i minutes left to order').'</div>';
}
else{
//If today is before start date set start date to THIS week otherwise NEXT week
if($startDate >= $today){
$startDate = $startDate->modify('this '.$routeStart->format('l'));
}
else{
$startDate = $startDate->modify('next '.$routeStart->format('l'));
}
//Find out how much longer until ordering is allowed
$interval = $today->diff($startDate);
$output = 'Not allowed to order!';
$output .= '<div id="orderTimeCounter">'.$interval->format('%d days %h hours %i minutes until you can order').'</div>';
}
echo $output;
I am computing for the time difference of night shift schedule using time only
lets say that I have this data:
$actual_in_time = 6:45 PM //date July 30, 2013
$actual_out_timeout = 7:00 AM //date July 31, 2013
I have to compute for the time difference where the time in should be converted to a whole time, therefore
$actual_in_time = //(some code to convert 6:45 PM to 7:00 PM)
$converted_in_time = $actual_in_time;
Now here is my code to that:
$actual_out_time += 86400;
$getInterval = $actual_out_time - $converted_in_time;
$hours = round($getInterval/60/60, 2, PHP_ROUND_HALF_DOWN);
$hours = floor($hours);
I am not getting the results I wanted. How do you compute for the time difference where the basis is just the time?
Using DateTime object
$start = new DateTime('2000-01-01 6:45 PM');
$end = new DateTime('2000-01-01 7:00 AM');
if ($end<$start)$end->add(new DateInterval('P1D'));
$diff = date_diff($start,$end);
echo $diff->format('%h hours %i minutes');
Add 1 day if your end time is less than start time.
You can use the second parameter in strtotime to get a relative time.
$start = strtotime($startTime);
$end = strtotime($endTime, $start);
echo ($end-$start)/3600;
I've been working a lot with the DateTime class and recently ran into what I thought was a bug when adding months. After a bit of research, it appears that it wasn't a bug, but instead working as intended. According to the documentation found here:
Example #2 Beware when adding or
subtracting months
<?php
$date = new DateTime('2000-12-31');
$date->modify('+1 month');
echo $date->format('Y-m-d') . "\n";
$date->modify('+1 month');
echo $date->format('Y-m-d') . "\n";
?>
The above example will output:
2001-01-31
2001-03-03
Can anyone justify why this isn't considered a bug?
Furthermore does anyone have any elegant solutions to correct the issue and make it so +1 month will work as expected instead of as intended?
Why it's not a bug:
The current behavior is correct. The following happens internally:
+1 month increases the month number (originally 1) by one. This makes the date 2010-02-31.
The second month (February) only has 28 days in 2010, so PHP auto-corrects this by just continuing to count days from February 1st. You then end up at March 3rd.
How to get what you want:
To get what you want is by: manually checking the next month. Then add the number of days next month has.
I hope you can yourself code this. I am just giving what-to-do.
PHP 5.3 way:
To obtain the correct behavior, you can use one of the PHP 5.3's new functionality that introduces the relative time stanza first day of. This stanza can be used in combination with next month, fifth month or +8 months to go to the first day of the specified month. Instead of +1 month from what you're doing, you can use this code to get the first day of next month like this:
<?php
$d = new DateTime( '2010-01-31' );
$d->modify( 'first day of next month' );
echo $d->format( 'F' ), "\n";
?>
This script will correctly output February. The following things happen when PHP processes this first day of next month stanza:
next month increases the month number (originally 1) by one. This makes the date 2010-02-31.
first day of sets the day number to 1, resulting in the date 2010-02-01.
Here is another compact solution entirely using DateTime methods, modifying the object in-place without creating clones.
$dt = new DateTime('2012-01-31');
echo $dt->format('Y-m-d'), PHP_EOL;
$day = $dt->format('j');
$dt->modify('first day of +1 month');
$dt->modify('+' . (min($day, $dt->format('t')) - 1) . ' days');
echo $dt->format('Y-m-d'), PHP_EOL;
It outputs:
2012-01-31
2012-02-29
This may be useful:
echo Date("Y-m-d", strtotime("2013-01-01 +1 Month -1 Day"));
// 2013-01-31
echo Date("Y-m-d", strtotime("2013-02-01 +1 Month -1 Day"));
// 2013-02-28
echo Date("Y-m-d", strtotime("2013-03-01 +1 Month -1 Day"));
// 2013-03-31
echo Date("Y-m-d", strtotime("2013-04-01 +1 Month -1 Day"));
// 2013-04-30
echo Date("Y-m-d", strtotime("2013-05-01 +1 Month -1 Day"));
// 2013-05-31
echo Date("Y-m-d", strtotime("2013-06-01 +1 Month -1 Day"));
// 2013-06-30
echo Date("Y-m-d", strtotime("2013-07-01 +1 Month -1 Day"));
// 2013-07-31
echo Date("Y-m-d", strtotime("2013-08-01 +1 Month -1 Day"));
// 2013-08-31
echo Date("Y-m-d", strtotime("2013-09-01 +1 Month -1 Day"));
// 2013-09-30
echo Date("Y-m-d", strtotime("2013-10-01 +1 Month -1 Day"));
// 2013-10-31
echo Date("Y-m-d", strtotime("2013-11-01 +1 Month -1 Day"));
// 2013-11-30
echo Date("Y-m-d", strtotime("2013-12-01 +1 Month -1 Day"));
// 2013-12-31
My solution to the problem:
$startDate = new \DateTime( '2015-08-30' );
$endDate = clone $startDate;
$billing_count = '6';
$billing_unit = 'm';
$endDate->add( new \DateInterval( 'P' . $billing_count . strtoupper( $billing_unit ) ) );
if ( intval( $endDate->format( 'n' ) ) > ( intval( $startDate->format( 'n' ) ) + intval( $billing_count ) ) % 12 )
{
if ( intval( $startDate->format( 'n' ) ) + intval( $billing_count ) != 12 )
{
$endDate->modify( 'last day of -1 month' );
}
}
I agree with the sentiment of the OP that this is counter-intuitive and frustrating, but so is determining what +1 month means in the scenarios where this occurs. Consider these examples:
You start with 2015-01-31 and want to add a month 6 times to get a scheduling cycle for sending an email newsletter. With the OP's initial expectations in mind, this would return:
2015-01-31
2015-02-28
2015-03-31
2015-04-30
2015-05-31
2015-06-30
Right away, notice that we are expecting +1 month to mean last day of month or, alternatively, to add 1 month per iteration but always in reference to the start point. Instead of interpreting this as "last day of month" we could read it as "31st day of next month or last available within that month". This means that we jump from April 30th to May 31st instead of to May 30th. Note that this is not because it is "last day of month" but because we want "closest available to date of start month."
So suppose one of our users subscribes to another newsletter to start on 2015-01-30. What is the intuitive date for +1 month? One interpretation would be "30th day of next month or closest available" which would return:
2015-01-30
2015-02-28
2015-03-30
2015-04-30
2015-05-30
2015-06-30
This would be fine except when our user gets both newsletters on the same day. Let's assume that this is a supply-side issue instead of demand-side We're not worried that the user will be annoyed with getting 2 newsletters in the same day but instead that our mail servers can't afford the bandwidth for sending twice as many newsletters. With that in mind, we return to the other interpretation of "+1 month" as "send on the second to last day of each month" which would return:
2015-01-30
2015-02-27
2015-03-30
2015-04-29
2015-05-30
2015-06-29
Now we've avoided any overlap with the first set, but we also end up with April and June 29th, which certainly does match our original intuitions that +1 month simply should return m/$d/Y or the attractive and simple m/30/Y for all possible months. So now let's consider a third interpretation of +1 month using both dates:
Jan. 31st
2015-01-31
2015-03-03
2015-03-31
2015-05-01
2015-05-31
2015-07-01
Jan. 30th
2015-01-30
2015-03-02
2015-03-30
2015-04-30
2015-05-30
2015-06-30
The above has some issues. February is skipped, which could be a problem both supply-end (say if there is a monthly bandwidth allocation and Feb goes to waste and March gets doubled up on) and demand-end (users feel cheated out of Feb and perceive the extra March as attempt to correct mistake). On the other hand, notice that the two date sets:
never overlap
are always on the same date when that month has the date (so the Jan. 30 set looks pretty clean)
are all within 3 days (1 day in most cases) of what might be considered the "correct" date.
are all at least 28 days (a lunar month) from their successor and predecessor, so very evenly distributed.
Given the last two sets, it would not be difficult to simply roll back one of the dates if it falls outside of the actual following month (so roll back to Feb 28th and April 30th in the first set) and not lose any sleep over the occasional overlap and divergence from the "last day of month" vs "second to last day of month" pattern. But expecting the library to choose between "most pretty/natural", "mathematical interpretation of 02/31 and other month overflows", and "relative to first of month or last month" is always going to end with someone's expectations not being met and some schedule needing to adjust the "wrong" date to avoid the real-world problem that the "wrong" interpretation introduces.
So again, while I also would expect +1 month to return a date that actually is in the following month, it is not as simple as intuition and given the choices, going with math over the expectations of web developers is probably the safe choice.
Here's an alternative solution that is still as clunky as any but I think has nice results:
foreach(range(0,5) as $count) {
$new_date = clone $date;
$new_date->modify("+$count month");
$expected_month = $count + 1;
$actual_month = $new_date->format("m");
if($expected_month != $actual_month) {
$new_date = clone $date;
$new_date->modify("+". ($count - 1) . " month");
$new_date->modify("+4 weeks");
}
echo "* " . nl2br($new_date->format("Y-m-d") . PHP_EOL);
}
It's not optimal but the underlying logic is : If adding 1 month results in a date other than the expected next month, scrap that date and add 4 weeks instead. Here are the results with the two test dates:
Jan. 31st
2015-01-31
2015-02-28
2015-03-31
2015-04-28
2015-05-31
2015-06-28
Jan. 30th
2015-01-30
2015-02-27
2015-03-30
2015-04-30
2015-05-30
2015-06-30
(My code is a mess and wouldn't work in a multi-year scenario. I welcome anyone to rewrite the solution with more elegant code so long as the underlying premise is kept intact, i.e. if +1 month returns a funky date, use +4 weeks instead.)
In conjunction with shamittomar's answer, it could then be this for adding months "safely":
/**
* Adds months without jumping over last days of months
*
* #param \DateTime $date
* #param int $monthsToAdd
* #return \DateTime
*/
public function addMonths($date, $monthsToAdd) {
$tmpDate = clone $date;
$tmpDate->modify('first day of +'.(int) $monthsToAdd.' month');
if($date->format('j') > $tmpDate->format('t')) {
$daysToAdd = $tmpDate->format('t') - 1;
}else{
$daysToAdd = $date->format('j') - 1;
}
$tmpDate->modify('+ '. $daysToAdd .' days');
return $tmpDate;
}
I made a function that returns a DateInterval to make sure that adding a month shows the next month, and removes the days into the after that.
$time = new DateTime('2014-01-31');
echo $time->format('d-m-Y H:i') . '<br/>';
$time->add( add_months(1, $time));
echo $time->format('d-m-Y H:i') . '<br/>';
function add_months( $months, \DateTime $object ) {
$next = new DateTime($object->format('d-m-Y H:i:s'));
$next->modify('last day of +'.$months.' month');
if( $object->format('d') > $next->format('d') ) {
return $object->diff($next);
} else {
return new DateInterval('P'.$months.'M');
}
}
This is an improved version of Kasihasi's answer in a related question. This will correctly add or subtract an arbitrary number of months to a date.
public static function addMonths($monthToAdd, $date) {
$d1 = new DateTime($date);
$year = $d1->format('Y');
$month = $d1->format('n');
$day = $d1->format('d');
if ($monthToAdd > 0) {
$year += floor($monthToAdd/12);
} else {
$year += ceil($monthToAdd/12);
}
$monthToAdd = $monthToAdd%12;
$month += $monthToAdd;
if($month > 12) {
$year ++;
$month -= 12;
} elseif ($month < 1 ) {
$year --;
$month += 12;
}
if(!checkdate($month, $day, $year)) {
$d2 = DateTime::createFromFormat('Y-n-j', $year.'-'.$month.'-1');
$d2->modify('last day of');
}else {
$d2 = DateTime::createFromFormat('Y-n-d', $year.'-'.$month.'-'.$day);
}
return $d2->format('Y-m-d');
}
For example:
addMonths(-25, '2017-03-31')
will output:
'2015-02-28'
I found a shorter way around it using the following code:
$datetime = new DateTime("2014-01-31");
$month = $datetime->format('n'); //without zeroes
$day = $datetime->format('j'); //without zeroes
if($day == 31){
$datetime->modify('last day of next month');
}else if($day == 29 || $day == 30){
if($month == 1){
$datetime->modify('last day of next month');
}else{
$datetime->modify('+1 month');
}
}else{
$datetime->modify('+1 month');
}
echo $datetime->format('Y-m-d H:i:s');
Here is an implementation of an improved version of Juhana's answer in a related question:
<?php
function sameDateNextMonth(DateTime $createdDate, DateTime $currentDate) {
$addMon = clone $currentDate;
$addMon->add(new DateInterval("P1M"));
$nextMon = clone $currentDate;
$nextMon->modify("last day of next month");
if ($addMon->format("n") == $nextMon->format("n")) {
$recurDay = $createdDate->format("j");
$daysInMon = $addMon->format("t");
$currentDay = $currentDate->format("j");
if ($recurDay > $currentDay && $recurDay <= $daysInMon) {
$addMon->setDate($addMon->format("Y"), $addMon->format("n"), $recurDay);
}
return $addMon;
} else {
return $nextMon;
}
}
This version takes $createdDate under the presumption that you are dealing with a recurring monthly period, such as a subscription, that started on a specific date, such as the 31st. It always takes $createdDate so late "recurs on" dates won't shift to lower values as they are pushed forward thru lesser-valued months (e.g., so all 29th, 30th or 31st recur dates won't eventually get stuck on the 28th after passing thru a non-leap-year February).
Here is some driver code to test the algorithm:
$createdDate = new DateTime("2015-03-31");
echo "created date = " . $createdDate->format("Y-m-d") . PHP_EOL;
$next = sameDateNextMonth($createdDate, $createdDate);
echo " next date = " . $next->format("Y-m-d") . PHP_EOL;
foreach(range(1, 12) as $i) {
$next = sameDateNextMonth($createdDate, $next);
echo " next date = " . $next->format("Y-m-d") . PHP_EOL;
}
Which outputs:
created date = 2015-03-31
next date = 2015-04-30
next date = 2015-05-31
next date = 2015-06-30
next date = 2015-07-31
next date = 2015-08-31
next date = 2015-09-30
next date = 2015-10-31
next date = 2015-11-30
next date = 2015-12-31
next date = 2016-01-31
next date = 2016-02-29
next date = 2016-03-31
next date = 2016-04-30
$ds = new DateTime();
$ds->modify('+1 month');
$ds->modify('first day of this month');
If you just want to avoid skipping a month you can perform something like this to get the date out and run a loop on the next month reducing the date by one and rechecking until a valid date where $starting_calculated is a valid string for strtotime (i.e. mysql datetime or "now"). This finds the very end of the month at 1 minute to midnight instead of skipping the month.
$start_dt = $starting_calculated;
$next_month = date("m",strtotime("+1 month",strtotime($start_dt)));
$next_month_year = date("Y",strtotime("+1 month",strtotime($start_dt)));
$date_of_month = date("d",$starting_calculated);
if($date_of_month>28){
$check_date = false;
while(!$check_date){
$check_date = checkdate($next_month,$date_of_month,$next_month_year);
$date_of_month--;
}
$date_of_month++;
$next_d = $date_of_month;
}else{
$next_d = "d";
}
$end_dt = date("Y-m-$next_d 23:59:59",strtotime("+1 month"));
Extension for DateTime class which solves problem of adding or subtracting months
https://gist.github.com/66Ton99/60571ee49bf1906aaa1c
If using strtotime() just use $date = strtotime('first day of +1 month');
I needed to get a date for 'this month last year' and it becomes unpleasant quite quickly when this month is February in a leap year. However, I believe this works... :-/ The trick seems to be to base your change on the 1st day of the month.
$this_month_last_year_end = new \DateTime();
$this_month_last_year_end->modify('first day of this month');
$this_month_last_year_end->modify('-1 year');
$this_month_last_year_end->modify('last day of this month');
$this_month_last_year_end->setTime(23, 59, 59);
$month = 1; $year = 2017;
echo date('n', mktime(0, 0, 0, $month + 2, -1, $year));
will output 2 (february). will work for other months too.
$current_date = new DateTime('now');
$after_3_months = $current_date->add(\DateInterval::createFromDateString('+3 months'));
For days:
$after_3_days = $current_date->add(\DateInterval::createFromDateString('+3 days'));
Important:
The method add() of DateTime class modify the object value so after calling add() on a DateTime Object it returns the new date object and also it modify the object it self.
you can actually do it with just date() and strtotime() as well. For example to add 1 month to todays date:
date("Y-m-d",strtotime("+1 month",time()));
if you are wanting to use the datetime class thats fine too but this is just as easy. more details here
$date = date('Y-m-d', strtotime("+1 month"));
echo $date;