File Coverage

blib/lib/DateTime/Format/Alami.pm
Criterion Covered Total %
statement 239 337 70.9
branch 94 126 74.6
condition 26 42 61.9
subroutine 33 47 70.2
pod 3 28 10.7
total 395 580 68.1


line stmt bran cond sub pod time code
1             package DateTime::Format::Alami;
2              
3             our $DATE = '2016-06-30'; # DATE
4             our $VERSION = '0.13'; # VERSION
5              
6 4     4   2095 use 5.014000;
  4         18  
7 4     4   14 use strict;
  4         5  
  4         77  
8 4     4   14 use warnings;
  4         5  
  4         101  
9 4     4   2712 use Log::Any::IfLOG '$log';
  4         37  
  4         32  
10              
11 4     4   171 use Role::Tiny;
  4         5  
  4         17  
12              
13             my @short_mons = qw(jan feb mar apr may jun jul aug sep oct nov dec);
14             my @dow = qw(monday tuesday wednesday thursday friday saturday sunday);
15              
16             requires 'o_num';
17             requires '_parse_num';
18              
19             requires 'w_year';
20             requires 'w_month';
21             requires 'w_week';
22             requires 'w_day';
23             requires 'w_hour';
24             requires 'w_minute';
25             requires 'w_second';
26              
27             requires "w_$_" for @short_mons;
28             requires "w_$_" for @dow;
29              
30             requires 'p_now';
31             requires 'p_today';
32             requires 'p_yesterday';
33             requires 'p_tomorrow';
34             requires 'p_dateymd';
35             requires 'o_date';
36             requires 'p_dur_ago';
37             requires 'p_dur_later';
38             requires 'p_which_dow';
39             requires 'p_time';
40             requires 'p_date_time';
41              
42             our ($m, $o);
43             sub new {
44 7     7 1 25 my $class = shift;
45 7 50       21 if ($class eq __PACKAGE__) {
46 0         0 die "Use one of the DateTime::Format::Alami::* instead, ".
47             "e.g. DateTime::Format::Alami::EN";
48             }
49 7         13 my $self = bless {}, $class;
50 4     4   988 no strict 'refs';
  4         10  
  4         2060  
51 7 50 33     7 unless (${"$class\::RE_DT"} && ${"$class\::RE_DUR"}) {
  7         47  
  7         39  
52 0         0 require Class::Inspector;
53 0         0 require Data::Graph::Util;
54              
55 0         0 my $meths = Class::Inspector->methods($class);
56 0         0 my %pats; # key = "p_..."
57             my %pat_lengths; # key = "p_..."
58 0         0 my %graph;
59 0         0 for my $meth (@$meths) {
60 0 0       0 next unless $meth =~ /^(odur|o|pdur|p)_/;
61 0         0 my $pat = $self->$meth;
62 0         0 my $is_p = $meth =~ /^p_/;
63 0         0 my $is_pdur = $meth =~ /^pdur_/;
64 0         0 $pat =~ s/<(\w+)>/push @{$graph{$meth}}, $1; "(?\&$1)"/eg;
  0         0  
  0         0  
  0         0  
65 0         0 my $action_meth = $meth;
66 0 0       0 if ($is_pdur) { $action_meth =~ s/^pdur_/adur_/ } else { $action_meth =~ s/^p_/a_/ }
  0         0  
  0         0  
67             #my $before_meth = $meth; $before_meth =~ s/^p_/before_p_/;
68             #$before_meth = undef unless $is_p && $self->can($before_meth);
69             $pat = join(
70             "",
71             "(",
72             #($before_meth ? "(?{ ".($ENV{DEBUG} ? "say \"invoking $before_meth()\";" : "")."\$DateTime::Format::Alami::o->$before_meth(\$DateTime::Format::Alami::m) })" : ""),
73             ($is_p || $is_pdur ? "\\b $pat \\b" : $pat), ")",
74              
75             # we capture ourselves instead of relying on named capture
76             # because subpattern capture are discarded
77             "(?{ \$DateTime::Format::Alami::m->{$meth} = \$^N })",
78              
79 0 0 0     0 ($is_p || $is_pdur ? "(?{ ".($ENV{DEBUG} ? "say \"invoking $action_meth(\$^N)\";" : "")."\$DateTime::Format::Alami::o->{_pat} = \"$meth\"; \$DateTime::Format::Alami::o->$action_meth(\$DateTime::Format::Alami::m) })" : ""),
    0 0        
    0          
80             );
81 0         0 $pats{$meth} = $pat;
82 0         0 $pat_lengths{$meth} = length($pat);
83             }
84 0         0 my @pat_names_by_deps = Data::Graph::Util::toposort(\%graph);
85 0         0 my %pat_name_dep_orders = map { $pat_names_by_deps[$_] => $_ }
  0         0  
86             0..$#pat_names_by_deps;
87 0         0 my @pat_names = sort {(
88             ($pat_name_dep_orders{$a} // 9999) <=>
89             ($pat_name_dep_orders{$b} // 9999)
90             ||
91 0 0 0     0 $pat_lengths{$b} <=> $pat_lengths{$a}) } keys %pats;
      0        
92 0 0       0 my $nl = $ENV{DEBUG} ? "\n" : "";
93             my $re_dt = join(
94             "",
95             "(?&top)", $nl,
96             #"(?&p_dateymd)", $nl, # testing
97             "(?(DEFINE)", $nl,
98             "(?", join("|",
99 0         0 map {"(?&$_)"} grep {/^p_/} @pat_names), ")$nl",
  0         0  
100 0         0 (map { "(?<$_> $pats{$_})$nl" } grep {/^(o|p)_/} @pat_names),
  0         0  
  0         0  
101             ")", # end of define
102             );
103             my $re_dur = join(
104             "",
105             "(?&top)", $nl,
106             #"(?&pdur_dur)", $nl, # testing
107             "(?(DEFINE)", $nl,
108             "(?", join("|",
109 0         0 map {"(?&$_)"} grep {/^pdur_/} @pat_names), ")$nl",
  0         0  
110 0         0 (map { "(?<$_> $pats{$_})$nl" } grep {/^(odur|pdur)_/} @pat_names),
  0         0  
  0         0  
111             ")", # end of define
112             );
113             {
114 4     4   51 use re 'eval';
  4         4  
  4         1201  
  0         0  
115 0         0 ${"$class\::RE_DT"} = qr/$re_dt/ix;
  0         0  
116 0         0 ${"$class\::RE_DUR"} = qr/$re_dur/ix;
  0         0  
117             }
118             }
119 7 50       8 unless (${"$class\::MAPS"}) {
  7         23  
120 0         0 my $maps = {};
121             # month names -> num
122             {
123 0         0 my $i = 0;
124 0         0 for my $m (@short_mons) {
125 0         0 ++$i;
126 0         0 my $meth = "w_$m";
127 0         0 for (@{ $self->$meth }) {
  0         0  
128 0         0 $maps->{months}{$_} = $i;
129             }
130             }
131             }
132             # day-of-week names -> num (monday=1, sunday=7)
133             {
134 0         0 my $i = 0;
  0         0  
  0         0  
135 0         0 for my $m (@dow) {
136 0         0 ++$i;
137 0         0 my $meth = "w_$m";
138 0         0 for (@{ $self->$meth }) {
  0         0  
139 0         0 $maps->{dow}{$_} = $i;
140             }
141             }
142             }
143 0         0 ${"$class\::MAPS"} = $maps;
  0         0  
144             }
145              
146             # _time_zone is old name (<= 0.11) will be removed later
147 7   33     44 $self->{time_zone} //= $self->{_time_zone};
148              
149 7         12 $self;
150             }
151              
152             sub _reset {
153 112     112   107 my $self = shift;
154 112         152 undef $self->{_pat};
155 112         210 undef $self->{_dt};
156 112         115 undef $self->{_uses_time};
157             }
158              
159             sub parse_datetime {
160 4     4   17 no strict 'refs';
  4         3  
  4         2143  
161              
162             # we require DateTime here, for all the a_* methods
163 97     97 1 19833 require DateTime;
164              
165 97         148020 my ($self, $str, $opts) = @_;
166              
167             # allow calling as static method
168 97 100       238 unless (ref $self) { $self = $self->new }
  2         10  
169              
170 97   100     168 $opts //= {};
171 97   100     365 $opts->{format} //= 'DateTime';
172             #$opts->{prefers} //= 'nearest';
173 97   100     269 $opts->{returns} //= 'first';
174              
175 97 100       270 local $self->{time_zone} = $opts->{time_zone} if $opts->{time_zone};
176              
177             # we need /o to avoid repeated regcomp, but we need to make it work with all
178             # subclasses, so we use eval() here.
179 97 100       77 unless (defined *{ref($self).'::_code_match_dt'}) {
  97         454  
180 4     112   377 *{ref($self).'::_code_match_dt'} = eval "sub { \$_[0] =~ /(\$".ref($self)."::RE_DT)/go; \$1 }";
  4         23  
  112         3439  
  112         13048  
181 4 50       12 die if $@;
182             }
183              
184 97         113 $o = $self;
185 97         92 my @res;
186 97         75 while (1) {
187 112         212 $o->_reset;
188 112         113 $m = {};
189 112 100       222 my $match = &{ref($self).'::_code_match_dt'}($str) or last;
  112         3218  
190 98 100       383 $o->{_dt}->truncate(to=>'day') unless $o->{_uses_time};
191             my $res = {
192             verbatim => $match,
193             pattern => $o->{_pat},
194 98         11320 pos => pos($str) - length($match),
195             m => {%$m},
196             };
197 98 100       260 $res->{uses_time} = $o->{_uses_time} ? 1:0;
198 98         134 $res->{DateTime} = $o->{_dt};
199             $res->{epoch} = $o->{_dt}->epoch if
200 98 100 100     518 $opts->{format} eq 'combined' || $opts->{format} eq 'epoch';
201 98         148 push @res, $res;
202 98 100       269 last if $opts->{returns} eq 'first';
203             }
204              
205 97 100       274 die "Can't parse date '$str'" unless @res;
206              
207 88 100       174 @res = ($res[-1]) if $opts->{returns} eq 'last';
208              
209 88 100       162 if ($opts->{returns} =~ /\A(?:all_cron|earliest|latest)\z/) {
210             # sort chronologically, note that by this time the DateTime module
211             # should already have been loaded
212             @res = sort {
213 3         19 DateTime->compare($a->{DateTime}, $b->{DateTime})
214 9         411 } @res;
215             }
216              
217 88 100       264 if ($opts->{format} eq 'DateTime') {
    100          
    100          
218 80         87 @res = map { $_->{DateTime} } @res;
  80         298  
219             } elsif ($opts->{format} eq 'epoch') {
220 1         3 @res = map { $_->{epoch} } @res;
  1         8  
221             } elsif ($opts->{format} eq 'verbatim') {
222 6         10 @res = map { $_->{verbatim} } @res;
  14         71  
223             }
224              
225 88 100       439 if ($opts->{returns} =~ /\A(?:all|all_cron)\z/) {
    100          
    50          
226 2         9 return \@res;
227             } elsif ($opts->{returns} =~ /\A(?:first|earliest)\z/) {
228 84         340 return $res[0];
229             } elsif ($opts->{returns} =~ /\A(?:last|latest)\z/) {
230 2         16 return $res[-1];
231             } else {
232 0         0 die "Unknown returns option '$opts->{returns}'";
233             }
234             }
235              
236             sub _reset_dur {
237 32     32   24 my $self = shift;
238 32         48 undef $self->{_pat};
239 32         35 undef $self->{_dtdur};
240             }
241              
242             sub parse_datetime_duration {
243             # we require DateTime here, for all the adur_* methods
244 17     17 1 12111 require DateTime;
245 17         41 require DateTime::Duration;
246              
247 4     4   16 no strict 'refs';
  4         4  
  4         3305  
248              
249 17         25 my ($self, $str, $opts) = @_;
250              
251             # allow calling as static method
252 17 100       43 unless (ref $self) { $self = $self->new }
  2         4  
253              
254 17   100     52 $opts //= {};
255 17   100     47 $opts->{format} //= 'Duration';
256 17   100     47 $opts->{returns} //= 'first';
257              
258             # we need /o to avoid repeated regcomp, but we need to make it work with all
259             # subclasses, so we use eval() here.
260 17 100       14 unless (defined *{ref($self).'::_code_match_dur'}) {
  17         79  
261 4     32   327 *{ref($self).'::_code_match_dur'} = eval "sub { \$_[0] =~ /(\$".ref($self)."::RE_DUR)/go; \$1 }";
  4         27  
  32         679  
  32         1741  
262 4 50       14 die if $@;
263             }
264              
265 17         39 $o = $self;
266 17         25 my @res;
267 17         14 while (1) {
268 32         58 $o->_reset_dur;
269 32         33 $m = {};
270 32 100       53 my $match = &{ref($self).'::_code_match_dur'}($str) or last;
  32         680  
271             my $res = {
272             verbatim => $match,
273             pattern => $o->{_pat},
274 24         117 pos => pos($str) - length($match),
275             m => {%$m},
276             };
277 24         98 $res->{Duration} = $o->{_dtdur};
278 24 100 100     105 if ($opts->{format} eq 'combined' || $opts->{format} eq 'seconds') {
279 2         3 my $d = $o->{_dtdur};
280             $res->{seconds} =
281 2         5 $d->years * 365.25*86400 +
282             $d->months * 30.4375*86400 +
283             $d->weeks * 7*86400 +
284             $d->days * 86400 +
285             $d->hours * 3600 +
286             $d->minutes * 60 +
287             $d->seconds +
288             $d->nanoseconds * 1e-9;
289             }
290 24         229 push @res, $res;
291 24 100       53 last if $opts->{returns} eq 'first';
292             }
293              
294 17 100       58 die "Can't parse duration" unless @res;
295              
296 14 100       34 @res = ($res[-1]) if $opts->{returns} eq 'last';
297              
298             # XXX support returns largest, smallest, all_sorted
299 14 100       38 if ($opts->{returns} =~ /\A(?:all_sorted|largest|smallest)\z/) {
300 3         11 my $base_dt = DateTime->now;
301             # sort from smallest to largest
302             @res = sort {
303 3         720 DateTime::Duration->compare($a->{Duration}, $b->{Duration}, $base_dt)
  9         3401  
304             } @res;
305             }
306              
307 14 100       1654 if ($opts->{format} eq 'Duration') {
    100          
    100          
308 6         9 @res = map { $_->{Duration} } @res;
  6         25  
309             } elsif ($opts->{format} eq 'seconds') {
310 1         2 @res = map { $_->{seconds} } @res;
  1         3  
311             } elsif ($opts->{format} eq 'verbatim') {
312 6         8 @res = map { $_->{verbatim} } @res;
  14         37  
313             }
314              
315 14 100       73 if ($opts->{returns} =~ /\A(?:all|all_sorted)\z/) {
    100          
    50          
316 2         7 return \@res;
317             } elsif ($opts->{returns} =~ /\A(?:first|smallest)\z/) {
318 10         32 return $res[0];
319             } elsif ($opts->{returns} =~ /\A(?:last|largest)\z/) {
320 2         6 return $res[-1];
321             } else {
322 0         0 die "Unknown returns option '$opts->{returns}'";
323             }
324             }
325              
326 0     0 0 0 sub o_dayint { "(?:[12][0-9]|3[01]|0?[1-9])" }
327              
328 0     0 0 0 sub o_monthint { "(?:0?[1-9]|1[012])" }
329              
330 0     0 0 0 sub o_yearint { "(?:[0-9]{4}|[0-9]{2})" }
331              
332 0     0 0 0 sub o_hour { "(?:[0-9][0-9]?)" }
333              
334 0     0 0 0 sub o_minute { "(?:[0-9][0-9]?)" }
335              
336 0     0 0 0 sub o_second { "(?:[0-9][0-9]?)" }
337              
338             sub o_monthname {
339 0     0 0 0 my $self = shift;
340             "(?:" . join(
341             "|",
342 0         0 (map {my $meth="w_$_"; @{ $self->$meth }} @short_mons)
  0         0  
  0         0  
  0         0  
343             ) . ")";
344             }
345              
346             sub o_dow {
347 0     0 0 0 my $self = shift;
348             "(?:" . join(
349             "|",
350 0         0 (map {my $meth="w_$_"; @{ $self->$meth }} @dow)
  0         0  
  0         0  
  0         0  
351             ) . ")";
352             }
353              
354             sub o_durwords {
355 5     5 0 7 my $self = shift;
356             "(?:" . join(
357             "|",
358 5         17 @{ $self->w_year }, @{ $self->w_month }, @{ $self->w_week },
  5         14  
  5         17  
359 5         16 @{ $self->w_day },
360 5         7 @{ $self->w_hour }, @{ $self->w_minute }, @{ $self->w_second },
  5         13  
  5         15  
  5         15  
361             ) . ")";
362             }
363              
364             sub o_dur {
365 0     0 0 0 my $self = shift;
366 0         0 "(?:(" . $self->o_num . "\\s*" . $self->o_durwords . "\\s*(?:,\\s*)?)+)";
367             }
368              
369             sub odur_dur {
370 0     0 0 0 my $self = shift;
371 0         0 $self->o_dur;
372             }
373              
374             sub pdur_dur {
375 0     0 0 0 my $self = shift;
376 0         0 "(?:)";
377             }
378              
379             # durations less than a day
380             sub o_timedurwords {
381 0     0 0 0 my $self = shift;
382             "(?:" . join(
383             "|",
384 0         0 @{ $self->w_hour }, @{ $self->w_minute }, @{ $self->w_second },
  0         0  
  0         0  
  0         0  
385             ) . ")";
386             }
387              
388             sub o_timedur {
389 0     0 0 0 my $self = shift;
390 0         0 "(?:(" . $self->o_num . "\\s*" . $self->o_timedurwords . "\\s*(?:,\\s*)?)+)";
391             }
392              
393             sub _parse_dur {
394 4     4   1787 use experimental 'smartmatch';
  4         9717  
  4         15  
395              
396 30     30   30 my ($self, $str) = @_;
397              
398             #say "D:dur=$str";
399 30         24 my %args;
400 30 100       56 unless ($self->{_cache_re_parse_dur}) {
401 5         21 my $o_num = $self->o_num;
402 5         17 my $o_dw = $self->o_durwords;
403 5         501 $self->{_cache_re_parse_dur} = qr/($o_num)\s*($o_dw)/ix;
404             }
405 30 100       64 unless ($self->{_cache_w_second}) {
406 5         16 $self->{_cache_w_second} = $self->w_second;
407 5         13 $self->{_cache_w_minute} = $self->w_minute;
408 5         13 $self->{_cache_w_hour} = $self->w_hour;
409 5         12 $self->{_cache_w_day} = $self->w_day;
410 5         11 $self->{_cache_w_week} = $self->w_week;
411 5         13 $self->{_cache_w_month} = $self->w_month;
412 5         12 $self->{_cache_w_year} = $self->w_year;
413             }
414 30         198 while ($str =~ /$self->{_cache_re_parse_dur}/g) {
415 40         95 my ($n, $unit) = ($1, $2);
416 40         85 $n = $self->_parse_num($n);
417 40 100       796 if ($unit ~~ $self->{_cache_w_second}) {
    100          
    100          
    50          
    0          
    0          
    0          
418 6         9 $args{seconds} = $n;
419 6         19 $self->{_uses_time} = 1;
420             } elsif ($unit ~~ $self->{_cache_w_minute}) {
421 8         12 $args{minutes} = $n;
422 8         29 $self->{_uses_time} = 1;
423             } elsif ($unit ~~ $self->{_cache_w_hour}) {
424 9         12 $args{hours} = $n;
425 9         50 $self->{_uses_time} = 1;
426             } elsif ($unit ~~ $self->{_cache_w_day}) {
427 17         73 $args{days} = $n;
428             } elsif ($unit ~~ $self->{_cache_w_week}) {
429 0         0 $args{weeks} = $n;
430             } elsif ($unit ~~ $self->{_cache_w_month}) {
431 0         0 $args{months} = $n;
432             } elsif ($unit ~~ $self->{_cache_w_year}) {
433 0         0 $args{years} = $n;
434             }
435             }
436 30         127 DateTime::Duration->new(%args);
437             }
438              
439             sub _now_if_unset {
440 14     14   15 my $self = shift;
441 14 100       44 $self->a_now unless $self->{_dt};
442             }
443              
444             sub _today_if_unset {
445 0     0   0 my $self = shift;
446 0 0       0 $self->a_today unless $self->{_dt};
447             }
448              
449             sub a_now {
450 23     23 0 24 my $self = shift;
451             $self->{_dt} = DateTime->now(
452 23         99 (time_zone => $self->{time_zone}) x !!defined($self->{time_zone}),
453             );
454 23         4489 $self->{_uses_time} = 1;
455             }
456              
457             sub a_today {
458 194     194 0 174 my $self = shift;
459             $self->{_dt} = DateTime->today(
460 194         644 (time_zone => $self->{time_zone}) x !!defined($self->{time_zone}),
461             );
462 194         80344 $self->{_uses_time} = 0;
463             }
464              
465             sub a_yesterday {
466 10     10 0 10 my $self = shift;
467 10         16 $self->a_today;
468 10         28 $self->{_dt}->subtract(days => 1);
469             }
470              
471             sub a_tomorrow {
472 8     8 0 8 my $self = shift;
473 8         11 $self->a_today;
474 8         23 $self->{_dt}->add(days => 1);
475             }
476              
477             sub a_dateymd {
478 143     143 0 151 my ($self, $m) = @_;
479 143         216 $self->a_today;
480 143 100       347 if (defined $m->{o_yearint}) {
481 75         73 my $year;
482 75 100       148 if (length($m->{o_yearint}) == 2) {
483 54         120 my $start_of_century_year = int($self->{_dt}->year / 100) * 100;
484 54         264 $year = $start_of_century_year + $m->{o_yearint};
485             } else {
486 21         28 $year = $m->{o_yearint};
487             }
488 75         165 $self->{_dt}->set_year($year);
489             }
490 143 50       18269 if (defined $m->{o_dayint}) {
491 143         386 $self->{_dt}->set_day($m->{o_dayint});
492             }
493 143 100       31562 if (defined $m->{o_monthint}) {
494 92         201 $self->{_dt}->set_month($m->{o_monthint});
495             }
496 143 100       22955 if (defined $m->{o_monthname}) {
497 4     4   2114 no strict 'refs';
  4         5  
  4         294  
498 57         45 my $maps = ${ ref($self) . '::MAPS' };
  57         186  
499 57         233 $self->{_dt}->set_month($maps->{months}{lc $m->{o_monthname}});
500             }
501             }
502              
503             sub a_which_dow {
504 4     4   16 no strict 'refs';
  4         4  
  4         1275  
505              
506 27     27 0 30 my ($self, $m) = @_;
507 27         43 $self->a_today;
508 27         70 my $dow_num = $self->{_dt}->day_of_week;
509              
510 27         74 my $maps = ${ ref($self) . '::MAPS' };
  27         94  
511 27         58 my $wanted_dow_num = $maps->{dow}{lc $m->{o_dow} };
512              
513 27         69 $self->{_dt}->add(days => ($wanted_dow_num-$dow_num));
514              
515 27 100       8994 if ($m->{offset}) {
516 12         39 $self->{_dt}->add(days => (7*$m->{offset}));
517             }
518             }
519              
520             sub adur_dur {
521 24     24 0 26 my ($self, $m) = @_;
522 24         53 $self->{_dtdur} = $self->_parse_dur($m->{odur_dur});
523             }
524              
525             sub a_dur_ago {
526 3     3 0 6 my ($self, $m) = @_;
527 3         9 $self->a_now;
528 3         11 my $dur = $self->_parse_dur($m->{o_dur});
529 3         195 $self->{_dt}->subtract_duration($dur);
530             }
531              
532             sub a_dur_later {
533 3     3 0 5 my ($self, $m) = @_;
534 3         12 $self->a_now;
535 3         11 my $dur = $self->_parse_dur($m->{o_dur});
536 3         169 $self->{_dt}->add_duration($dur);
537             }
538              
539             sub a_time {
540 14     14 0 18 my ($self, $m) = @_;
541 14         30 $self->_now_if_unset;
542 14         195 $self->{_uses_time} = 1;
543 14         18 my $hour = $m->{o_hour};
544 14 100       31 if ($m->{o_ampm}) {
545 3 100 66     19 $hour += 12 if lc($m->{o_ampm}) eq 'pm' && $hour < 12;
546 3 50 66     16 $hour = 0 if lc($m->{o_ampm}) eq 'am' && $hour == 12;
547             }
548 14         50 $self->{_dt}->set_hour($hour);
549 14         2913 $self->{_dt}->set_minute($m->{o_minute});
550 14   100     2813 $self->{_dt}->set_second($m->{o_second} // 0);
551             }
552              
553             sub a_date_time {
554 8     8 0 268 my ($self, $m) = @_;
555             }
556              
557             1;
558             # ABSTRACT: Parse human date/time expression (base class)
559              
560             __END__