1 |
wakaba |
1.1 |
package Message::Date; |
2 |
|
|
use strict; |
3 |
|
|
use warnings; |
4 |
|
|
|
5 |
|
|
my $default_level = {must => 'm', unsupported => 'u'}; |
6 |
|
|
my $default_onerror = sub { |
7 |
|
|
my %opt = @_; |
8 |
|
|
my @msg = ($opt{type}); |
9 |
|
|
push @msg, $opt{value} if defined $opt{value}; |
10 |
|
|
warn join '; ', @msg, "\n"; |
11 |
|
|
}; |
12 |
|
|
|
13 |
|
|
sub new ($) { |
14 |
|
|
my $self = bless {}, shift; |
15 |
|
|
|
16 |
|
|
## Public fields |
17 |
|
|
$self->{onerror} = $default_onerror; |
18 |
|
|
$self->{level} = $default_level; |
19 |
|
|
|
20 |
|
|
return $self; |
21 |
|
|
} # new |
22 |
|
|
|
23 |
|
|
sub _create_object ($$$$$$$$$;$) { |
24 |
|
|
#my ($self, $y, $M, $d, $h, $m, $s, $zh, $zm, $diff) = @_; |
25 |
|
|
my $self = shift; |
26 |
|
|
|
27 |
|
|
my $class = 'Message::Date::TimeT'; |
28 |
|
|
unless ($DateTime::VERSION) { |
29 |
|
|
# eval { require DateTime }; |
30 |
|
|
} |
31 |
|
|
if ($DateTime::VERSION) { |
32 |
|
|
$class = 'Message::Date::DateTime'; |
33 |
|
|
} |
34 |
|
|
|
35 |
|
|
bless $self, $class; |
36 |
|
|
return $self->_set_value (@_); |
37 |
|
|
} # _create_object |
38 |
|
|
|
39 |
|
|
sub _is_leap_year ($) { |
40 |
|
|
return ($_[0] % 400 == 0 or ($_[0] % 4 == 0 and $_[0] % 100 != 0)); |
41 |
|
|
} # _is_leap_year |
42 |
|
|
|
43 |
|
|
sub _last_week_number ($) { |
44 |
|
|
## ISSUE: HTML5 definition is wrong. <http://en.wikipedia.org/wiki/ISO_week_date#Relation_with_the_Gregorian_calendar> |
45 |
|
|
my $jan1_dow = [gmtime Time::Local::timegm (0, 0, 0, 1, 1 - 1, $_[0])]->[6]; |
46 |
|
|
return ($jan1_dow == 4 or |
47 |
|
|
($jan1_dow == 3 and _is_leap_year ($_[0]))) ? 53 : 52; |
48 |
|
|
} # _last_week_number |
49 |
|
|
|
50 |
|
|
sub _week_year_diff ($) { |
51 |
|
|
my $jan1_dow = [gmtime Time::Local::timegm (0, 0, 0, 1, 1 - 1, $_[0])]->[6]; |
52 |
|
|
if ($jan1_dow <= 4) { |
53 |
|
|
return $jan1_dow - 1; |
54 |
|
|
} else { |
55 |
|
|
return $jan1_dow - 8; |
56 |
|
|
} |
57 |
|
|
} # _week_year_diff |
58 |
|
|
|
59 |
|
|
## Time string [HTML5] |
60 |
|
|
sub parse_time_string ($$) { |
61 |
|
|
my ($self, $value) = @_; |
62 |
|
|
$self = $self->new unless ref $self; |
63 |
|
|
|
64 |
|
|
if ($value =~ /\A |
65 |
|
|
([0-9]{2}):([0-9]{2})(?>:([0-9]{2})(?>(\.[0-9]+))?)? |
66 |
|
|
\z/x) { |
67 |
|
|
my ($h, $m, $s, $sf) = ($1, $2, $3, $4); |
68 |
|
|
$self->{onerror}->(type => 'datetime:bad hour', |
69 |
|
|
level => $self->{level}->{must}), return undef if $h > 23; |
70 |
|
|
$self->{onerror}->(type => 'datetime:bad minute', |
71 |
|
|
level => $self->{level}->{must}), return undef if $m > 59; |
72 |
|
|
$s ||= 0; |
73 |
|
|
$self->{onerror}->(type => 'datetime:bad second', |
74 |
|
|
level => $self->{level}->{must}), return undef if $s > 59; |
75 |
|
|
$sf = defined $sf ? $sf : ''; |
76 |
|
|
|
77 |
|
|
if (defined wantarray) { |
78 |
|
|
return $self->_create_object (1970, 1, 1, $h, $m, $s, $sf, 0, 0); |
79 |
|
|
} |
80 |
|
|
} else { |
81 |
|
|
$self->{onerror}->(type => 'time:syntax error', ## TODOC: type |
82 |
|
|
level => $self->{level}->{must}); |
83 |
|
|
return undef; |
84 |
|
|
} |
85 |
|
|
} # parse_time_string |
86 |
|
|
|
87 |
|
|
## Time string [HTML5] |
88 |
|
|
sub to_time_string ($) { |
89 |
|
|
my $self = shift; |
90 |
|
|
|
91 |
|
|
return sprintf '%02d:%02d:%02d%s', |
92 |
|
|
$self->utc_hour, $self->utc_minute, |
93 |
|
|
$self->utc_second, $self->utc_second_fraction_string; |
94 |
|
|
} # to_time_string |
95 |
|
|
|
96 |
|
|
## Week string [HTML5] |
97 |
|
|
sub parse_week_string ($$) { |
98 |
|
|
my ($self, $value) = @_; |
99 |
|
|
$self = $self->new unless ref $self; |
100 |
|
|
|
101 |
|
|
if ($value =~ /\A([0-9]{4,})-W([0-9]{2})\z/x) { |
102 |
|
|
my ($y, $w) = ($1, $2); |
103 |
|
|
$self->{onerror}->(type => 'week:bad year', ## TODOC: type |
104 |
|
|
level => $self->{level}->{must}) if $y == 0; |
105 |
|
|
$self->{onerror}->(type => 'week:bad week', ## TODOC: type |
106 |
|
|
level => $self->{level}->{must}) |
107 |
|
|
if $w > _last_week_number ($y); |
108 |
|
|
|
109 |
|
|
if (defined wantarray) { |
110 |
|
|
my $day = $w * 7 - _week_year_diff ($y); |
111 |
|
|
|
112 |
|
|
return $self->_create_object ($y, 1, 1, 0, 0, 0, '', 0, 0, |
113 |
|
|
$day * 24 * 3600 * 1000); |
114 |
|
|
} |
115 |
|
|
} else { |
116 |
|
|
$self->{onerror}->(type => 'week:syntax error', ## TODOC: type |
117 |
|
|
level => $self->{level}->{must}); |
118 |
|
|
return undef; |
119 |
|
|
} |
120 |
|
|
} # parse_week_string |
121 |
|
|
|
122 |
|
|
## Week string [HTML5] |
123 |
|
|
sub to_week_string ($) { |
124 |
|
|
my $self = shift; |
125 |
|
|
|
126 |
|
|
return sprintf '%04d-W%02d', $self->utc_week_year, $self->utc_week; |
127 |
|
|
} # to_week_string |
128 |
|
|
|
129 |
|
|
## Month string [HTML5] |
130 |
|
|
sub parse_month_string ($$) { |
131 |
|
|
my ($self, $value) = @_; |
132 |
|
|
$self = $self->new unless ref $self; |
133 |
|
|
|
134 |
|
|
if ($value =~ /\A([0-9]{4,})-([0-9]{2})\z/) { |
135 |
|
|
my ($y, $M) = ($1, $2); |
136 |
|
|
if (0 < $M and $M < 13) { |
137 |
|
|
# |
138 |
|
|
} else { |
139 |
|
|
$self->{onerror}->(type => 'datetime:bad month', |
140 |
|
|
level => $self->{level}->{must}); |
141 |
|
|
return undef; |
142 |
|
|
} |
143 |
|
|
|
144 |
|
|
if (defined wantarray) { |
145 |
|
|
return $self->_create_object ($y, $M, 1, 0, 0, 0, '', 0, 0); |
146 |
|
|
} |
147 |
|
|
} else { |
148 |
|
|
$self->{onerror}->(type => 'month:syntax error', ## TODOC: type |
149 |
|
|
level => $self->{level}->{must}); |
150 |
|
|
return undef; |
151 |
|
|
} |
152 |
|
|
} # parse_month_string |
153 |
|
|
|
154 |
|
|
## Month string [HTML5] |
155 |
|
|
sub to_month_string ($) { |
156 |
|
|
my $self = shift; |
157 |
|
|
|
158 |
|
|
return sprintf '%04d-%02d', $self->utc_year, $self->utc_month; |
159 |
|
|
} # to_month_string |
160 |
|
|
|
161 |
|
|
## Date string [HTML5] |
162 |
|
|
sub parse_date_string ($$) { |
163 |
|
|
my ($self, $value) = @_; |
164 |
|
|
$self = $self->new unless ref $self; |
165 |
|
|
|
166 |
|
|
if ($value =~ /\A([0-9]{4,})-([0-9]{2})-([0-9]{2})\z/x) { |
167 |
|
|
my ($y, $M, $d) = ($1, $2, $3); |
168 |
|
|
if (0 < $M and $M < 13) { |
169 |
|
|
$self->{onerror}->(type => 'datetime:bad day', |
170 |
|
|
level => $self->{level}->{must}), return undef |
171 |
|
|
if $d < 1 or |
172 |
|
|
$d > [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]->[$M]; |
173 |
|
|
$self->{onerror}->(type => 'datetime:bad day', |
174 |
|
|
level => $self->{level}->{must}), return undef |
175 |
|
|
if $M == 2 and $d == 29 and |
176 |
|
|
not ($y % 400 == 0 or ($y % 4 == 0 and $y % 100 != 0)); |
177 |
|
|
} else { |
178 |
|
|
$self->{onerror}->(type => 'datetime:bad month', |
179 |
|
|
level => $self->{level}->{must}); |
180 |
|
|
return undef; |
181 |
|
|
} |
182 |
|
|
|
183 |
|
|
if (defined wantarray) { |
184 |
|
|
return $self->_create_object ($y, $M, $d, 0, 0, 0, '', 0, 0); |
185 |
|
|
} |
186 |
|
|
} else { |
187 |
|
|
$self->{onerror}->(type => 'date:syntax error', ## TODOC: type |
188 |
|
|
level => $self->{level}->{must}); |
189 |
|
|
return undef; |
190 |
|
|
} |
191 |
|
|
} # parse_date_string |
192 |
|
|
|
193 |
|
|
## Date string [HTML5] |
194 |
|
|
sub to_date_string ($) { |
195 |
|
|
my $self = shift; |
196 |
|
|
|
197 |
|
|
return sprintf '%04d-%02d-%02d', |
198 |
|
|
$self->utc_year, $self->utc_month, $self->utc_day; |
199 |
|
|
} # to_date_string |
200 |
|
|
|
201 |
|
|
## Local date and time string [HTML5] |
202 |
|
|
sub parse_local_date_and_time_string ($$) { |
203 |
|
|
my ($self, $value) = @_; |
204 |
|
|
$self = $self->new unless ref $self; |
205 |
|
|
|
206 |
|
|
if ($value =~ /\A([0-9]{4,})-([0-9]{2})-([0-9]{2})T |
207 |
|
|
([0-9]{2}):([0-9]{2})(?>:([0-9]{2})(?>(\.[0-9]+))?)?\z/x) { |
208 |
|
|
my ($y, $M, $d, $h, $m, $s, $sf) = ($1, $2, $3, $4, $5, $6, $7); |
209 |
|
|
if (0 < $M and $M < 13) { |
210 |
|
|
$self->{onerror}->(type => 'datetime:bad day', |
211 |
|
|
level => $self->{level}->{must}), return undef |
212 |
|
|
if $d < 1 or |
213 |
|
|
$d > [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]->[$M]; |
214 |
|
|
$self->{onerror}->(type => 'datetime:bad day', |
215 |
|
|
level => $self->{level}->{must}), return undef |
216 |
|
|
if $M == 2 and $d == 29 and not _is_leap_year ($y); |
217 |
|
|
} else { |
218 |
|
|
$self->{onerror}->(type => 'datetime:bad month', |
219 |
|
|
level => $self->{level}->{must}); |
220 |
|
|
return undef; |
221 |
|
|
} |
222 |
|
|
$self->{onerror}->(type => 'datetime:bad hour', |
223 |
|
|
level => $self->{level}->{must}), return undef if $h > 23; |
224 |
|
|
$self->{onerror}->(type => 'datetime:bad minute', |
225 |
|
|
level => $self->{level}->{must}), return undef if $m > 59; |
226 |
|
|
$s ||= 0; |
227 |
|
|
$self->{onerror}->(type => 'datetime:bad second', |
228 |
|
|
level => $self->{level}->{must}), return undef if $s > 59; |
229 |
|
|
$sf = defined $sf ? $sf : ''; |
230 |
|
|
|
231 |
|
|
if (defined wantarray) { |
232 |
|
|
return $self->_create_object ($y, $M, $d, $h, $m, $s, $sf, '-00', 0); |
233 |
|
|
} |
234 |
|
|
} else { |
235 |
|
|
$self->{onerror}->(type => 'datetime-local:syntax error', ## TODOC: type |
236 |
|
|
level => $self->{level}->{must}); |
237 |
|
|
return undef; |
238 |
|
|
} |
239 |
|
|
} # parse_local_date_and_time_string |
240 |
|
|
|
241 |
|
|
## Local date and time string [HTML5] |
242 |
|
|
sub to_local_date_and_time_string ($) { |
243 |
|
|
my $self = shift; |
244 |
|
|
|
245 |
|
|
return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%s', |
246 |
|
|
$self->year, $self->month, $self->day, |
247 |
|
|
$self->hour, $self->minute, $self->second, $self->second_fraction_string; |
248 |
|
|
} # to_local_date_and_time_string |
249 |
|
|
|
250 |
|
|
## Global date and time string [HTML5] |
251 |
|
|
sub parse_global_date_and_time_string ($$) { |
252 |
|
|
my ($self, $value) = @_; |
253 |
|
|
$self = $self->new unless ref $self; |
254 |
|
|
|
255 |
|
|
if ($value =~ /\A([0-9]{4,})-([0-9]{2})-([0-9]{2})T |
256 |
|
|
([0-9]{2}):([0-9]{2})(?>:([0-9]{2})(?>(\.[0-9]+))?)? |
257 |
|
|
(?>Z|([+-][0-9]{2}):([0-9]{2}))\z/x) { |
258 |
|
|
my ($y, $M, $d, $h, $m, $s, $sf, $zh, $zm) |
259 |
|
|
= ($1, $2, $3, $4, $5, $6, $7, $8, $9); |
260 |
|
|
if (0 < $M and $M < 13) { |
261 |
|
|
$self->{onerror}->(type => 'datetime:bad day', |
262 |
|
|
level => $self->{level}->{must}), return undef |
263 |
|
|
if $d < 1 or |
264 |
|
|
$d > [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]->[$M]; |
265 |
|
|
$self->{onerror}->(type => 'datetime:bad day', |
266 |
|
|
level => $self->{level}->{must}), return undef |
267 |
|
|
if $M == 2 and $d == 29 and not _is_leap_year ($y); |
268 |
|
|
} else { |
269 |
|
|
$self->{onerror}->(type => 'datetime:bad month', |
270 |
|
|
level => $self->{level}->{must}); |
271 |
|
|
return undef; |
272 |
|
|
} |
273 |
|
|
$self->{onerror}->(type => 'datetime:bad hour', |
274 |
|
|
level => $self->{level}->{must}), return undef if $h > 23; |
275 |
|
|
$self->{onerror}->(type => 'datetime:bad minute', |
276 |
|
|
level => $self->{level}->{must}), return undef if $m > 59; |
277 |
|
|
$s ||= 0; |
278 |
|
|
$self->{onerror}->(type => 'datetime:bad second', |
279 |
|
|
level => $self->{level}->{must}), return undef if $s > 59; |
280 |
|
|
$sf = defined $sf ? $sf : ''; |
281 |
|
|
if (defined $zh) { |
282 |
|
|
$self->{onerror}->(type => 'datetime:bad timezone hour', |
283 |
|
|
level => $self->{level}->{must}), return undef |
284 |
|
|
if $zh > 23; |
285 |
|
|
$self->{onerror}->(type => 'datetime:bad timezone minute', |
286 |
|
|
level => $self->{level}->{must}), return undef |
287 |
|
|
if $zm > 59; |
288 |
|
|
} else { |
289 |
|
|
$zh = 0; |
290 |
|
|
$zm = 0; |
291 |
|
|
} |
292 |
|
|
## ISSUE: Maybe timezone -00:00 should have same semantics as in RFC 3339. |
293 |
|
|
|
294 |
|
|
if (defined wantarray) { |
295 |
|
|
return $self->_create_object ($y, $M, $d, $h, $m, $s, $sf, $zh, $zm); |
296 |
|
|
} |
297 |
|
|
} else { |
298 |
|
|
$self->{onerror}->(type => 'datetime:syntax error', |
299 |
|
|
level => $self->{level}->{must}); |
300 |
|
|
return undef; |
301 |
|
|
} |
302 |
|
|
} # parse_global_date_and_time_string |
303 |
|
|
|
304 |
|
|
## Global date and time string [HTML5], always in UTC |
305 |
|
|
sub to_global_date_and_time_string ($) { |
306 |
|
|
my $self = shift; |
307 |
|
|
|
308 |
|
|
return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%sZ', |
309 |
|
|
$self->utc_year, $self->utc_month, $self->utc_day, |
310 |
|
|
$self->utc_hour, $self->utc_minute, |
311 |
|
|
$self->utc_second, $self->utc_second_fraction_string; |
312 |
|
|
} # to_global_date_and_time_string |
313 |
|
|
|
314 |
|
|
sub timezone_offset_second ($) { |
315 |
|
|
my $self = shift; |
316 |
|
|
return $self->timezone_hour * 3600 + $self->timezone_minute * 60; |
317 |
|
|
} # timezone_offset_second |
318 |
|
|
|
319 |
|
|
sub utc_week ($) { |
320 |
|
|
my $self = shift; |
321 |
|
|
|
322 |
|
|
if (defined $self->{cache}->{utc_week}) { |
323 |
|
|
return $self->{cache}->{utc_week}; |
324 |
|
|
} |
325 |
|
|
|
326 |
|
|
my $year = $self->utc_year; |
327 |
|
|
|
328 |
|
|
my $jan1 = __PACKAGE__->new->_create_object ($year, 1, 1, 0, 0, 0, 0, 0, 0); |
329 |
|
|
|
330 |
|
|
my $days = $self->to_unix_integer - $jan1->to_unix_integer; |
331 |
|
|
$days /= 24 * 3600; |
332 |
|
|
|
333 |
|
|
my $week_year_diff = _week_year_diff ($year); |
334 |
|
|
$days += $week_year_diff; |
335 |
|
|
|
336 |
|
|
my $week = int ($days / 7) + 1; |
337 |
|
|
|
338 |
|
|
if ($days < 0) { |
339 |
|
|
$year--; |
340 |
|
|
$week = _last_week_number ($year); |
341 |
|
|
} elsif ($week > _last_week_number ($year)) { |
342 |
|
|
$year++; |
343 |
|
|
$week = 1; |
344 |
|
|
} |
345 |
|
|
|
346 |
|
|
$self->{cache}->{utc_week_year} = $year; |
347 |
|
|
$self->{cache}->{utc_week} = $week; |
348 |
|
|
|
349 |
|
|
return $week; |
350 |
|
|
} # utc_week |
351 |
|
|
|
352 |
|
|
sub utc_week_year ($) { |
353 |
|
|
my $self = shift; |
354 |
|
|
$self->utc_week; |
355 |
|
|
return $self->{cache}->{utc_week_year}; |
356 |
|
|
} # utc_week_year |
357 |
|
|
|
358 |
|
|
sub to_html5_month_number ($) { |
359 |
|
|
my $self = shift; |
360 |
|
|
|
361 |
|
|
## ISSUE: "the number of months between January 1970 and the parsed |
362 |
|
|
## month.": "between"? inclusive or exclusive or anything else? |
363 |
|
|
## months before 1970? |
364 |
|
|
|
365 |
|
|
my $y = $self->year - 1970; |
366 |
|
|
my $m = $self->month - 1; |
367 |
|
|
|
368 |
|
|
return $y * 12 + $m; |
369 |
|
|
} # to_html5_month_number |
370 |
|
|
|
371 |
|
|
package Message::Date::TimeT; |
372 |
|
|
push our @ISA, 'Message::Date'; |
373 |
|
|
|
374 |
|
|
## TODO: Should be moved to a separate module. |
375 |
|
|
|
376 |
|
|
require Time::Local; |
377 |
|
|
my $unix_epoch = Time::Local::timegm (0, 0, 0, 1, 1 - 1, 1970); |
378 |
|
|
|
379 |
|
|
sub _set_value ($$$$$$$$$$;$) { |
380 |
|
|
my $self = shift; |
381 |
|
|
my ($y, $M, $d, $h, $m, $s, $sf, $zh, $zm, $diff) = @_; |
382 |
|
|
|
383 |
|
|
$self->{value} = Time::Local::timegm_nocheck |
384 |
|
|
($s, $m - $zm, $h - $zh, $d, $M-1, $y); |
385 |
|
|
$self->{timezone_hour} = $zh; |
386 |
|
|
$self->{timezone_minute} = $zm; |
387 |
|
|
|
388 |
|
|
if ($self->year != $y or |
389 |
|
|
$self->month != $M or |
390 |
|
|
$self->day != $d or |
391 |
|
|
$self->hour != $h or |
392 |
|
|
$self->minute != $m) { |
393 |
|
|
$self->{onerror}->(type => 'date value not supported', |
394 |
|
|
value => join (", ", @_), |
395 |
|
|
level => $self->{level}->{unsupported}); |
396 |
|
|
return undef; |
397 |
|
|
} |
398 |
|
|
|
399 |
|
|
if ($diff) { |
400 |
|
|
my $v = $self->{value} . $sf; |
401 |
|
|
$v += $diff / 1000; |
402 |
|
|
my $int_v = int $v; |
403 |
|
|
if ($int_v != $v) { |
404 |
|
|
if ($v > 0) { |
405 |
|
|
$self->{value} = $int_v; |
406 |
|
|
$sf = $v - $int_v; |
407 |
|
|
} else { |
408 |
|
|
$self->{value} = $int_v - 1; |
409 |
|
|
$sf = $v - $int_v - 1; |
410 |
|
|
} |
411 |
|
|
} else { |
412 |
|
|
$self->{value} = $v; |
413 |
|
|
$sf = ''; |
414 |
|
|
} |
415 |
|
|
} |
416 |
|
|
|
417 |
|
|
$self->{second_fraction} = $sf; |
418 |
|
|
|
419 |
|
|
delete $self->{cache}; |
420 |
|
|
|
421 |
|
|
return $self; |
422 |
|
|
} # _set_value |
423 |
|
|
|
424 |
|
|
sub second_fraction_string ($) { |
425 |
|
|
my $self = shift; |
426 |
|
|
if ($self->{second_fraction}) { |
427 |
|
|
my $v = $self->{second_fraction}; |
428 |
|
|
unless (substr ($v, 0, 1) eq '.') { |
429 |
|
|
$v = sprintf '%.100f', $v; |
430 |
|
|
$v = substr $v, 1; |
431 |
|
|
} |
432 |
|
|
$v = substr $v, 1; |
433 |
|
|
$v =~ s/0+\z//; |
434 |
|
|
return length $v ? '.' . $v :''; |
435 |
|
|
} else { |
436 |
|
|
return ''; |
437 |
|
|
} |
438 |
|
|
} # second_fraction_string |
439 |
|
|
|
440 |
|
|
## Timezone component [HTML5] |
441 |
|
|
sub timezone_string ($) { |
442 |
|
|
my $self = shift; |
443 |
|
|
if ($self->{timezone_hour} eq '-00') { |
444 |
|
|
return sprintf '-00:%02d', $self->{timezone_minute}; |
445 |
|
|
} elsif ($self->{timezone_hour} == 0 and |
446 |
|
|
$self->{timezone_minute} == 0) { |
447 |
|
|
return 'Z'; |
448 |
|
|
} elsif ($self->{timezone_hour} >= 0) { |
449 |
|
|
return sprintf '+%02d:%02d', $self->{timezone_hour}, $self->{timezone_minute}; |
450 |
|
|
} else { |
451 |
|
|
return sprintf '-%02d:%02d', |
452 |
|
|
-$self->{timezone_hour}, $self->{timezone_minute}; |
453 |
|
|
} |
454 |
|
|
} # timezone_string |
455 |
|
|
|
456 |
|
|
sub _utc_time ($) { |
457 |
|
|
my $self = shift; |
458 |
|
|
$self->{cache}->{utc_time} = [gmtime $self->{value}]; |
459 |
|
|
} # _utc_time |
460 |
|
|
|
461 |
|
|
sub _local_time ($) { |
462 |
|
|
my $self = shift; |
463 |
|
|
$self->{cache}->{local_time} = [gmtime ($self->{value} + $self->timezone_offset_second)]; |
464 |
|
|
} # _local_time |
465 |
|
|
|
466 |
|
|
sub year ($) { |
467 |
|
|
my $self = shift; |
468 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
469 |
|
|
return $self->{cache}->{local_time}->[5] + 1900; |
470 |
|
|
} # year |
471 |
|
|
|
472 |
|
|
sub month ($) { |
473 |
|
|
my $self = shift; |
474 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
475 |
|
|
return $self->{cache}->{local_time}->[4] + 1; |
476 |
|
|
} # month |
477 |
|
|
|
478 |
|
|
sub day ($) { |
479 |
|
|
my $self = shift; |
480 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
481 |
|
|
return $self->{cache}->{local_time}->[3]; |
482 |
|
|
} # day |
483 |
|
|
|
484 |
|
|
sub day_of_week ($) { |
485 |
|
|
my $self = shift; |
486 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
487 |
|
|
return $self->{cache}->{local_time}->[6]; # 0..6 |
488 |
|
|
} # day_of_week |
489 |
|
|
|
490 |
|
|
sub hour ($) { |
491 |
|
|
my $self = shift; |
492 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
493 |
|
|
return $self->{cache}->{local_time}->[2]; |
494 |
|
|
} # hour |
495 |
|
|
|
496 |
|
|
sub minute ($) { |
497 |
|
|
my $self = shift; |
498 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
499 |
|
|
return $self->{cache}->{local_time}->[1]; |
500 |
|
|
} # minute |
501 |
|
|
|
502 |
|
|
sub second ($) { |
503 |
|
|
my $self = shift; |
504 |
|
|
$self->_local_time unless defined $self->{cache}->{local_time}; |
505 |
|
|
return $self->{cache}->{local_time}->[0]; |
506 |
|
|
} # second |
507 |
|
|
|
508 |
|
|
sub fractional_second ($) { |
509 |
|
|
my $self = shift; |
510 |
|
|
return $self->second + $self->{second_fraction}; |
511 |
|
|
} # fractional_second |
512 |
|
|
|
513 |
|
|
sub utc_year ($) { |
514 |
|
|
my $self = shift; |
515 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
516 |
|
|
return $self->{cache}->{utc_time}->[5] + 1900; |
517 |
|
|
} # utc_year |
518 |
|
|
|
519 |
|
|
sub utc_month ($) { |
520 |
|
|
my $self = shift; |
521 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
522 |
|
|
return $self->{cache}->{utc_time}->[4] + 1; |
523 |
|
|
} # utc_month |
524 |
|
|
|
525 |
|
|
sub utc_day ($) { |
526 |
|
|
my $self = shift; |
527 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
528 |
|
|
return $self->{cache}->{utc_time}->[3]; |
529 |
|
|
} # utc_day |
530 |
|
|
|
531 |
|
|
sub utc_day_of_week ($) { |
532 |
|
|
my $self = shift; |
533 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
534 |
|
|
return $self->{cache}->{utc_time}->[6]; # 0..6 |
535 |
|
|
} # utc_day_of_week |
536 |
|
|
|
537 |
|
|
sub utc_hour ($) { |
538 |
|
|
my $self = shift; |
539 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
540 |
|
|
return $self->{cache}->{utc_time}->[2]; |
541 |
|
|
} # utc_hour |
542 |
|
|
|
543 |
|
|
sub utc_minute ($) { |
544 |
|
|
my $self = shift; |
545 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
546 |
|
|
return $self->{cache}->{utc_time}->[1]; |
547 |
|
|
} # utc_minute |
548 |
|
|
|
549 |
|
|
sub utc_second ($) { |
550 |
|
|
my $self = shift; |
551 |
|
|
$self->_utc_time unless defined $self->{cache}->{utc_time}; |
552 |
|
|
return $self->{cache}->{utc_time}->[0]; |
553 |
|
|
} # utc_second |
554 |
|
|
|
555 |
|
|
sub utc_fractional_second ($) { |
556 |
|
|
my $self = shift; |
557 |
|
|
return $self->utc_second + $self->{second_fraction}; |
558 |
|
|
} # utc_fractional_second |
559 |
|
|
|
560 |
|
|
sub timezone_hour ($) { |
561 |
|
|
my $self = shift; |
562 |
|
|
return $self->{timezone_hour}; |
563 |
|
|
} # timezone_hour |
564 |
|
|
|
565 |
|
|
sub timezone_minute ($) { |
566 |
|
|
my $self = shift; |
567 |
|
|
return $self->{timezone_minute}; |
568 |
|
|
} # timezone_minute |
569 |
|
|
|
570 |
|
|
sub to_html5_number ($) { |
571 |
|
|
my $self = shift; |
572 |
|
|
my $int = $self->{value} - $unix_epoch; |
573 |
|
|
my $frac = $self->second_fraction_string . '00000'; |
574 |
|
|
$frac = substr $frac, 1; # remove leading "." |
575 |
|
|
substr ($frac, 4, 0) = '.'; |
576 |
|
|
$frac =~ s/0+\z//; |
577 |
|
|
$frac =~ s/\.\z//; |
578 |
|
|
return $int . $frac; |
579 |
|
|
} # to_html5_number |
580 |
|
|
|
581 |
|
|
sub to_unix_integer ($) { |
582 |
|
|
my $self = shift; |
583 |
|
|
return $self->{value} - $unix_epoch; |
584 |
|
|
} # to_unix_integer |
585 |
|
|
|
586 |
|
|
package Message::Date::DateTime; |
587 |
|
|
push our @ISA, 'Message::Date'; |
588 |
|
|
|
589 |
|
|
## TODO: Implement this module. Use "floating" time_zone such that |
590 |
|
|
## leap seconds are not taken into account. |
591 |
|
|
|
592 |
|
|
1; |