2012-03-14 12 views
11

DateTime :: Diff powinien obliczać prawidłowy interwał i uwzględniać czas letni (DST) i lata przestępne. Chociaż podobno tak nie jest. Kod horroru:PHP's DateTime :: Diff robi się źle?

$d1 = new DateTime("2011-10-30 01:05:00", new DateTimeZone("Europe/Stockholm")); 
$d2 = new DateTime("2011-10-30 03:05:00", new DateTimeZone("Europe/Stockholm")); 

echo $d1->getOffset()/(60 * 60); 

Drukuje "2"! Należy pamiętać, że czas UTC = 1h - 2h = 23:05:00 dzień wcześniej.

echo $d2->getOffset()/(60 * 60); 

Drukuje "1". DST się stało. Czas UTC = 3h - 1h = 02:05:00.

$di = $d1->diff($d2); 
echo "Hours of DateInterval: " . $di->h; 

Drukuje "2"! Źle?

$hoursofdiff = ($d2->getTimeStamp() - $d1->getTimeStamp())/60/60; 
echo "Calculated difference in hours: $hoursofdiff"; 

Drukuje "3"! Poprawny?

Gdy zegar obrócił się o 03:00:00 w danym dniu, wszyscy Szwedzi odwrócili swój zegar o godzinę do godziny 02:00:00. Oznacza to, że całkowita kwota przekazana między 01:05 a 03:05 wynosi trzy godziny, podobnie jak w przypadku ręcznego obliczenia, które pojawiło się podczas korzystania z programu UNIX TimeStamp. I podobnie jak obliczamy na naszych palcach, jeśli używamy analogowego zegara. Tym bardziej, że obliczając różnicę między dwoma znacznikami czasu UTC, korzystałem z własnej logiki PHP (!).

Czy to PHP czy mój mózg przestał działać poprawnie? Nagana z każdego z was, wszystkich bogów, którzy istnieją na tej stronie, sprawiłaby mi wielką radość!

Używam PHP 5.4 (VC9) na serwerze Apache. Niestety używam Windows 7 x64 jako OS. Przetestowałem swoją instalację przed wszelkimi roszczeniami błędów w klasach Date/Time PHP (jest kilka powiązanych z Windowsiem) i mogę potwierdzić, że mój system nie ma żadnego z nich. Poza powyższym kodem nie znalazłem żadnych innych błędów. Prawie zweryfikowałem cały kod i wyprowadziłem książkę "Przewodnik architekta PHP do programowania daty i czasu". Dlatego muszę stwierdzić, że to musi być mój mózg, który wiedźmo wyleczył, ale myślałem, że najpierw go tu zrobię.

+1

samo na 5.3.6 na OS X i 5.3.9 na Ubuntu – Phil

+0

także efekty 'DateTime :: sub()' i 'DateTime :: add() ' – Phil

Odpowiedz

9

Masz rację, PHP obecnie nie obsługuje przejścia DST ...

Raporty o błędach #51051 (ciągle otwarty) i #55253 (stałe w PHP 5.3.9) opisują problemy masz.

Jakiś czas temu Daniel Convissor napisał: an RFC trying to address the issue, ale dzienniki zmian nie sugerują, że zostało to rozwiązane. Miałem nadzieję, że zostanie to naprawione w 5.4, ale nie widzę żadnych dowodów na to.

Kiedy/jeśli jest zaimplementowane, wygląda na to, że musisz dodać "DST" lub "ST" do ciągu czasu.

Najlepszą praktyką jest wykonanie wszystkich obliczeń daty w UTC, co pozwala uniknąć tego problemu.

jest również bardzo pouczający.

+1

Dziękuję bardzo Jonathan, naprawdę to doceniam. W RFC, które łączyłeś, jest napisane, że "Rozwiązywanie tych problemów przed 5.4 [...] wydaje się być mądre". Ponieważ zrobiłem wszystkie moje testy używając 5.4, muszę stwierdzić, że to się nigdy nie wydarzyło. Wysłałem zarówno Daniela, jak i Dericka, i dążę do tego problemu w najpełniejszym zakresie. Jest to niezwykle ważne dla aplikacji, nad którą teraz pracuję, która poprawnie pobiera różnice czasowe. Pójdę naprzód i piszę własną klasę, która rozszerza DateTime i przesłania metodę diff, aby uzyskać dokładne wyniki. Będę przesyłać ponownie, gdy będę mieć nowe informacje! –

+0

Proszę, zbliżamy się do przedłużenia terminu DateTime, aby napisać mnóstwo hacków do modify(), między innymi. – taiganaut

+1

... i wracam ponownie rok później z powodu https://bugs.php.net/bug.php?id=61955 między innymi. Entropia PHP jest jednokierunkowa; to tylko się pogarsza. – taiganaut

1

Dobra, mam działającą klasę opakowania. Oblicza miniony czas rzeczywisty. Najpierw porównuje przesunięcia z UTC i dodaje lub odejmuje tę różnicę czasu do obiektu datetime-object przekazanego jako argument. Następnie nie musi robić nic więcej niż wywoływać parent :: diff. No dobrze, musiałem wprowadzić jednolinijkę do zhakowania czegoś, co może być kolejnym błędem w PHP (zobacz kod źródłowy poniżej). Metoda DateTimeDiff: diff oblicza PRAWDZIWY czas, jaki upłynął.Aby zrozumieć, co to oznacza, radzę ci przetestować tę klasę przy użyciu różnych dat i czasów, a żeby ułatwić sobie pracę, na samym dole tego komentarza zamieściłem dość prostą stronę HTML, którą napisałem. Ten link może być dobry punkt wyjścia, aby uzyskać kilka pomysłów na daty i godziny połączeń:

https://wiki.php.net/rfc/datetime_and_daylight_saving_time

Ponadto, warto zauważyć, że kiedy mamy do tyłu przejście w DST, niektóre kombinacje data/czas może należeć do obu strefy czasowe. Ta niejednoznaczność może sprawić, że wyniki tej klasy będą się różnić od oczekiwanych. Dlatego jeśli poważnie myślisz o używaniu tej klasy, rozwiń ją dalej i poproś o wyjaśnienia użytkowników w tych przypadkach.

Tutaj jesteś, klasa:

<?php 
class DateTimeDiff extends DateTime 
{ 
    public function diff($datetime, $absolute = false) 
    { 
    // Future releases could fix this bug and if so, this method would become counterproductive. 
    if (version_compare(PHP_VERSION, '5.4.0') > 0) 
     trigger_error("You are using a PHP version that might have adressed the problems of DateTime::diff", E_USER_WARNING); 

    // Have the clock changed? 
    $offset_start = $this->getOffset(); 
    $offset_end = $datetime->getOffset(); 

    if ($offset_start != $offset_end) 
    { 
     // Remember the difference. 
     $clock_moved = $offset_end - $offset_start; 

     // We wouldn't wanna fuck things up for our caller; thus work on a clone. 
     $copy = clone $datetime; 


     if ($clock_moved > 0) 
     { 
      $timestamp_beforesub = $copy->getTimestamp(); 

      // Subtract timedifference from end-datetime should make parent::diff produce accurate results. 
      $copy->sub(DateInterval::createFromDateString("$clock_moved seconds")); 

      // No change occured; sometimes sub() fails. This is a workable hack. 
      if ($timestamp_beforesub == $copy->getTimestamp()) 
       $copy->setTimezone(new DateTimeZone("UTC")); 
     } 

     else // ..else < 0 and its a negative. 
     { 
      $clock_moved *= -1; 

      // Adding that timedifference to end-datetime should make parent::diff produce accurate results. 
      $copy->add(DateInterval::createFromDateString("$clock_moved seconds")); 
     } 

     return parent::diff($copy, $absolute); 
    } // <-- END "if ($offset_start != $offset_end)" 

    return parent::diff($datetime, $absolute); 
    } 
} 
?> 

a strona do testowania (spowoduje wyświetlenie wyników przy użyciu zarówno DateTime :: Diff i DateTimeDiff :: Diff):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
<title>DateTimeDiff-class</title> 

<?php 
if (! (empty($_GET['identifier']) && empty($_GET['start']) && empty($_GET['end']))) 
{ 
    $dt1_new = new DateTimeDiff("{$_GET['start']} {$_GET['identifier']}"); 
    $dt1_old = new DateTime("{$_GET['start']} {$_GET['identifier']}"); 

    $dt2 = new DateTime("{$_GET['end']} {$_GET['identifier']}"); 

    $di_new = $dt1_new->diff($dt2); 
    $di_old = $dt1_old->diff($dt2); 


    // Extract UNIX timestamp and transitional data 
    $timezone_start = $dt1_new->getTimezone(); 
    $timezone_end = $dt2->getTimezone(); 

    $timestamp_start = $dt1_new->getTimeStamp(); 
    $timestamp_end = $dt2->getTimeStamp(); 

    $transitions_start = $timezone_start->getTransitions($timestamp_start, $timestamp_start); 
    $transitions_end = $timezone_end->getTransitions($timestamp_end, $timestamp_end); 

    echo <<<BUILDCONTAINER 

    <script type='text/javascript'> 

     function Container() { } 
     var c_new = new Container; 
     var c_old = new Container; 
     var t_start = new Container; 
     var t_end = new Container; 

    </script> 

BUILDCONTAINER; 

    echo <<<SETTRANSITIONS 

    <script type='text/javascript'> 

     t_start.ts = '{$transitions_start[0]['ts']}'; 
     t_start.time = '{$transitions_start[0]['time']}'; 
     t_start.offset = '{$transitions_start[0]['offset']}'; 

     t_end.ts = '{$transitions_end[0]['ts']}'; 
     t_end.time = '{$transitions_end[0]['time']}'; 
     t_end.offset = '{$transitions_end[0]['offset']}'; 

    </script> 

SETTRANSITIONS; 

    foreach ($di_new as $property => $value) 
     echo "<script type='text/javascript'>c_new.$property = $value</script>"; 

    foreach ($di_old as $property => $value) 
     echo "<script type='text/javascript'>c_old.$property = $value</script>"; 
} 
?> 

<script type='text/javascript'> 

window.onload = function() 
{ 
    if (c_new != null) // <-- em assume everything else is valid too. 
    { 
     // Update page with the results 
     for (var prop in c_new) 
      addtext(prop + ": " + c_new[prop] + " (" + c_old[prop] + ")"); 

     addtext("Read like so.."); 
     addtext("PROPERTY of DateInterval: VALUE using DateTimeDiff::diff ( VALUE using DateTime::diff )"); 

     // Restore values sent/recieved 
     <?php 

      foreach ($_GET as $key => $value) 
       echo "document.getElementById('$key').value = '$value';"; 

     ?> 

     // Display transitiondata (For DateTime start) 
     var p_start = document.getElementById('p_start'); 
     var appendstring = "TS: " + t_start.ts + ", Time: " + t_start.time + ", Offset: " + t_start.offset; 
     p_start.appendChild(document.createTextNode(appendstring)); 

     // Display transitiondata (For DateTime end) 
     var p_end = document.getElementById('p_end'); 
     appendstring = "TS: " + t_end.ts + ", Time: " + t_end.time + ", Offset: " + t_end.offset; 
     p_end.appendChild(document.createTextNode(appendstring)); 
    } 
} 

function addtext() 
{ 
    var p = document.createElement("p"); 
    p.appendChild(document.createTextNode(arguments[0])); 
    document.forms[0].appendChild(p); 
} 

</script> 

</head> 
<body> 
<form action="test2.php" method="get"> 

    <p>Identifier: <input type="text" name="identifier" id="identifier" value="Europe/Stockholm" /></p> 
    <p id="p_start">Start: <input type="text" name="start" id="start" /></p> 
    <p id="p_end">End: <input type="text" name="end" id="end" /></p> 
    <p><input type="submit" /></p> 

</form> 
</body> 
</html> 
4