/[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 - (hide annotations) (download)
Mon Dec 15 06:14:48 2008 UTC (15 years, 11 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 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;

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24