Find dates between 2 dates with custom interval and without overflow - php

I have a customisable DateInterval object, along with a customisable start and end date. I want to find the dates between the start and end using the interval. I am using Carbon to try and help with this.
Here's the problem:
I the interval is 𝑥 months but the start date is > 28 I cannot control the overflow using CarbonPeriod.
Here is the code I am testing with:
$di = CarbonInterval::create('P1M');
$start = Carbon::parse('31 january 2020')->startOfDay();
$end = Carbon::parse('01 april 2020')->startOfDay();
$period = CarbonPeriod::create($start, $di, $end);
$items = [];
foreach ($period as $item) {
$items[] = $item;
}
I want the above to result in
2020-01-31
2020-02-29
2020-03-30
But I get
2020-01-31
2020-03-02
2020-04-02
Remember, the DateInterval is customisable (or I would just use Carbon::addMonthNoOverflow()).
Can anyone please help with how I achieve what I need to, above?

tl;dr: It doesn't work with Carbon.
Date arithmetics are hard to convey and harder to get right. So for example, you couldn't even express, that you want "the last day of a month" in an interval, when the start date is for example in february:
start: 2020-02-29
P1M -> 2020-03-29 (not end of month, obviously)
An interval alone can't express the semantics that you want the end of month.
And this problem will carry over even if you would find a way to get the overflow working such that you don't end up on the start of the next month. (I have tried some approaches that all failed)
All you can offer (to the user) is to apply extra functions to the array of dates to achieve your goal perhaps, like $item->endOfMonth(). But you would still have to pay attention that the start's day of month is <= 28.
Ironically, you can call ->settings(['monthOverflow'=>false, 'yearOverflow'=>false]) on all Carbon, CarbonInterval and CarbonPeriod, and it has no effect (except when you call addMonth() on it, which is rather disappointing, it won't be applied on $start->add($di)). This comes down to the fact that Carbon is ultimately just a wrapper around the standard DateTime objects, which don't support overflow either.
Long story short, there is no elegant and/or easy solution with Carbon (current version). ;o/

After much digging, and realising that #Jakumi is correct, I have just come up with a solution using Carbon after asking the same question here.
The reason why DatePeriod() does not work is that it is always reliant on the previous date in the loop. That is why you get stuck at 29/28 for february and then repeating throughout the rest of the loop.
Here is my solution:
$endDate = CarbonImmutable::parse('10 april 2020')->startOfDay();
$startDate = CarbonImmutable::parse('31 january 2020')->startOfDay();
$interval = CarbonInterval::create('P1M'); // only works for number of months, not composite intervals
$workingDate = $startDate->copy();
for ($i = 1; $workingDate <= $endDate; $i = $i + $interval->m) {
echo = $workingDate->format('Y-m-d') . "\n";
$workingDate = $startDate->addMonthsNoOverflow($i);
}
I am involved in a bit of a code-off with a contributor of the Carbon codebase. If he finds a better solution then I will update my answer. For now, this works.

Related

Find next occurance of a given time after given timestamp

Given an arbitrary timestamp (e.g. 2019-02-26 10:30:00) I would like to find the next occurrence of an arbitrary time.
For example, the next occurrence of 12:00:00 will be 2019-02-26 12:00:00 but the next occurrence of 09:00:00 will be the next day at 2019-02-27 09:00:00. The results could be Carbon or Datetime objects. The test time will just be a string as shown.
Is there a way to calculate this in native PHP or PHP Carbon without conditionally boxing in time periods. An obvious way would be to see if the time being tested is past the check time for today, and if it is, taking the result as the check time plus 24 hours (the next day). That feels to me like too much chopping and joining of dates and times, so is there a way to calculate it by considering time to be a simple linear line?
All times will be in a single timezone, with DST. Note: the arbitrary datetimes and check times will stay clear of DST changeovers i.e. 01:00 to 02:00 so hopefully they will not be an issue to take into account.
Short answer is no for PHP (partial answer, I'm no specialist of Carbon but from quick look it's also no, but you can create a macro from following code).
However, with a ternary condition the one-liner is simple enough IMHO (replace the second DateTime($str) with DateTime() if you want to compare with current date and time, and change the >= by > if you want next day when time compared is exactly the same):
$str = '2019-02-26 10:30:00';
$date1 = ( ($a = (new DateTime($str))->setTime(12,00)) >= (new DateTime($str)) ) ? $a : $a->modify('+1 day');
$date2 = ( ($a = (new DateTime($str))->setTime(9,00)) >= (new DateTime($str)) ) ? $a : $a->modify('+1 day');
echo $date1->format('Y-m-d H:i:s'); //2019-02-26 12:00:00
echo $date2->format('Y-m-d H:i:s'); //2019-02-27 09:00:00
quick note: what you gave us is not a timestamp, but a formatted date.
Here is what I am using now through Carbon, which appears to give me the correct results:
$dateTime = Carbon::parse('2019-03-30 17:34:50', 'Europe/London');
$testTime = '16:00:00';
list ($hour, $minute, $second) = explode(':', $testTime);
$nextTimeOccurrence = $dateTime
->copy() // Carbon 1 only
->hour($hour)->minute($minute)->second($second);
if ($dateTime->gt($nextTimeOccurrence)) {
$nextTimeOccurrence = $nextTimeOccurrence->addDay();
}
// $nextTimeOccurrence is the next occurrence of $testTime after $dateTime
The splitting of the time seems clumsy, but might be the best way? The approach is:
Create a timestamp with the test time on the same day as the timestamp I'm checking. This will be the timestamp I am looking for.
If the timestamp I'm checking is after the timestamp created in the previous step, then add a day to it.
I've tested this around DST, and happily Carbon/Datetime keeps the same time when adding a day over a DST period, where a day there would be 25 hours or 23 hours, depending on which way it goes.
I still think there is a more "linear time" way to do this, but this seems simple and robust. Thanks go to #michael-stokoe here at the office for my lead on this.

PHP - Parsing String As Past Date

Currently, I'm trying to parse out dates when messages were received into timestamps. I have the month and day but the year is not specified. The event always occurs at the most recent (human) reading of the time. It works great in most cases to do this:
$time = strtotime("Jan 2 8:38pm");
That returns a date for this year, which is correct. Unfortunately, I get problems when I try to do for example:
$time = strtotime("Dec 31 8:38pm");
That returns a date which hasn't happened yet, and wont happen for the whole rest of the year. Obviously, my message was not sent in the future. I need it to return December 31st of last year.
For weekdays, I had a solution by prepending 'last' before the weekday like so:
$time = strtotime("Last Saturday 8:38pm");
That always returned the time of the last Saturday. However, trying to do the same thing here doesn't work:
$time = strtotime("Last Dec 31 8:38pm");
This returns false. I know to decrement a date by 1 year, I can do this:
$time = strtotime("Dec 31 8:38pm -1 year");
And that works great for Dec 31. However, Jan 2 will now fail:
$time = strtotime("Jan 2 8:38pm -1 year");
One solution I thought of was to subtract off a year (86400 * 365) from the resulting value if it is past today's date. However, this result will fail if we passed over February of a leap year. In that case, we would end up with a time that was ahead by a day.
The best solution I came up with so far is this:
$time = strtotime($raw_time);
if ($time > time()) {
$time = strtotime($raw_time." -1 year");
}
It seems kind of wasteful to make two calls to strtotime which I know is probably not a very efficient function. Is this the most elegant solution?
Is anyone aware of an option in strtotime which forces the dates to be in the past instead of in the future?
Is there another way to parse these dates that I should consider?
Efficiency is important for this because I am going to be parsing a lot of dates with it, but I would also like simple and readable code so I can understand it later.
Your approach is fine, as there is no date format to get what you want. Another approach could be using the DateTime class:
$datetime = new DateTime($raw_time);
if ($datetime > new DateTime()) {
$datetime->modify('-1 year');
}
You could test which one of the two approaches is faster. My guess is that this is a micro-optimization that won't make a lot of difference.

Understanding date processing with strtotime

I'm trying to get my head round someone else's code which they've written for handling the dates of when news stories are published. The problem has come up because they are using this line -
$date = strtotime("midnight", strtotime($dateString));
to process a date selected using a jquery calendar widget. This works fine for future dates, but when you try to use a date which is in the previous calendar year, it uses the current year instead. I think this is due to "midnight" finding the closest instance of the selected day and month.
I could remove the "midnight", but I'm not sure what the repercussions of this would be - is there a reason that the midnight could be there?
EDIT: this is the full block of code which handles the date. The date contains the time, which allows the user to publish an item at a specific time.
$array['display_date'] = '24 October, 2011 17:30';
$string = $array['display_date'];
$dateString = substr($string, 0, -5);
$timeArray = explode(':', substr($string, -5));
$hours_in_secs = 60 * 60 * $timeArray[0];
$mins_in_secs = $timeArray[1];
$date = strtotime("midnight", strtotime($dateString));
$timestamp = $date + $hours_in_secs + $mins_in_secs;
//assign timestamp to validation array
$array['display_date'] = $timestamp;
echo $array['display_date']; // Output = 1351094430 (Oct 24 2012 17:00:30)
This really depends on what $dateString contains. Assuming your jQuery widget delivered the time portion as well, your colleague likely wanted to remove the time portion. Compare the following:
echo date(DATE_ATOM, strtotime('2010-10-01 17:32:00'));
// 2010-10-01T17:32:00+02:00
echo date(DATE_ATOM, strtotime("midnight", strtotime('2010-10-01 17:32:00')));
// 2010-10-01T00:00:00+02:00
If your widget doesnt return the time portion, I dont see any reason for setting the date to midnight, because it will be midnight automatically:
echo date(DATE_ATOM, strtotime('2010-10-01'));
// 2010-10-01T00:00:00+02:00
Note that all these are dates in the past and they will result in the given year in the past, not the current year like you say. If they do in your code, the cause must be somewhere else.
Will there be repercussions when you change the code? We cannot know. This is just one line of code and we have no idea of any context. Your unit-tests should tell you when something breaks when you change code.
EDIT after update
The codeblock you show makes no sense whatsoever. Ask the guy who wrote it what it is supposed to do. Not only will it falsely return the current year for past years, but it will also give incorrect results for the minutes, e.g.
24 March, 2010 17:30 will be 2012-03-24T17:00:30+01:00
I assume this was an attempt at turning 24 March, 2010 17:30 into a valid timestamp, which is in a format strtotime does not recognize. But the approach is broken. When you are on PHP5.3 use
$dt = DateTime::createFromFormat('d F, Y H:i', '24 March, 2010 17:30');
echo $dt->format(DATE_ATOM); // 2010-03-24T17:30:00+01:00
If you are not on 5.3 yet, go through https://stackoverflow.com/search?q=createFromFormat+php for alternate solutions. There is a couple in there.

Month by week of the year?

I'm trying to get the number of the month of the year by the number of a week of the year and the year.
So for example week 1 is in january and returns 1, week 6 is in february so I want 2.
I tried to go with date_parse_from_format('W/Y') but had no success (it's giving me errors).
Is there any way to go with date_parse_from_format() or is there another way?
print date("m",strtotime("2011-W6-1"));
(noting that in 2011, January has six weeks so week 6 (by some definitions) is in month 1).
Just wanted to add a note for the first answer, the week number should be 01-09 for Weeks 1 through 9 (it will always give month 1 if you don't add the leading zero)
date("m",strtotime("2011-W06-1"));
Using PHP DateTime objects (which is the preferred way of dealing with dates see links below for more info) you can accomplish it this way:
$dateTime = new \DateTime();
$dateTime->setISODate($year,$week);
$month = $dateTime->format('n');
Note that the following will not work as week "W" is not a supported format:
$month = \DateTime::createFromFormat("W/Y ", "1/2015")->format('n');
The format used by this method is the same supported by the function you where trying to use date_parse_from_format, hence the errors.
Why PHP DateTime Rocks
DateTime class vs. native PHP date-functions
strtotime notes
PHP/Architect's Guide to Date and Time Programming (Chapter 2)
Something like this will do, this is also tested and works:
function getMonthByNumber($number,$year)
{
return date("F",strtotime('+ '.$number.' weeks', mktime(0,0,0,1,1,$year,-1)));
}
echo getMonthByNumber(27,2011);
Hope this helps

Weeknumber print date out

Hallo all.
if i have week number 42 and i need to know what dates there are in this week, can sombardy maby tell me a easy way to get this info?
tanks a lot :)
<?php
$date = new DateTime('2009W52');
echo $date->format(DateTime::RFC850);
Which outputs
Monday, 21-Dec-09 00:00:00 EET
and you can modify it like
$date->modify("last monday");
$date->modify("next monday");
Week has to be zero padded for weeks 1-9
There is a thread discussing how to get the first date of the week (Monday) using the week of the year and the year. That should help you.
I know you have already accepted an answer, but I feel this bit of code is also useful.
$year = date("Y");
$week = date("W"); // Can be replaced with '42' for your example.
$start = strtotime($year.'W'.$week.'1');
This will return a unix timestamp which some people find easier to manipulate.
You can also use this for PHP 5.1 and higher.
$start = date(datetime::ISO8601, strtotime("2009W421"));
Using the above method you can easily format it.
Hope this provides useful to someone.
-Mathew

Categories