Remind::Parser - parse `remind -lp' output


Remind-Parser documentation Contained in the Remind-Parser distribution.

Index


Code Index:

NAME

Top

Remind::Parser - parse `remind -lp' output

SYNOPSIS

Top

    use Remind::Parser;

    $parser = Remind::Parser->new(...);

    $parser->parse(\*STDIN);

    $reminders = $parser->reminders;
    foreach $rem (@$reminders) {
        ($Y, $M, $D) = @$rem{qw(year month day)};
        $descrip = $rem->{'description'};
    }

    $days = $parser->days;
    foreach $day (@$days) {
        $reminders_for_day = $day->{'reminders'};
        foreach $rem (@$reminders_for_day) {
            ...
        }
    }

DESCRIPTION

Top

Remind::Parser parses a stream produced by remind(1) and intended for back-end programs such as rem2ps(1) or wyrd(1).

The input must have been produced by invoking remind -l -p[num]; for details on this format, see rem2ps(1).

PUBLIC METHODS

Top

new(%args)
    $parser = Remind::Parser->new;
    $parser = Remind::Parser->new('strict' => 1);

Create a new parser. The following (key, value) pairs may be supplied; they have the same effect as calling the mutator method with the same name; see below.

strict
strip_times
fill_gaps

strict([boolean])
    $is_strict = $parser->strict;
    $parser->strict(1);  # Be strict
    $parser->strict(0);  # Don't be strict

Get or set the parser's strict property. If strict is set, the parse method will complain about invalid input, e.g., lines of input following the # rem2ps end line.

This option is off by default.

strip_times([boolean])
    $will_strip_times = $parser->strip_times;
    $parser->strip_times(1);  # Strip times
    $parser->strip_times(0);  # Don't strip times

Setting the strip_times option will result in a reminder's time being stripped from the beginning of the reminder. It's much better to invoke remind using -b2 instead if you don't want these times to appear, but this option is available just in case you need it for some reason.

This option is off by default.

fill_gaps([boolean])
    $will_fill_gaps = $parser->fill_gaps;
    $parser->fill_gaps(1);  # Fill gaps
    $parser->fill_gaps(0);  # Don't fill gaps

If fill_gaps is set, then when the days method is called, any days that have no reminders but that fall within the operative date range will be represented in the value returned.

This may also be specified on a case-by-case basis when calling days.

parse($filehandle)
    $reminders = Remind::Parser->parse(\*STDIN);

Parse the contents of a filehandle, returning a reference to a list of reminders. The input must have been produced by invoking remind -l -p[num]; otherwise, it will not be parsed correctly. (If remind's -pa option was used, "pre-notification" reminders are correctly parsed but cannot be distinguished from other reminders.)

Each reminder returned is a hash containing the following elements:

description

The reminder description (taken from the MSG portion of the remind(1) source).

date

The reminder's date, in ISO8601 compact format, e.g., 20080320.

date_time

The reminder's date (and time, if it's a timed event), in ISO8601 compact format, e.g., 20080320 or 20080320T104500. Keep in mind that remind doesn't assume any particular time zone.

year
month
day
day_of_week

The day, month, year, and day of week of the reminder. Days of the week are numbered 1 to 7 and start with Monday.

all_day

If this element is present and has a true value, the reminder is an all-day event. Otherwise, it's a timed event.

hour
minute

The hour and minute of the reminder, if it's a timed reminder. Absent otherwise.

duration

If the reminder has a duration, this is set to a reference to a hash with hours, minutes, and seconds elements with the appropriate values. Otherwise, there is no duration element.

tag

The TAG string from the remind(1) source. Absent if no TAG string was present.

special

The SPECIAL string from the remind(1) source. Absent if no SPECIAL string was present.

line
file

The line number and file name of the file containing the reminder.

event
instance

These two elements, both integers, together uniquely identify a reminder. Multiple reminders that are all triggered from the same line in the same file share the same event identifier but have distinct instance identifiers.

reminders
    $reminders = $parser->reminders;

This method returns a reference to the same array of reminders that was returned by the parse method.

days
    $days = $parser->days;                    # Rely on $parser_fill_gaps
    $days = $parser->days('fill_gaps' => 1);  # Override $parser->fill_gaps
    $days = $parser->days('fill_gaps' => 0);  # Override $parser->fill_gaps

Returns a reference to an array of days for each of which one or more reminders was triggered. (If the fill_gaps option is set, then days that have no reminders but that fall within the operative date range will also be present.)

Each day is represented by a hash with the following elements:

date

The date in YYYYmmdd form.

year
month
day
day_of_week

The date expressed in all the same ways as it is in reminders.

reminders

A reference to an array of reminders for the day. Each reminder is a reference to a hash whose members are as described above. (In fact, each element in reminders is a reference to the same hash found in the return values of the parse and reminders methods.)

BUGS

Top

There are no known bugs. Please report any bugs or feature requests via RT at http://rt.cpan.org/NoAuth/Bugs.html?Queue=Remind-Parser; bugs will be automatically passed on to the author via e-mail.

TO DO

Top

Offer an option to read the reminder's source?

Parse formats other than that produced by remind -l -p[a|num]?

Add an option to skip reminders with unrecognized SPECIALs?

AUTHOR

Top

Paul Hoffman (nkuitse AT cpan DOT org)

COPYRIGHT

Top

SEE ALSO

Top

remind(1), rem2ps(1), wyrd(1)


Remind-Parser documentation Contained in the Remind-Parser distribution.

package Remind::Parser;

use strict;
use warnings;

use vars qw($VERSION);

use Date::DayOfWeek qw(dayofweek);

$VERSION = '0.07';

# --- Constructor

sub new {
    my $cls = shift;
    my $self = bless {
        @_,
    }, $cls;
    return $self->_init;
}

sub _init {
    my ($self) = @_;
    # Nothing to do
    return $self;
}

# --- Accessors

sub reminders { scalar(@_) > 1 ? $_[0]->{'reminders'} = $_[1] : $_[0]->{'reminders'} }
sub strict { scalar(@_) > 1 ? $_[0]->{'strict'} = $_[1] : $_[0]->{'strict'} }
sub strip_times { scalar(@_) > 1 ? $_[0]->{'strip_times'} = $_[1] : $_[0]->{'strip_times'} }
sub fill_gaps { scalar(@_) > 1 ? $_[0]->{'fill_gaps'} = $_[1] : $_[0]->{'fill_gaps'} }

# --- Other public methods

sub parse {
    my ($self, $fh) = @_;
    delete $self->{'days'};  # We'll regenerate later if asked
    my ($file, $line, $loc, %file);
    my ($past_header, $all_done);
    my @reminders;
    my %loc2event;
    my %loc2count;
    my $next_event = 1;
    my $start = <$fh>;
    return [] unless defined $start;
    if ($start !~ /^# rem2ps begin$/) {
        die "First line of input is not the proper header: $_"
            if $self->strict;
    }
    while (<$fh>) {
        chomp;
        if ($all_done) { 
            if ($_ !~ /^# rem2ps begin$/ ) {
                die "Spurious input at end of input: $_"
                    if $self->strict;
                last;
            } 
            else { $past_header = 0 ; $all_done = 0 }
        }
        if (/^# fileinfo (\d+) (.+)/) {
            ($line, $file) = ($1, $2);
            $loc = "$file:$line";
            $past_header = 1;
        }
        elsif ($past_header) {
            # We've skipped past the header
            if (/^# rem2ps end$/) {
                # All done
                $all_done = 1;
            }
            else {
                unless (defined $loc) {
                    die "Input does not contain file and line offsets; you must use option -p with remind";
                }
                my ($date, $special, $tag, $duration, $offset, $description) = split / +/, $_, 6;
                my ($year, $month, $day) = split m{[-/]}, $date;
                if ($self->strip_times && $description =~ s/^((\d\d?):(\d\d)([ap]m) )//) {
                    # Strip the time -- but then restore it if it doesn't match
                    #   the offset in minutes
                    my ($stripped, $H, $M, $pm) = ($1, $2, $3, $4 eq 'pm');
                    $description = $stripped . $description
                        unless $offset == _HMpm2min($H, $M, $pm);
                }
                my $event = $loc2event{$loc} ||= $next_event++;
                my $instance = ++$loc2count{$loc};
                my %reminder = (
                    'event'       => $event,
                    'instance'    => $instance,
                    'file'        => $file,
                    'line'        => $line,
                    'year'        => $year  + 0,
                    'month'       => $month + 0,
                    'day'         => $day   + 0,
                    'description' => $description,
                    $tag eq '*'     ? () : ('tag'     => $tag),
                    $special eq '*' ? () : ('special' => $special),
                );
                $reminder{'date'} = sprintf('%04d%02d%02d', @reminder{qw(year month day)});
                my ($begin, $end);
                if ($offset eq '*') {
                    # Untimed (whole day) reminder
                    $reminder{'all_day'} = 1;
                }
                else {
                    # Timed reminder
                    $reminder{'hour'}   = int($offset / 60);
                    $reminder{'minute'} = $offset % 60;
                    $reminder{'second'} = 0;
                    if ($duration ne '*') {
                        $reminder{'duration'} = {
                            'hours'   => int($duration / 60),
                            'minutes' => $duration % 60,
                            'seconds' => 0,
                        };
                    }
                }
                push @reminders, \%reminder;
            }
        }
    }
    return $self->{'reminders'} = \@reminders;
}

sub days {
    my ($self, %args) = @_;
    return $self->{'days'} if $self->{'days'};
    my ($begin_date, $end_date) = @args{qw(begin end)};
    my $reminders = $self->reminders;
    my %date_info;
    _consolidate_reminders($reminders, \%date_info);
    _sort_date_reminders(\%date_info);
    if (exists $args{'fill_gaps'}) {
        _fill_gaps(\%date_info) if $args{'fill_gaps'};
    }
    elsif ($self->fill_gaps) {
        _fill_gaps(\%date_info);
    }
    if (defined $begin_date) {
        foreach (sort keys %date_info) {
            delete $date_info{$_}
                if $_ lt $begin_date;
        }
    }
    if (defined $end_date) {
        foreach (sort keys %date_info) {
            delete $date_info{$_}
                if $_ gt $end_date;
        }
    }
    return $self->{'days'} = [ map { $date_info{$_} } sort keys %date_info ];
}

sub _HMpm2min {
    my ($H, $M, $pm) = @_;
    my $base = $pm ? 12 * 60 : 0;
    $H = 0 if $H == 12;  # 12:XXam --> 00:XXam, 12:XXpm --> 00:XXpm
    return $base + $H * 60 + $M;
}

# -------------------------------- Functions

sub _consolidate_reminders {
    my ($reminders, $date_info) = @_;
    foreach my $r (@$reminders) {
        my ($year, $month, $day) = @$r{qw/year month day/};
        my $ymd = _format_date($year, $month, $day);
        my $info = $date_info->{$ymd} ||= _norm_reminder({
            'year'  => $year,
            'month' => $month,
            'day'   => $day,
            'reminders' => [],
        });
        delete $date_info->{$ymd}->{'date_time'};
        push @{ $info->{'reminders'} }, $r;
    }
}

sub _sort_date_reminders {
    my ($date_info) = @_;
    foreach my $ymd (keys %$date_info) {
        my $reminders = $date_info->{$ymd}->{'reminders'};
        foreach my $r (@$reminders) {
            $r->{'date_time'} = $ymd;
            $r->{'date_time'} .= sprintf('T%02d%02d00', $r->{'hour'}, $r->{'minute'})
                unless $r->{'all_day'};
        }
        # Sort reminders within the date
        @$reminders = sort { $a->{'date_time'} cmp $b->{'date_time'} } @$reminders;
    }
}

sub _fill_gaps {
    my ($date_info) = @_;
    my @dates = sort keys %$date_info;
    my $iter = _iter_dates($dates[0], $dates[-1]);
    while (my $dt = $iter->()) {
        if (!exists $date_info->{$dt}) {
            my ($y, $m, $d) = _parse_date($dt);
            my $ymd = _format_date($y, $m, $d);
            $date_info->{$dt} = _norm_reminder({
                'year'      => $y,
                'month'     => $m,
                'day'       => $d,
                'reminders' => [],
            });
            delete $date_info->{$ymd}->{'date_time'};
        }
    }
}

BEGIN {
    # Adapted from Date::ISO8601 by Zefram
    my @days_in_month = (undef, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    sub _is_leap_year {
        my ($y) = @_;
        return $y % 4 == 0 && ($y % 100 != 0 || $y % 400 == 0);
    }
    sub _last_day_in_month {
        my ($y, $m) = @_;
        #die unless $m >= 1 && $m <= 12;
        return $m == 2
            ? ( _is_leap_year($y) ? 29 : 28 )
            : $days_in_month[$m];
    }
}

sub _norm_reminder {
    my ($r) = @_;
    my ($y, $m, $d) = @$r{qw(year month day)};
    $r->{'date'} = _format_date($y, $m, $d);
    $r->{'day_of_week'} = dayofweek($d, $m, $y) || 7;  # Sun --> 7, not 0
    if ($r->{'all_day'}) {
        $r->{'date_time'} = $r->{'date'};
    }
    else {
        my ($H, $M, $S) = @$r{qw(hour minute second)};
        $r->{'date_time'} = $r->{'date'} . _format_time($H, $M, $S);
    }
    return $r;
}

sub _norm_date {
    _format_date(_parse_date($_[0]))
}

sub _parse_date {
    my ($dt) = @_;
    $dt =~ m{^(\d\d\d\d)[-/]?(\d\d)[-/]?(\d\d)} or die;
    return ($1, $2, $3);
}

sub _format_date {
    my ($y, $m, $d) = @_;
    return sprintf('%04d%02d%02d', $y, $m, $d);
}

sub _format_time {
    my ($H, $M, $S) = @_;
    return '' unless defined $H;
    return sprintf('T%02d%02d%02d', $H || 0, $M || 0, $S || 0);
}

sub _day_after {
    my ($dt) = @_;
    my ($y, $m, $d) = _parse_date($dt);
    my $n = _last_day_in_month($y, $m);
    if ($d == $n) {
        if ($m == 12) {
            # YYYY-12-31 --> (YYYY+1)-01-01
            $y++;
            $m = 1;
            $d = 1;
        }
        else {
            # YYYY-MM-nn --> YYYY-(MM+1)-01
            $m++;
            $d = 1;
        }
    }
    else {
        # YYYY-MM-DD --> YYYY-MM-(DD+1)
        $d++;
    }
    return _format_date($y, $m, $d);
}

sub _day_before {
    my ($dt) = @_;
    my ($y, $m, $d) = _parse_date($dt);
    if ($d == 1) {
        if ($m == 1) {
            $y--;
            $m = 12;
            $d = 1;
        }
        else {
            $d = _last_day_in_month($y, --$m);
        }
    }
    else {
        $d--;
    }
    return _format_date($y, $m, $d);
}

sub _iter_dates {
    my ($dt1, $dtn) = @_;
    return if $dt1 > $dtn;
    my ($y, $m, $d)    = _parse_date($dt1);
    my ($yn, $mn, $dn) = _parse_date($dtn);
    my $dlim = _last_day_in_month($y, $m);
    return sub {
        my $dt = _format_date($y, $m, $d++);
        return if $dt gt $dtn;
        if ($d > $dlim) {
            $d = 1;
            $m++;
            if ($m > 12) {
                $y++;
                $m = 1;
            }
            $dlim = _last_day_in_month($y, $m);
        }
        return $dt;
    }
}

1;

# vim:fenc=utf-8:et:sw=4:ts=4:sts=4