/[suikacvs]/messaging/manakai/lib/Message/Date.pm
Suika

Contents of /messaging/manakai/lib/Message/Date.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.1 - (show annotations) (download)
Mon Dec 15 06:14:48 2008 UTC (15 years, 5 months ago) by wakaba
Branch: MAIN
CVS Tags: HEAD
++ manakai/lib/Message/ChangeLog	15 Dec 2008 06:14:23 -0000
2008-12-14  Wakaba  <wakaba@suika.fam.cx>

	* Date.pm: New module.

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;

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24