How to Detect Ambiguous and Invalid Datetime in PHP

How to detect Ambiguous and Invalid DateTime in PHP?

I 'm not aware of any existing implementations and I haven't had cause to use advanced date/time features such as these as yet, so here is a clean room implementation.

To enable the syntax illustrated in the question we are going to extend DateTimeZone as follows:

class DateTimeZoneEx extends DateTimeZone
{
const MAX_DST_SHIFT = 7200; // let's be generous

// DateTime instead of DateTimeInterface for PHP < 5.5
public function isValidTime(DateTimeInterface $date);

public function isAmbiguousTime(DateTimeInterface $date);
}

To keep distracting details from cluttering the implementation, I am going to assume that the $date arguments have been created with the proper time zone; this is in contrast to the example code given in the question.

That is to say, the correct result will not be produced by this:

$tz = new DateTimeZoneEx('America/New_York');
echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00'));

but instead by this:

$tz = new DateTimeZoneEx('America/New_York');
echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00', $tz));

Of course since $tz is already known to the object as $this, it should be easy to extend the methods so that this requirement is removed. In any case, making the interface super user friendly is out of the scope of this answer; going forward I will focus on the technical details.

isValidTime

The idea here is to use getTransitions to see if there are any transitions around the date/time we are interested in. getTransitions will return an array with either one or two elements; the timezone situation for the "begin" timestamp will always be there, and another element will exist if a transition occurs shortly after it. The value of MAX_DST_SHIFT is small enough that there is no chance of getting a second transition/third element.

Let's see the code:

public function isValidTime(DateTime $date)
{
$ts = $date->getTimestamp();
$transitions = $this->getTransitions(
$ts - self::MAX_DST_SHIFT,
$ts + self::MAX_DST_SHIFT
);

if (count($transitions) == 1) {
// No DST changes around here, so obviously $date is valid
return true;
}

$shift = $transitions[1]['offset'] - $transitions[0]['offset'];

if ($shift < 0) {
// The clock moved backward, so obviously $date is valid
// (although it might be ambiguous)
return true;
}

$compare = new DateTime($date->format('Y-m-d H:i:s'), $this);

return $compare->modify("$shift seconds")->getTimestamp() != $ts;
}

The final point of the code depends on the fact that PHP's date functions calculate timestamps for invalid date/times as if wall clock time had not shifted. That is, the timestamps calculated for 2013-03-10 02:30:00 and 2013-03-10 03:30:00 will be identical on the New York timezone.

It's not difficult to see how to take advantage of this fact: create a new DateTime instance equal to the input $date, then shift it forward in wall clock time terms an amount equal to the DST shift in seconds (it is imperative that DST not be taken into account to make this adjustment). If the timestamp of the result (here the DST rules come into play) is equal to the timestamp of the input, then the input is an invalid date/time.

isAmbiguousTime

The implementation is quite similar to isValidTime, only a few details change:

public function isAmbiguousTime(DateTime $date)
{
$ts = $date->getTimestamp();
$transitions = $this->getTransitions(
$ts - self::MAX_DST_SHIFT,
$ts + self::MAX_DST_SHIFT);

if (count($transitions) == 1) {
return false;
}

$shift = $transitions[1]['offset'] - $transitions[0]['offset'];

if ($shift > 0) {
// The clock moved forward, so obviously $date is not ambiguous
// (although it might be invalid)
return false;
}

$shift = -$shift;
$compare = new DateTime($date->format('Y-m-d H:i:s'), $this);
return $compare->modify("$shift seconds")->getTimestamp() - $ts > $shift;
}

The final point depends on another implementation detail of PHP's date functions: when asked to produce the timestamp for an ambiguous date/time, PHP produces the timestamp of the first (in absolute time terms) occurrence. This means that the timestamps of the latest ambiguous time and the earliest non-ambiguous time for a given DST change will differ by an amount larger than the DST offset (specifically, the difference will be in the range [offset + 1, 2 * offset], where offset is an absolute value).

The implementation takes advantage of this by again doing a "wall clock shift" forward and checking the timestamp difference between the result and the input $date.

See the code in action.

PHP DateTime::format('I') inaccuracy bug?

While creating a method to isolate the timestamp when DST switches from on to off and vice versa ...

That method already exists as DateTimeZone::GetTransitions. Documentation here.

You may also be interested in the related problem of determining whether a particular date/time is within a transition period, which is described in this answer.

Parsing Ambiguous Dates (Language Independent)

Unless you know exactly what the language/culture the format is coming from, you need to establish a common date format.

There is something called locale-neutral date format that I would recommend. (YYYY-MM-DD)

It's either use that or be clear as to what part is the year, month and day. (DD MON YYYY or 22 Apr 2003)

See: the w3's view on date formatting.

Edit: mistyped the locale-neutral date format

PHP Daylight savings conundrum

Have you tried looking at DateTimeZone::getTransitions() ?

http://www.php.net/manual/en/datetimezone.gettransitions.php

In particular use the [offset] and [isdst] properties.

  • When they save the time, find the first transition before the current date that is NOT DST. (Typically one of the two values in the past year). Convert using the offset of the non-DST period
  • When retrieving the value and you are currently in a DST period use the offset of a non-DST period to translate the time, not the current offset.

Taking your EST example, in August even though you are in EDT, you save values using the EST conversion of -5.

When pulling the value back out if they view that value in January you add 5, and if you are in August you add 4.

This will work for 95% of cases I'm assuming that the switches are consistent. If Eastern decided to merge with Central, you could have transitions that run –5/–4/–5/–4/–5/–5/–6/–5/–6/–6 and that would mess things up.

There's no magic bullet for this one. I don't know the details of your app structure, you may just have to try adding 3 hours to the midnight of whatever day you are on so that any recurring daily appointment is stored as a time only.

How do I force strtotime() to return date instead of time when given ambiguous $time?

Is modifying the input an option for you?

$str = '10.01.11';
$str = str_replace('.', '/', $str);

echo date('r', strtotime($str));

However, this will still output Sat, 01 Oct 2011 00:00:00, according to the MM.DD.YY pattern (US standard).


EDIT: Depending on you usage, you might consider creating a list of regex patterns and parse the date accordingly. It is very hard to make a code like this to be open to all possibilities.

Why can't DateTime parse dd.mm.yy always correct?

As everyone suggested, try using DateTime::createFromFormat(). Please check below code:

<?php

$date1 = DateTime::createFromFormat('d.m.y','10.10.04');
$date2 = DateTime::createFromFormat('d.m.y','25.03.07');

echo $date1->format('Y-m-d'); // result: 2004-10-10

echo $date2->format('Y-m-d'); // result: 2007-03-25
?>

Output: https://eval.in/518801



Related Topics



Leave a reply



Submit