File Coverage

blib/lib/Remind/Parser.pm
Criterion Covered Total %
statement 93 168 55.3
branch 30 88 34.0
condition 14 30 46.6
subroutine 17 27 62.9
pod 7 7 100.0
total 161 320 50.3


line stmt bran cond sub pod time code
1             package Remind::Parser;
2              
3 2     2   1617 use strict;
  2         4  
  2         74  
4 2     2   9 use warnings;
  2         4  
  2         70  
5              
6 2     2   18 use vars qw($VERSION);
  2         4  
  2         126  
7              
8 2     2   1667 use Date::DayOfWeek qw(dayofweek);
  2         4545  
  2         3524  
9              
10             $VERSION = '0.08';
11              
12             my %dow_cache;
13              
14             # --- Constructor
15              
16             sub new {
17 2     2 1 888 my $cls = shift;
18 2         7 my $self = bless {
19             @_,
20             }, $cls;
21 2         11 return $self->_init;
22             }
23              
24             sub _init {
25 2     2   4 my ($self) = @_;
26             # Nothing to do
27 2         12 return $self;
28             }
29              
30             # --- Accessors
31              
32 1 50   1 1 4 sub reminders { scalar(@_) > 1 ? $_[0]->{'reminders'} = $_[1] : $_[0]->{'reminders'} }
33 0 0   0 1 0 sub strict { scalar(@_) > 1 ? $_[0]->{'strict'} = $_[1] : $_[0]->{'strict'} }
34 14 50   14 1 61 sub strip_times { scalar(@_) > 1 ? $_[0]->{'strip_times'} = $_[1] : $_[0]->{'strip_times'} }
35 1 50   1 1 6 sub fill_gaps { scalar(@_) > 1 ? $_[0]->{'fill_gaps'} = $_[1] : $_[0]->{'fill_gaps'} }
36              
37             # --- Other public methods
38              
39             sub parse {
40 1     1 1 549 my ($self, $fh) = @_;
41 1         4 delete $self->{'days'}; # We'll regenerate later if asked
42 1         2 my ($file, $line, $loc, %file);
43 0         0 my ($past_header, $all_done);
44 0         0 my @reminders;
45 0         0 my %loc2event;
46 0         0 my %loc2count;
47 1         2 my $next_event = 1;
48 1         17 my $start = <$fh>;
49 1 50       6 return [] unless defined $start;
50 1 50       7 if ($start !~ /^# rem2ps begin$/) {
51 0 0       0 die "First line of input is not the proper header: $_"
52             if $self->strict;
53             }
54 1         8 while (<$fh>) {
55 33         36 chomp;
56 33 50       56 if ($all_done) {
57 0 0       0 if ($_ !~ /^# rem2ps begin$/ ) {
58 0 0       0 die "Spurious input at end of input: $_"
59             if $self->strict;
60 0         0 last;
61             }
62 0         0 else { $past_header = 0 ; $all_done = 0 }
  0         0  
63             }
64 33 100       110 if (/^# fileinfo (\d+) (.+)/) {
    100          
65 14         38 ($line, $file) = ($1, $2);
66 14         22 $loc = "$file:$line";
67 14         42 $past_header = 1;
68             }
69             elsif ($past_header) {
70             # We've skipped past the header
71 15 100       24 if (/^# rem2ps end$/) {
72             # All done
73 1         14 $all_done = 1;
74             }
75             else {
76 14 50       29 unless (defined $loc) {
77 0         0 die "Input does not contain file and line offsets; you must use option -p with remind";
78             }
79 14         88 my ($date, $special, $tag, $duration, $offset, $description) = split / +/, $_, 6;
80 14         52 my ($year, $month, $day) = split m{[-/]}, $date;
81 14 50 33     36 if ($self->strip_times && $description =~ s/^((\d\d?):(\d\d)([ap]m) )//) {
82             # Strip the time -- but then restore it if it doesn't match
83             # the offset in minutes
84 0         0 my ($stripped, $H, $M, $pm) = ($1, $2, $3, $4 eq 'pm');
85 0 0       0 $description = $stripped . $description
86             unless $offset == _HMpm2min($H, $M, $pm);
87             }
88 14   66     64 my $event = $loc2event{$loc} ||= $next_event++;
89 14         25 my $instance = ++$loc2count{$loc};
90 14 100       131 my %reminder = (
    50          
91             'event' => $event,
92             'instance' => $instance,
93             'file' => $file,
94             'line' => $line,
95             'year' => $year + 0,
96             'month' => $month + 0,
97             'day' => $day + 0,
98             'description' => $description,
99             $tag eq '*' ? () : ('tag' => $tag),
100             $special eq '*' ? () : ('special' => $special),
101             );
102 14         34 $reminder{'date'} = _format_date(@reminder{qw(year month day)});
103 14         16 my ($begin, $end);
104 14 100       22 if ($offset eq '*') {
105             # Untimed (whole day) reminder
106 7         8 $reminder{'all_day'} = 1;
107             }
108             else {
109             # Timed reminder
110 7         37 my $H = $reminder{'hour'} = int($offset / 60);
111 7         14 my $M = $reminder{'minute'} = $offset % 60;
112 7         7 my $S = $reminder{'second'} = 0;
113 7 100       584 if ($duration ne '*') {
114 2         12 $reminder{'duration'} = {
115             'hours' => int($duration / 60),
116             'minutes' => $duration % 60,
117             'seconds' => 0,
118             };
119             }
120             }
121 14         28 push @reminders, _normalize_date(\%reminder);
122             }
123             }
124             }
125 1         9 return $self->{'reminders'} = \@reminders;
126             }
127              
128             sub days {
129 1     1 1 367 my ($self, %args) = @_;
130 1 50       4 return $self->{'days'} if $self->{'days'};
131 1         3 my ($begin_date, $end_date) = @args{qw(begin end)};
132 1         3 my $reminders = $self->reminders;
133 1         2 my %date_info;
134 1         3 _consolidate_reminders($reminders, \%date_info);
135 1         4 _sort_date_reminders(\%date_info);
136 1 50       7 if (exists $args{'fill_gaps'}) {
    50          
137 0 0       0 _fill_gaps(\%date_info) if $args{'fill_gaps'};
138             }
139             elsif ($self->fill_gaps) {
140 0         0 _fill_gaps(\%date_info);
141             }
142 1 50       6 if (defined $begin_date) {
143 0         0 foreach (sort keys %date_info) {
144 0 0       0 delete $date_info{$_}
145             if $_ lt $begin_date;
146             }
147             }
148 1 50       3 if (defined $end_date) {
149 0         0 foreach (sort keys %date_info) {
150 0 0       0 delete $date_info{$_}
151             if $_ gt $end_date;
152             }
153             }
154 1         7 return $self->{'days'} = [ map { $date_info{$_} } sort keys %date_info ];
  6         18  
155             }
156              
157             sub _HMpm2min {
158 0     0   0 my ($H, $M, $pm) = @_;
159 0 0       0 my $base = $pm ? 12 * 60 : 0;
160 0 0       0 $H = 0 if $H == 12; # 12:XXam --> 00:XXam, 12:XXpm --> 00:XXpm
161 0         0 return $base + $H * 60 + $M;
162             }
163              
164             # -------------------------------- Functions
165              
166             sub _consolidate_reminders {
167 1     1   2 my ($reminders, $date_info) = @_;
168 1         2 foreach my $r (@$reminders) {
169 14         32 my ($ymd, $year, $month, $day) = @$r{qw/date year month day/};
170 14   66     96 my $info = $date_info->{$ymd} ||= _normalize_date({
171             'date' => $ymd,
172             'year' => $year,
173             'month' => $month,
174             'day' => $day,
175             'reminders' => [],
176             });
177 14         29 delete $date_info->{$ymd}->{'date_time'};
178 14         16 push @{ $info->{'reminders'} }, $r;
  14         34  
179             }
180             }
181              
182             sub _sort_date_reminders {
183 1     1   3 my ($date_info) = @_;
184 1         5 foreach my $ymd (keys %$date_info) {
185 6         11 my $reminders = $date_info->{$ymd}->{'reminders'};
186             # Sort reminders within the date
187 6         19 @$reminders = sort { $a->{'date_time'} cmp $b->{'date_time'} } @$reminders;
  11         30  
188             }
189             }
190              
191             sub _fill_gaps {
192 0     0   0 my ($date_info) = @_;
193 0         0 my @dates = sort keys %$date_info;
194 0         0 my $iter = _iter_dates($dates[0], $dates[-1]);
195 0         0 while (my $dt = $iter->()) {
196 0 0       0 if (!exists $date_info->{$dt}) {
197 0         0 my ($y, $m, $d) = _parse_date($dt);
198 0         0 my $ymd = _format_date($y, $m, $d);
199 0         0 $date_info->{$dt} = _normalize_date({
200             'date' => $ymd,
201             'year' => $y,
202             'month' => $m,
203             'day' => $d,
204             'reminders' => [],
205             });
206 0         0 delete $date_info->{$ymd}->{'date_time'};
207             }
208             }
209             }
210              
211             BEGIN {
212             # Adapted from Date::ISO8601 by Zefram
213 2     2   1643 my @days_in_month = (undef, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
214             sub _is_leap_year {
215 0     0   0 my ($y) = @_;
216 0   0     0 return $y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0);
217             }
218             sub _last_day_in_month {
219 0     0   0 my ($y, $m) = @_;
220             #die unless $m >= 1 && $m <= 12;
221 0 0       0 return $m == 2
    0          
222             ? ( _is_leap_year($y) ? 29 : 28 )
223             : $days_in_month[$m];
224             }
225             }
226              
227             sub _normalize_date {
228 20     20   24 my ($r) = @_;
229 20         39 my ($y, $m, $d) = @$r{qw(year month day)};
230 20   33     49 my $ymd = $r->{'date'} ||= _format_date($y, $m, $d);
231 20   100     101 $r->{'day_of_week'} ||= $dow_cache{$ymd} ||= dayofweek($d, $m, $y) || 7; # Sun --> 7, not 0
      66        
      33        
232 20 100       230 if ($r->{'all_day'}) {
233 7         16 $r->{'date_time'} = $r->{'date'};
234             }
235             else {
236 13         20 my ($H, $M, $S) = @$r{qw(hour minute second)};
237 13         29 $r->{'date_time'} = $r->{'date'} . _format_time($H, $M, $S);
238             }
239 20         92 return $r;
240             }
241              
242             sub _parse_date {
243 0     0   0 my ($dt) = @_;
244 0 0       0 $dt =~ m{^(\d\d\d\d)[-/]?(\d\d)[-/]?(\d\d)} or die;
245 0         0 return ($1, $2, $3);
246             }
247              
248             sub _format_date {
249 14     14   20 my ($y, $m, $d) = @_;
250 14         54 return sprintf('%04d%02d%02d', $y, $m, $d);
251             }
252              
253             sub _format_time {
254 13     13   16 my ($H, $M, $S) = @_;
255 13 100       44 return '' unless defined $H;
256 7   100     63 return sprintf('T%02d%02d%02d', $H, $M || 0, $S || 0);
      50        
257             }
258              
259             sub _day_after {
260 0     0     my ($dt) = @_;
261 0           my ($y, $m, $d) = _parse_date($dt);
262 0 0 0       if ($d < 28 || $d != _last_day_in_month($y, $m)) {
    0          
263             # YYYY-MM-DD --> YYYY-MM-(DD+1)
264 0           $d++;
265             }
266             elsif ($m == 12) {
267             # YYYY-12-31 --> (YYYY+1)-01-01
268 0           $y++;
269 0           $m = 1;
270 0           $d = 1;
271             }
272             else {
273             # YYYY-MM-nn --> YYYY-(MM+1)-01
274 0           $m++;
275 0           $d = 1;
276             }
277 0           return _format_date($y, $m, $d);
278             }
279              
280             sub _day_before {
281 0     0     my ($dt) = @_;
282 0           my ($y, $m, $d) = _parse_date($dt);
283 0 0         if ($d > 1) {
    0          
284 0           $d--;
285             }
286             elsif ($m == 1) {
287 0           $y--;
288 0           $m = 12;
289 0           $d = 31;
290             }
291             else {
292 0           $d = _last_day_in_month($y, --$m);
293             }
294 0           return _format_date($y, $m, $d);
295             }
296              
297             sub _iter_dates {
298 0     0     my ($dt1, $dtn) = @_;
299 0 0         return if $dt1 > $dtn;
300 0           my ($y, $m, $d) = _parse_date($dt1);
301 0           my ($yn, $mn, $dn) = _parse_date($dtn);
302 0           my $dlim = _last_day_in_month($y, $m);
303             return sub {
304 0     0     my $dt = _format_date($y, $m, $d++);
305 0 0         return if $dt gt $dtn;
306 0 0         if ($d > $dlim) {
307 0           $d = 1;
308 0           $m++;
309 0 0         if ($m > 12) {
310 0           $y++;
311 0           $m = 1;
312             }
313 0           $dlim = _last_day_in_month($y, $m);
314             }
315 0           return $dt;
316             }
317 0           }
318              
319             1;
320              
321             =pod
322              
323             =head1 NAME
324              
325             Remind::Parser - parse `remind -lp' output
326              
327             =head1 SYNOPSIS
328              
329             use Remind::Parser;
330              
331             $parser = Remind::Parser->new(...);
332              
333             $parser->parse(\*STDIN);
334              
335             $reminders = $parser->reminders;
336             foreach $rem (@$reminders) {
337             ($Y, $M, $D) = @$rem{qw(year month day)};
338             $descrip = $rem->{'description'};
339             }
340              
341             $days = $parser->days;
342             foreach $day (@$days) {
343             $reminders_for_day = $day->{'reminders'};
344             foreach $rem (@$reminders_for_day) {
345             ...
346             }
347             }
348              
349             =head1 DESCRIPTION
350              
351             B parses a stream produced by B and intended for
352             back-end programs such as B or B.
353              
354             The input must have been produced by invoking BIB<]>;
355             for details on this format, see L.
356              
357             =head1 PUBLIC METHODS
358              
359             =over 4
360              
361             =item B(I<%args>)
362              
363             $parser = Remind::Parser->new;
364             $parser = Remind::Parser->new('strict' => 1);
365              
366             Create a new parser. The following (key, value) pairs may be supplied; they
367             have the same effect as calling the mutator method with the same name; see
368             below.
369              
370             =over 4
371              
372             =item B
373              
374             =item B
375              
376             =item B
377              
378             =back
379              
380             =item B([I])
381              
382             $is_strict = $parser->strict;
383             $parser->strict(1); # Be strict
384             $parser->strict(0); # Don't be strict
385              
386             Get or set the parser's B property. If B is set, the B
387             method will complain about invalid input, e.g., lines of input following the
388             C<# rem2ps end> line.
389              
390             This option is off by default.
391              
392             =item B([I])
393              
394             $will_strip_times = $parser->strip_times;
395             $parser->strip_times(1); # Strip times
396             $parser->strip_times(0); # Don't strip times
397              
398             Setting the B option will result in a reminder's time being
399             stripped from the beginning of the reminder. It's much better to invoke remind
400             using B<-b2> instead if you don't want these times to appear, but this option
401             is available just in case you need it for some reason.
402              
403             This option is off by default.
404              
405             =item B([I])
406              
407             $will_fill_gaps = $parser->fill_gaps;
408             $parser->fill_gaps(1); # Fill gaps
409             $parser->fill_gaps(0); # Don't fill gaps
410              
411             If B is set, then when the B method is called, any days that
412             have no reminders but that fall within the operative date range will be
413             represented in the value returned.
414              
415             This may also be specified on a case-by-case basis when calling B.
416              
417             =item B(I<$filehandle>)
418              
419             $reminders = Remind::Parser->parse(\*STDIN);
420              
421             Parse the contents of a filehandle, returning a reference to a list of
422             reminders. The input must have been produced by invoking
423             BIB<]>; otherwise, it will not be parsed correctly.
424             (If remind's B<-pa> option was used, "pre-notification" reminders are correctly
425             parsed but cannot be distinguished from other reminders.)
426              
427             Each reminder returned is a hash containing the following elements:
428              
429             =over 4
430              
431             =item B
432              
433             The reminder description (taken from the B portion of the remind(1)
434             source).
435              
436             =item B
437              
438             The reminder's date, in ISO8601 C format, e.g., C<20080320>.
439              
440             =item B
441              
442             The reminder's date (and time, if it's a timed event), in ISO8601 C
443             format, e.g., C<20080320> or C<20080320T104500>. Keep in mind that remind
444             doesn't assume any particular time zone.
445              
446             =item B
447              
448             =item B
449              
450             =item B
451              
452             =item B
453              
454             The day, month, year, and day of week of the reminder. Days of the week are
455             numbered 1 to 7 and start with Monday.
456              
457             =item B
458              
459             If this element is present and has a true value, the reminder is an all-day
460             event. Otherwise, it's a timed event.
461              
462             =item B
463              
464             =item B
465              
466             The hour and minute of the reminder, if it's a timed reminder. Absent
467             otherwise.
468              
469             =item B
470              
471             If the reminder has a duration, this is set to a reference to a hash with
472             B, B, and B elements with the appropriate values.
473             Otherwise, there is no B element.
474              
475             =item B
476              
477             The B string from the remind(1) source. Absent if no B string was
478             present.
479              
480             =item B
481              
482             The B string from the remind(1) source. Absent if no B string
483             was present.
484              
485             =item B
486              
487             =item B
488              
489             The line number and file name of the file containing the reminder.
490              
491             =item B
492              
493             =item B
494              
495             These two elements, both integers, together uniquely identify a reminder.
496             Multiple reminders that are all triggered from the same line in the same file
497             share the same B identifier but have distinct B identifiers.
498              
499             =back
500              
501             =item B
502              
503             $reminders = $parser->reminders;
504              
505             This method returns a reference to the same array of reminders that was returned
506             by the B method.
507              
508             =item B
509              
510             $days = $parser->days; # Rely on $parser_fill_gaps
511             $days = $parser->days('fill_gaps' => 1); # Override $parser->fill_gaps
512             $days = $parser->days('fill_gaps' => 0); # Override $parser->fill_gaps
513              
514             Returns a reference to an array of days for each of which one or more reminders
515             was triggered. (If the B option is set, then days that have no
516             reminders but that fall within the operative date range will also be present.)
517              
518             Each day is represented by a hash with the following elements:
519              
520             =over 4
521              
522             =item B
523              
524             The date in YYYYmmdd form.
525              
526             =item B
527              
528             =item B
529              
530             =item B
531              
532             =item B
533              
534             The date expressed in all the same ways as it is in reminders.
535              
536             =item B
537              
538             A reference to an array of reminders for the day. Each reminder is a reference
539             to a hash whose members are as described above. (In fact, each element in
540             B is a reference to the same hash found in the return values of the
541             B and B methods.)
542              
543             =back
544              
545             =back
546              
547             =head1 BUGS
548              
549             There are no known bugs. Please report any bugs or feature requests via RT at
550             L; bugs will be
551             automatically passed on to the author via e-mail.
552              
553             =head1 TO DO
554              
555             Offer an option to read the reminder's source?
556              
557             Parse formats other than that produced by C?
558              
559             Add an option to skip reminders with unrecognized Bs?
560              
561             =head1 AUTHOR
562              
563             Paul Hoffman (nkuitse AT cpan DOT org)
564              
565             =head1 COPYRIGHT
566              
567             Copyright 2007-2009 Paul M. Hoffman.
568              
569             This is free software, and is made available under the same terms as Perl
570             itself.
571              
572             =head1 SEE ALSO
573              
574             L,
575             L,
576             L
577              
578             =cut
579              
580             # vim:fenc=utf-8:et:sw=4:ts=4:sts=4