PHP's DateTime class has the two methods add() and sub() to add or subtract a time span from a DateTime object. This looks very familiar to me, in .NET I can do the same thing. But once I tried it, it did very strange things. I found out that in PHP, those methods will modify the object itself. This is not documented and the return value type indicates otherwise. Okay, so in some scenarios I need three lines of code instead of one, plus an additional local variable. But then PHP5's copy by reference model comes into play, too. Just copying the object isn't enough, you need to explicitly clone it to not accidently modify the original instance still. So here's the C# code:
DateTime today = DateTime.Today;
...
if (date < today.Add(TimeSpan.FromDays(7)) ...
Here's the PHP equivalent:
$today = new DateTime('today');
...
$then = clone $today;
$then->add(new DateInterval('P7D'));
if ($date < $then) ...
(I keep a copy of today to have the same time for all calculations during the method runtime. I've seen it often enough that seconds or larger time units change in that time...)
Is this it in PHP? I need to write a wrapper class for that if there's no better solution! Or I'll just stay with the good ol' time() function and just use DateTime for easier parsing of the ISO date I get from the database.
$today = time() % 86400;
...
if ($date < $today + 7 * 86400) ...
Time zones not regarded in these examples.
Update: I just found out that I can use the strtotime() function as well for parsing such dates. So what's the use for a DateTime class in PHP after all if it's so complicated to use?
Update^2: I noticed that my $today = ... thing above is garbage. Well, please just imagine some correct way of getting 'today' there instead...
It looks like you will definitely have to make a clone of the object. There does not seem to be a way to create a copy of the DateTime object any other way.
As far as I can see, you could save one line:
$today = new DateTime('today');
...
$then = clone $today;
if ($then->add(new DateInterval('P7D')) < $then) ...
I agree this isn't perfect. But do stick with DateTime nevertheless - write a helper class if need be. It is way superior to the old date functions: It doesn't have the year 2038 bug, and it makes dealing with time zones much, much easiert.
Related
So it seems like the PHP DateTime class allows you to use simple relative language strings such a next Monday 12:00 to create objects associated with relative dates.
The Problem
The use case I'd like to achieve is to omit the day ("Monday") from the above string, and create a DateTime object for the next instance of 06:00.
This presents a challenge because the code may be running at 07:00, in which case the string 06:00 will return a date which is in the past, whereas if we run the code at 05:00 (or anytime before 06:00), we will get the correct result.
Note: As an addendum, I don't want to have to do a bunch of complicated arithmetic and want to leverage the DateTime class to its fullest to keep myself from any sneaky little TimeZone or leap year issues.
My Question
How can I use the PHP DateTime class to obtain a DateTime object for the next instance of a certain time (without supplying the day as well)?
I created a PHP extension for the DateTime API called dt. You can find it here. Using it for this task is very simple. Some Examples:
$dt = dt::create("today 06:00");
if($dt->isPast()) {
$dt->modify("+ 1Day");
}
echo $dt;
Optionally, you can always set a time zone as the second parameter.
$time = "06:00";
$dt = dt::create("today $time","Africa/Johannesburg");
//:
The class works with special chains which can contain conditions. This allows you to create an expression that does the job with the create method alone.
$dt = dt::create("today 6:00|{{?YmdHis<NOW}}+1 Day");
The class also works with cron expressions.
$cron = "0 6 * * 1-5";//every Mo-Fr at 6:00
$dt = dt::create('now')->nextCron($cron);
Always delivers the next time 6:00 for Monday to Friday. At the weekend, the next time is Monday 6:00.
The Solution
Here's how I ended up solving this...
/*
* Simple function that returns a DateTime object representing the next instance of the time
* supplied by the $timeString parameter.
*
* $timeString: A string representing the time you wish to process, in 24h notation (i.e.
* '06:00' for 6:00am).
*/
function getNextTimeInstance($timeString){
// First, grab an instance of the current moment as a DateTime object ('now' is optional).
$rightNow = new DateTime('now');
// Basic initializations for the object we want to use.
$processedTime = new DateTime();
// Change the Timezone if you need to do so.
$processedTime->setTimezone(new DateTimeZone('Africa/Johannesburg'));
// Set the time for our working DateTime to the time we want (i.e. '06:00').
$processedTime->modify($timeString);
// If the next instance of this time we're trying to process is in the past
// (meaning it is "less than" the current moment).
if($rightNow > $processedTime){
// Simply add one day to our working DateTime object.
$processedTime->modify('+1 day');
}
// Return the DateTime objects that represents the next instance of that time.
return $processedTime;
}
I relied heavily on the following documentation to reach this result:
The PHP DateTime class.
The list of date formats that the PHP DateTime class will accept as inputs.
The list of supported TimeZone strings that PHP will accept.
I hope this helps anyone else stuck in the same situation as myself, and if anyone else has a more elegant solution, please feel free to contribute.
I made a function to convert datestrings to other timezones, but my function creates new DateTime and DateTimeZone objects every time it's called. This is bad practice so I looked into how I could reuse the objects.
DateTime has a method called setDate, but it requires a year, month and day as arguments instead of a datestring. DateTimeZone has no such method.
Is there a way I can reuse my DateTime and DateTimeZone objects?
Edit: This function will be called anywhere from 4 to hundreds of times in a single POST, depending on how many dates are being returned from the server.
/**
* Converts a datestring to another timezone
*
* #arg $ds (String) - The datestring to be converted
* #arg $tz (String) - The timezone to convert the datestring to
*
* #return (String) - The converted datetime
*/
function convertDateTime($ds, $tz) {
$fooDateTime = new DateTime($ds);
$fooTimeZone = new DateTimeZone($tz);
$newDate = $fooDateTime->setTimezone($fooTimeZone);
return $newDate->format('Y-m-d H:i:s');
}
Compared to the work that is going on in the DateTime and DateTimeZone constructors, the overhead of initialising a new object is miniscule. What is really happening when you call those functions?
The input is being parsed, validated, deconstructed, and in case of DateTimeZone timezone information is being loaded from somewhere.
That data is being stored somewhere (in object properties, but might as well be in variables, an array or whatever else).
You get a handle back to the result of the work which just happened (your new object).
Since every date is unique, this work needs to happen anyway for each date. Whether this is dressed up as an object or as a regular function call which returns an array or something is irrelevant and makes little difference.
Arguably you could memoize the DateTimeZone objects, since identical timezones are identical and immutable anyway. Something like:
static $timezones = [];
if (!isset($timezones[$tz])) {
$timezones[$tz] = new DateTimeZone($tz);
}
But really, for all you know PHP is already optimising this behind the scenes. It probably won't make any appreciable difference in practice.
What you should certainly not do is mutate objects and set entirely new values on them. If you read the list of things that happen above again, it should make sense why; because you don't know what work exactly PHP did there, and you may create inconsistent objects which screw with your business logic due to stupid mistakes.
First of all, confirm it IS a problem. Benchmark your app with current code, then change convertDateTime to return a hardcoded string:
function convertDateTime($ds, $tz) {
return '2016-02-03 15:54:00';
}
and measure the difference.
If there is any, you can probably memoize responses, but it will increase memory consumption, which IMHO is more valuable.
One more thing to keep in mind. One said:
Premature optimization is the root of all evil.
In PHP it is extremely true, as the language itself is far from optimal.
I don't see any bad practice creating multiple DateObject instances, but if you prefer a unique instance, you can extend the DateTime class:
class MyDateTime extends DateTime
{
public function convertDateTime( $ds, $tz )
{
$fooTimeZone = new DateTimeZone( $tz );
$newDate = $this->setTimezone( $fooTimeZone );
return $newDate->format( 'Y-m-d H:i:s' );
}
}
It remains the problem of DateTimeZone class...
Please note that this is only an example to partially answer to your question ( «How can I reuse DateTime» ) without getting into the arguments.
It's easy also add more code to reuse DateTimeZone, i.e. setting an array and then populating it when new TimeZone is called. (Edit: see example in deceze answer)
How can I add a number of days to a DateTime object without modifying the original. Every question on StackOverflow appears to be about date and not DateTime, and the ones that do mention DateTime talk about modifying the original.
Eg.
$date = new DateTime('2014-12-31');
$date->modify('+1 day');
But how can you calculate a date several days in advance without modifying the original, so you can write something like:
if($dateTimeNow > ($startDate + $daysOpen days) {
//
}
I could always just create another DateTime object, but I'd rather do it the above way.
Use DateTimeImmutable, it's the same as DateTime except it never modifies itself but returns a new object instead.
http://php.net/manual/en/class.datetimeimmutable.php
you can take the original variable in a separate variable and add no. of days in other variable so you have both(original and updated)value in different variable.
$startDate = new DateTime('2014-12-31');
$endDate = clone $startDate;
$endDate->modify('+'.$days.'days');
echo $endDate->format('Y-m-d H:i:s');
You can always use clone, too:
$datetime = clone $datetime_original;
Is it possible in PHP to change the DateTime() to a custom time difference. Like adding it 20 min and everytime I call new DateTime() I would get those extra 20 min to the current date.
I thought about __construct() but didn't see how to fix it.
I can use a function of course, but just curious to know if its possible to change the prototype new DateTime()
Yes, it's possible:
$date = new DateTime();
$date->modify("+20 minutes"); //or whatever value you want
Example online
Ilia Rostovtsev's answer is a good one for achieving your goal. Alternatively you can also use the DateInterval class with DateTime::add(). Personally, I prefer that one because you don't need to know how strings need to look like while DateInterval uses a standard like P1d for a day.
I wouldn't use inheritance to get a DateTime class including your wished behaviour. Instead you can create some kind of factory which contains Ilia's code (and every other code that is part for the object creation). The interval can be added as parameter. Your added interval, your 20 minutes should be stored in a constant in case you need to change that interval in the future while technically being able to use other intervals than 20 minutes.
You can do this all in one line:-
$date = (new \DateTime())->add(new \DateInterval('PT20M'));
That would be my preferred way of doing it.
See it working in PHP >= 5.3
I have this code:
<?php
$start = new Zend_Date("2011-09-06T10:00:00+02:00",Zend_Date::ISO_8601);
$end = new Zend_Date("2011-09-06T10:01:00+02:00",Zend_Date::ISO_8601);
echo $end->sub($start);
?>
In short: I create two dates, with a minute's difference between them. Then I print out the difference (subtraction) between them.
The result, however, is:
01-01-1970 02:01:00
Basically, what I understand from this behaviour is that Zend_Date operates on dates without taking timezone into consideration, and then puts the timezone back in the result. Of course, this means that the subtraction result is off by the value of the timezone (+2h in my case).
What's the best way to get around this?
Yes, when echo'ing a Zend_Date, it will take your timezone into account. To get the difference formatted for GMT dates, you have to set the timezone explicitly:
$start = new Zend_Date("2011-09-06T10:00:00+02:00",Zend_Date::ISO_8601);
$end = new Zend_Date("2011-09-06T10:01:00+02:00",Zend_Date::ISO_8601);
echo $end->sub($start)->setTimezone('GMT')->get(Zend_Date::ISO_8601);
This would output: 1970-01-01T00:01:00+00:00
On a sidenote, if you do not need the dynamic localization features of Zend_Date it's best to avoid it in favor or PHP's native DateTime API. There is really no reason to use Zend_Date just because it exists in ZF. PHP's own DateTime API is faster and easier to use. Getting the time difference with the DateTime API would be
$start = new DateTime("2011-09-06T10:00:00+02:00");
$end = new DateTime("2011-09-06T10:01:00+02:00");
echo $start->diff($end)->format('%H:%I:%S');
which would output 00:01:00