Hi!
(long story at the top, suggested patch attached, grep for 'the patch'
if you're in a hurry..)
Recently I again stumble upon the old problem/issue of non-existent
datetimes caused by DST. As you all probably know, once a year a lot of
countries just "steal" an hour. This hardly causes any problems, because
it happens in the wee hours of the night.
But not in America/Sao Paulo (or Cairo, FWIW).
The problem with Sao Paulo is that the choose to remove the hour from
0:00 to 0:59. Which makes this explode:
~$ perl -MDateTime -E 'say
DateTime->new(year=>2012,month=>10,day=>21,time_zone=>"America/Sao_Paulo")'
Invalid local time for date in time zone: America/Sao_Paulo
Calling new without an hour defaults to hour=>0, which just doesn't
exist on this particlar day.
This issue was brought up several times on this list in the past years,
and the answer was "then use UTC, and convert later":
Now this is not always possible (eg I need to compare local timezone
values coming from Perl with local time zone values stored in a DB, and
not using the local time zone would be a hassle).
But what's really annoying is this inconsistency:
(note that we now start with 2012-10-20)
~$ perl -MDateTime -E 'say
DateTime->new(year=>2012,month=>10,day=>20,time_zone=>"America/Sao_Paulo")->add(days=>1)'
Invalid local time for date in time zone: America/Sao_Paulo
But:
~$ perl -MDateTime -E 'say
DateTime->new(year=>2012,month=>10,day=>20,time_zone=>"America/Sao_Paulo")->add(hours=>24)'
2012-10-21T01:00:00
I know that day math is complex, and adding days is different from
adding hours (~seconds). But I still find it annoying to have to work
around crazy corner cases (and basically having to pack every DateTime
call into an eval)
Anyway, I couldn't sleep tonight (no, I'm not writing from America/Sao
Paulo..), so I took a rather deep look into DateTime::TimeZone and after
lots of clueless poking I can up with something that seems to work:
the patch:
When going through all the DST-changes (aka $tz->{spans}) in
DateTime::TimeZone::_spans_binary_search I checked if the spans did not
connect. If the didn't and if the local time ($seconds) falls between
the gap, I just return the later span. Resulting in a sort of hackish
workaround that sort of works (according to the also attached test case)
The rest of the test suite (DateTime and DateTime::TimeZone) mostly
works, only the tests checking for DST-related exceptions fail.
With my patch, we now get:
~$ perl -MDateTime -E 'say
DateTime->new(year=>2012,month=>10,day=>21,time_zone=>"America/Sao_Paulo")'
2012-10-21T01:00:00
Adding/Subtracting seconds or days behave the same.
The only potential deal-breaker is the fact that if you now create a new
DateTime that does not exists, you'll get the next valid time instead of
an exception (which does sound like a feature to me, but backwards
compability might have other ideas about that...)
Oh, and requesting a specific non-existed time also has strange results:
~$ perl -MDateTime -E 'say
DateTime->new(year=>2012,month=>10,day=>21,hour=>0,minute=>30,time_zone=>"America/Sao_Paulo")'
2012-10-21T01:30:00
So, what do you think?
Totally crazy? Or any chance that this might be included in
DateTime::TimeZone
Greetings,
domm
PS: Regarding Cairo: It seems that after beeing totally crazy and
changing DST 4 times a year in 2010 (due to Ramadan), they now stopped
doing DST altogether and thus went completely sane...
http://www.timeanddate.com/news/time/egypt-ends-dst-2010.html
--
#!/usr/bin/perl http://domm.plix.at
for(ref bless{},just'another'perl'hacker){s-:+-$"-g&&print$_.$/}
>From f91bd008dba06bf49a4fa62b34922dae86e37c43 Mon Sep 17 00:00:00 2001
From: Thomas Klausner <[email protected]>
Date: Wed, 27 Jun 2012 02:43:54 +0200
Subject: [PATCH] try to work around nonexistent DST datetimes (incl test)
---
lib/DateTime/TimeZone.pm | 8 +++++
t/22_dst_mess.t | 73 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 81 insertions(+)
create mode 100644 t/22_dst_mess.t
diff --git a/lib/DateTime/TimeZone.pm b/lib/DateTime/TimeZone.pm
index 3353188..51111ec 100644
--- a/lib/DateTime/TimeZone.pm
+++ b/lib/DateTime/TimeZone.pm
@@ -217,6 +217,10 @@ sub _spans_binary_search {
my $current = $self->{spans}[$i];
if ( $seconds < $current->[$start] ) {
+ if ($seconds > $self->{spans}[$i-1][$end]) {
+ return $self->{spans}[$i-1];
+ }
+
$max = $i;
my $c = int( ( $i - $min ) / 2 );
$c ||= 1;
@@ -227,6 +231,10 @@ sub _spans_binary_search {
}
elsif ( $seconds >= $current->[$end] ) {
$min = $i;
+ if ($seconds < $self->{spans}[$i+1][$start]) {
+ return $current;
+ }
+
my $c = int( ( $max - $i ) / 2 );
$c ||= 1;
diff --git a/t/22_dst_mess.t b/t/22_dst_mess.t
new file mode 100644
index 0000000..a3b7263
--- /dev/null
+++ b/t/22_dst_mess.t
@@ -0,0 +1,73 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use DateTime;
+
+{ # adding to DateTime skips non-existing times America/Sao_Paulo
+ my $dt = DateTime->new(
+ year => 2012, month => 10, day => 20, hour=>23, minute=>59,
+ time_zone => 'America/Sao_Paulo',
+ );
+ $dt->add(minutes=>1);
+ is($dt->iso8601,'2012-10-21T01:00:00','America/Sao_Paulo: 2012-10-20T23:59:00 +1min = 2012-10-21T01:00:00');
+}
+
+{ # adding to DateTime skips non-existing times America/Sao_Paulo
+ my $dt = DateTime->new(
+ year => 2012, month => 10, day => 20,
+ time_zone => 'America/Sao_Paulo',
+ );
+ $dt->add(days=>1);
+ is($dt->iso8601,'2012-10-21T01:00:00','America/Sao_Paulo: 2012-10-20T23:59:00 +1min = 2012-10-21T01:00:00');
+}
+
+{ # adding to DateTime skips non-existing times Africa/Cairo
+ my $dt = DateTime->new(
+ year => 2010, month => 4, day => 29, hour=>23, minute=>59,
+ time_zone => 'Africa/Cairo',
+ );
+ $dt->add(minutes=>1);
+ is($dt->iso8601,'2010-04-30T01:00:00','Africa/Cairo: 2010-04-20T23:59:00 +1min = 2010-04-30T01:00:00');
+
+}
+
+{ # creating new DateTime
+ my $dt = DateTime->new(
+ year => 2012, month => 10, day => 21,
+ time_zone => 'America/Sao_Paulo',
+ );
+ is($dt->iso8601,'2012-10-21T01:00:00','America/Sao_Paulo: new 2012-10-21 -> 2012-10-21T01:00:00');
+
+}
+
+{ # subtracting to DateTime skips non-existing times America/Sao_Paulo
+ my $dt = DateTime->new(
+ year => 2012, month => 10, day => 21, hour=>1, minute=>0,
+ time_zone => 'America/Sao_Paulo',
+ );
+ $dt->subtract(minutes=>1);
+ is($dt->iso8601,'2012-10-20T23:59:00','America/Sao_Paulo: 2012-10-21T01:00:00 -1min = 2012-10-20T23:59:00');
+}
+
+{ # truncate
+ my $dt = DateTime->new(
+ year => 2012, month => 10, day => 21, hour=>12, minute=>42,
+ time_zone => 'America/Sao_Paulo',
+ );
+ $dt->truncate(to=>'day');
+ is($dt->iso8601,'2012-10-21T01:00:00','America/Sao_Paulo: truncate day 2012-10-21T01:00:00');
+}
+
+{ # creating new DateTime
+ my $dt = DateTime->new(
+ year => 2012, month => 10, day => 21,hour=>0,minute=>30,
+ time_zone => 'America/Sao_Paulo',
+ );
+ is($dt->iso8601,'2012-10-21T01:30:00','America/Sao_Paulo: new 2012-10-21T00:30:00 -> 2012-10-21T01:30:00');
+
+}
+
+
+done_testing();
--
1.7.10