1 package DateTime::TimeZone::OlsonDB::Observance;
6 use DateTime::Duration;
7 use DateTime::TimeZone::OlsonDB;
8 use DateTime::TimeZone::OlsonDB::Change;
10 use List::Util qw( first );
11 use Params::Validate qw( validate SCALAR ARRAYREF UNDEF OBJECT );
16 my %p = validate( @_, { gmtoff => { type => SCALAR },
17 rules => { type => ARRAYREF },
18 format => { type => SCALAR },
19 until => { type => SCALAR, default => '' },
20 utc_start_datetime => { type => OBJECT | UNDEF },
21 offset_from_std => { type => SCALAR, default => 0 },
22 last_offset_from_utc => { type => SCALAR, default => 0 },
23 last_offset_from_std => { type => SCALAR, default => 0 },
27 my $offset_from_utc = DateTime::TimeZone::offset_as_seconds( $p{gmtoff} );
28 my $offset_from_std = DateTime::TimeZone::offset_as_seconds( $p{offset_from_std} );
30 my $last_offset_from_utc = delete $p{last_offset_from_utc};
31 my $last_offset_from_std = delete $p{last_offset_from_std};
33 my $self = bless { %p,
34 offset_from_utc => $offset_from_utc,
35 offset_from_std => $offset_from_std,
36 until => [ split /\s+/, $p{until} ],
40 $self->_first_rule( $last_offset_from_utc, $last_offset_from_std );
42 if ( $p{utc_start_datetime} )
44 $offset_from_std += $self->{first_rule}->offset_from_std if $self->{first_rule};
46 my $local_start_datetime = $p{utc_start_datetime}->clone;
48 $local_start_datetime +=
49 DateTime::Duration->new( seconds => $offset_from_utc + $offset_from_std );
51 $self->{local_start_datetime} = $local_start_datetime;
57 sub offset_from_utc { $_[0]->{offset_from_utc} }
58 sub offset_from_std { $_[0]->{offset_from_std} }
59 sub total_offset { $_[0]->offset_from_utc + $_[0]->offset_from_std }
61 sub rules { @{ $_[0]->{rules} } }
62 sub first_rule { $_[0]->{first_rule} }
64 sub format { $_[0]->{format} }
66 sub utc_start_datetime { $_[0]->{utc_start_datetime} }
67 sub local_start_datetime { $_[0]->{local_start_datetime} }
73 # real max is year + 1 so we include max year
74 my $max_year = (shift) + 1;
78 if ( $self->utc_start_datetime )
80 $min_year = $self->utc_start_datetime->year;
84 # There is at least one time zone that has an infinite
85 # observance, but that observance has rules that only start at
86 # a certain point - Pacific/Chatham
88 # In this case we just find the earliest rule and start there
90 $min_year = ( sort { $a <=> $b } map { $_->min_year } $self->rules )[0];
93 my $until = $self->until( $zone->last_change->offset_from_std );
96 $max_year = $until->year;
100 # Some zones, like Asia/Tehran, have a predefined fixed set of
101 # rules that go well into the future (2037 for Asia/Tehran)
102 my $max_rule_year = 0;
103 foreach my $rule ( $self->rules )
105 $max_rule_year = $rule->max_year
106 if $rule->max_year && $rule->max_year > $max_rule_year;
109 $max_year = $max_rule_year if $max_rule_year > $max_year;
112 foreach my $year ( $min_year .. $max_year )
114 my @rules = $self->_sorted_rules_for_year($year);
116 foreach my $rule (@rules)
119 $rule->utc_start_datetime_for_year
120 ( $year, $self->offset_from_utc, $zone->last_change->offset_from_std );
122 next if $self->utc_start_datetime && $dt <= $self->utc_start_datetime;
124 my $until = $self->until( $zone->last_change->offset_from_std );
126 next if $until && $dt >= $until;
129 DateTime::TimeZone::OlsonDB::Change->new
131 utc_start_datetime => $dt,
132 local_start_datetime =>
134 DateTime::Duration->new
135 ( seconds => $self->total_offset + $rule->offset_from_std ),
136 short_name => sprintf( $self->{format}, $rule->letter ),
141 if ($DateTime::TimeZone::OlsonDB::DEBUG)
143 print "Adding rule change ...\n";
145 $change->_debug_output;
148 $zone->add_change($change);
153 sub _sorted_rules_for_year
160 sort { $a->[1] <=> $b->[1] }
161 map { my $dt = $_->utc_start_datetime_for_year( $year, $self->offset_from_utc, 0 );
163 grep { $_->min_year <= $year && ( ( ! $_->max_year ) || $_->max_year >= $year ) }
171 my $offset_from_std = shift || $self->offset_from_std;
173 return unless defined $self->until_year;
176 DateTime::TimeZone::OlsonDB::utc_datetime_for_time_spec
177 ( spec => $self->until_time_spec,
178 year => $self->until_year,
179 month => $self->until_month,
180 day => $self->until_day,
181 offset_from_utc => $self->offset_from_utc,
182 offset_from_std => $offset_from_std,
188 sub until_year { $_[0]->{until}[0] }
192 ( defined $_[0]->{until}[1] ?
193 $DateTime::TimeZone::OlsonDB::MONTHS{ $_[0]->{until}[1] } :
200 ( defined $_[0]->{until}[2]
201 ? DateTime::TimeZone::OlsonDB::parse_day_spec
202 ( $_[0]->{until}[2], $_[0]->until_month, $_[0]->until_year )
209 defined $_[0]->{until}[3] ? $_[0]->{until}[3] : '00:00:00';
215 my $last_offset_from_utc = shift;
216 my $last_offset_from_std = shift;
218 return unless $self->rules;
220 my $date = $self->utc_start_datetime
221 or return $self->_first_no_dst_rule;
223 my @rules = $self->rules;
227 my $year = $date->year;
228 foreach my $rule (@rules)
230 # We need to look at what the year _would_ be if we added the
231 # rule's offset to the UTC date. Otherwise we can end up with
232 # a UTC date in year X, and a rule that starts in _local_ year
233 # X + 1, where that rule really does apply to that UTC date.
236 ( seconds => $self->offset_from_utc + $rule->offset_from_std )->year;
238 # Save the highest value
239 $year = $temp_year if $temp_year > $year;
241 next if $rule->min_year > $temp_year;
243 $possible_rules{$rule} = $rule;
246 my $earliest_year = $year - 1;
247 foreach my $rule (@rules)
249 $earliest_year = $rule->min_year
250 if $rule->min_year < $earliest_year;
253 # figure out what date each rule would start on _if_ that rule
254 # were applied to this current observance. this could be a rule
255 # that started much earlier, but is only now active because of an
256 # observance switch. An obnoxious example of this is
257 # America/Phoenix in 1944, which applies the US rule in April,
258 # thus (re-)instating the "war time" rule from 1942. Can you say
259 # ridiculous crack-smoking stupidity?
261 foreach my $y ( $earliest_year .. $year )
264 foreach my $rule ( values %possible_rules )
266 # skip rules that can't have applied the year before the
267 # observance started.
268 if ( $rule->min_year > $y )
270 print "Skipping rule beginning in ", $rule->min_year, ". Year is $y.\n"
271 if $DateTime::TimeZone::OlsonDB::DEBUG;
276 if ( $rule->max_year && $rule->max_year < $y )
278 print "Skipping rule ending in ", $rule->max_year, ". Year is $y.\n"
279 if $DateTime::TimeZone::OlsonDB::DEBUG;
285 $rule->utc_start_datetime_for_year
286 ( $y, $last_offset_from_utc, $last_offset_from_std );
288 push @rule_dates, [ $rule_start, $rule ];
292 @rule_dates = sort { $a->[0] <=> $b->[0] } @rule_dates;
294 print "Looking for first rule ...\n" if $DateTime::TimeZone::OlsonDB::DEBUG;
295 print " Observance starts: ", $date->datetime, "\n\n"
296 if $DateTime::TimeZone::OlsonDB::DEBUG;
298 # ... look through the rules to see if any are still in
299 # effect at the beginning of the observance
300 for ( my $x = 0; $x < @rule_dates; $x++ )
302 my ( $dt, $rule ) = @{ $rule_dates[$x] };
303 my ( $next_dt, $next_rule ) =
304 $x < @rule_dates - 1 ? @{ $rule_dates[ $x + 1 ] } : undef;
306 next if $next_dt && $next_dt < $date;
308 print " This rule starts: ", $dt->datetime, "\n"
309 if $DateTime::TimeZone::OlsonDB::DEBUG;
311 print " Next rule starts: ", $next_dt->datetime, "\n"
312 if $next_dt && $DateTime::TimeZone::OlsonDB::DEBUG;
314 print " No next rule\n\n"
315 if ! $next_dt && $DateTime::TimeZone::OlsonDB::DEBUG;
321 return $rule if $date < $next_dt;
322 return $next_rule if $date == $next_dt;
331 # If this observance has rules, but the rules don't have any
332 # defined changes until after the observance starts, we get the
333 # earliest standard time rule and use it. If there is none, shit
334 # blows up (but this is not the case for any time zones as of
335 # 2009a). I really, really hate the Olson database a lot of the
336 # time! Could this be more arbitrary?
337 my $std_time_rule = $self->_first_no_dst_rule;
339 die "Cannot find a rule that applies to the observance's date range and cannot find a rule without DST to apply"
340 unless $std_time_rule;
342 return $std_time_rule;
345 sub _first_no_dst_rule
350 first { ! $_->offset_from_std } sort { $a->min_year <=> $b->min_year } $self->rules;