view release on metacpan or search on metacpan
lib/App/JobLog/Command/summary.pm view on Meta::CPAN
}
if ( $t2->{hour} < $t1->{hour}
|| $t2->{minute} < $t1->{minute}
|| $t2->{second} < $t1->{second} )
{
if ( $t2->{suffix} && $t2->{suffix} eq 'x' ) {
$t2->{hour} += 12;
}
else {
carp '"' . $time
. '" invalid time expression: endpoints out of order';
}
}
delete $t1->{suffix}, delete $t2->{suffix};
return { start => $t1, end => $t2 };
}
}
sub usage_desc { '%c ' . __PACKAGE__->name . ' %o [<date or date range>]' }
sub abstract {
lib/App/JobLog/Log.pm view on Meta::CPAN
sub all_events {
my ($self) = @_;
# reopen log in sequential reading mode
$self->[IO] = io log;
my ( @events, $previous );
while ( my $line = $self->[IO]->getline ) {
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
$previous->end = $ll->time if $previous;
if ( $ll->is_beginning ) {
$previous = App::JobLog::Log::Event->new($ll);
push @events, $previous;
}
else {
$previous = undef;
}
}
}
lib/App/JobLog/Log.pm view on Meta::CPAN
sub first_event {
my ($self) = @_;
return $self->[FIRST_EVENT], $self->[FIRST_INDEX] if $self->[FIRST_EVENT];
my $io = $self->[IO];
my ( $i, $e ) = 0;
while ( $i <= $#$io ) {
my $line = $io->[$i];
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
if ($e) {
$e->end = $ll->time;
last;
}
else {
$e = App::JobLog::Log::Event->new($ll);
$self->[FIRST_INDEX] = $i;
}
}
$i++;
lib/App/JobLog/Log.pm view on Meta::CPAN
return $self->[LAST_EVENT], $self->[LAST_INDEX] if $self->[LAST_EVENT];
my $io = $self->[IO];
# was hoping to use IO::All::backwards for this, but seems to be broken
# uncertain how to handle utf8 issue with File::ReadBackwards
my @lines;
my $i = $#$io;
for ( ; $i >= 0 ; $i-- ) {
my $line = $self->[IO][$i];
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
push @lines, $ll;
last if $ll->is_beginning;
}
}
return () unless @lines;
my $e = App::JobLog::Log::Event->new( pop @lines );
$e->end = $lines[0]->time if @lines;
$self->[LAST_EVENT] = $e;
$self->[LAST_INDEX] = $i;
return $e, $i;
lib/App/JobLog/Log.pm view on Meta::CPAN
return $self->all_events if $c1 && $c2;
$start = $start_event->start if $c1;
$end = $end_event->end if $c2;
# matters are simple if what we want is at the start of the log
if ($c1) {
my ( $line, $previous, @events );
while ( my $line = $io->getline ) {
chomp $line;
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
$previous->end = $end if $previous->is_open;
last;
}
if ( $previous && $previous->is_open ) {
$previous->end = $ll->time;
}
if ( $ll->is_beginning ) {
$previous = App::JobLog::Log::Event->new($ll);
push @events, $previous;
lib/App/JobLog/Log.pm view on Meta::CPAN
# matters are likewise simple if what we want is at the end of the log
if ($c2) {
# must restart io
$io = $self->[IO] = io log;
$io->backwards;
my ( $line, $previous, @events );
while ( my $line = $io->getline ) {
chomp $line;
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
my $e;
if ( $ll->is_beginning ) {
$e = App::JobLog::Log::Event->new($ll);
$e->end = $previous->time if $previous;
unshift @events, $e;
}
if ( DateTime->compare( $ll->time, $start ) <= 0 ) {
$e->start = $start if $e;
last;
}
lib/App/JobLog/Log.pm view on Meta::CPAN
# scan it sequentially
sub _scan_from {
my ( $self, $i, $start, $end ) = @_;
my $io = $self->[IO];
# collect events
my ( $previous, @events );
for my $index ( $i .. $#$io ) {
my $line = $io->[$index];
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
if ($previous) {
$previous->end = $ll->time if $previous->is_open;
push @events, $previous
if DateTime->compare( $start, $previous->end ) < 0;
}
if ( $ll->is_beginning ) {
last if DateTime->compare( $ll->time, $end ) >= 0;
$previous = App::JobLog::Log::Event->new($ll);
}
else {
lib/App/JobLog/Log.pm view on Meta::CPAN
sub _scan_for_previous {
my ( $self, $i, $e ) = @_;
my $io = $self->[IO];
# collect events
my ( $previous, $previous_index );
OUTER: {
for my $index ( $i .. $#$io ) {
my $line = $io->[$index];
my $ll = App::JobLog::Log::Line->parse($line);
if ( $ll->is_endpoint ) {
$previous->end = $ll->time if $previous && $previous->is_open;
if ( $ll->time > $e ) {
last if $previous;
$i--;
redo OUTER;
}
if ( $ll->is_beginning ) {
$previous = App::JobLog::Log::Event->new($ll);
$previous_index = $index;
}
lib/App/JobLog/Log/Line.pm view on Meta::CPAN
sub is_end { $_[0]->{done} }
sub is_note { $_[0]->{note} }
sub is_event { $_[0]->{time} }
sub is_endpoint { $_[0]->{time} && !$_[0]->{note} }
sub is_comment { exists $_[0]->{comment} }
sub tags_unspecified { $_[0]->{tags_unspecified} }
sub is_blank {
!( $_[0]->is_malformed || $_[0]->is_comment || $_[0]->is_event );
lib/App/JobLog/Log/Line.pm view on Meta::CPAN
=head2 is_note
Whether the line is a note rather than a terminus of an event or
a comment or blank line.
=head2 is_event
Whether line has a time stamp.
=head2 is_endpoint
Whether the line has a timestamp marking the beginning or end of a logged
interval.
=head2 is_comment
Whether line represents a comment in the log.
=head2 tags_unspecified
lib/App/JobLog/TimeGrammar.pm view on Meta::CPAN
$h1->set(%t1);
$h2->set(%t2);
if ( $h1 > $h2 ) {
if ( $h1->year == $h2->year
&& $h1->month == $h2->month
&& $h1->day == $h2->day
&& $h2->hour < 12
&& $s2 eq 'x' )
{
# we inferred the 12 hour period of the second endpoint incorrectly;
# it was in the evening rather than morning
$h2->add( hours => 12 );
}
else {
croak "dates in \"$phrase\" are out of order";
}
}
return $h1, $h2, $count == 2;
}
}
lib/App/JobLog/TimeGrammar.pm view on Meta::CPAN
else {
return 'years' => 1 if exists $h->{month};
return 'weeks' => 1 if exists $h->{day};
return 'months' => 1;
}
}
}
# produces interpretation of date expression consistent with a fixed start date
sub fixed_start {
my ( $h1, $h2, $two_endpoints ) = @_;
$h1 = fix_date( $h1, 1 );
unless ( $two_endpoints || $h2->{type} ne 'numeric' ) {
return $h1, $h1->clone if defined $h2->{day};
return $h1, $h1->clone->add( years => 1 )->subtract( days => 1 );
}
if ( is_fixed($h2) ) {
$h2 = fix_date($h2);
}
else {
my ( $unit, $amt ) = time_unit($h2);
$h2 = decontextualized_date($h2);
$h2 = adjust_weekday( $h2, $h1 ) unless ref $h2 eq 'DateTime';
lib/App/JobLog/TimeGrammar.pm view on Meta::CPAN
while (@$units) {
my $i = @$units;
my $u = pop @$units;
$h->{$u} = $i;
}
}
# produces interpretation of date expression such that neither date ends after
# the present
sub before_now {
my ( $h1, $h2, $two_endpoints ) = @_;
infer_missing( $h1, $h2 ) if $two_endpoints;
my $now = today;
my ( $u1, $amt1, $u2, $amt2 ) = ( time_unit($h1), time_unit($h2) );
( $h1, $h2 ) =
( decontextualized_date( $h1, 1 ), decontextualized_date($h2) );
$h2 = adjust_weekday( $h2, $now ) unless ref $h2 eq 'DateTime';
$h1 = adjust_weekday( $h1, $now ) unless ref $h1 eq 'DateTime';
while ( $now < $h2 ) {
$h2->subtract( $u2 => $amt2 );
}
while ( $h2 < $h1 ) {
$h1->subtract( $u1 => $amt1 );
}
if ($two_endpoints) {
# move the two dates as close together as possible
while ( $h1 < $h2 ) {
$h2->subtract( $u2 => $amt2 );
}
$h2->add( $u2 => $amt2 );
}
return $h1, $h2;
}
lib/App/JobLog/TimeGrammar.pm view on Meta::CPAN
use Modern::Perl;
use DateTime;
use App::JobLog::Time qw(tz);
use App::JobLog::TimeGrammar qw(parse);
# for demonstration purposes we modify "today"
$App::JobLog::Time::today =
DateTime->new( year => 2011, month => 2, day => 17, time_zone => tz );
for my $phrase ( 'Monday until the end of the week', 'Tuesday at 9:00 p.m.' ) {
my ( $start, $end, $endpoints ) = parse($phrase);
say $phrase;
say "$start - $end; both endpoints specified? "
. ( $endpoints ? 'yes' : 'no' );
}
produces
Monday until the end of the week
2011-02-14T00:00:00 - 2011-02-20T23:59:59; both endpoints specified? yes
Tuesday at 9:00 p.m.
2011-02-08T21:00:00 - 2011-02-15T23:59:59; both endpoints specified? no
=head1 DESCRIPTION
C<App::JobLog::TimeGrammar> converts natural language time expressions into pairs of
C<DateTime> objects representing intervals. This requires disambiguating ambiguous
terms such as 'yesterday', whose interpretation varies from day to day, and 'Friday', whose
interpretation must be fixed by some frame of reference. The heuristic used by this code
is to look first for a fixed date, either a fully specified date such as 2011/2/17 or
one fixed relative to the current moment such as 'now'. If such a date is present in the time
expression it determines the context for the other date, if it is present. Otherwise
it is assumed that the closest appropriate pair of dates immediately before the current
moment are intended.
Given a pair consisting of fixed and an ambiguous date, we assume the ambiguous date has the
sense such that it is ordered correctly relative to the fixed date and the interval between
them is minimized.
If the time expression provides no time of day, such as 8:00, it is assumed that the first moment
intended is the first second of the first day and the last moment is the last second of the second
day. If no second date is provided the endpoint of the interval will be the last moment of the single
date specified. If a larger time period such as week, month, or year is specified, e.g., 'last week', the
first moment is the first second in the period and the last moment is the last second.
If you wish to parse a single date, not an interval, you can ignore the second date, though you should
check the third value returned by C<parse>, whether an interval was parsed.
C<parse> will croak if it cannot parse the expression given.
=head2 Time Grammar
lib/App/JobLog/TimeGrammar.pm view on Meta::CPAN
=head2 daytime
Parses a time expression such as "11:00" or "8:15:40 pm". Returns a map from
C<hour>, C<minute>, C<second>, and C<suffix> to the appropriate value, where
'x' represents an ambiguous suffix.
=head2 parse
This function (it isn't actually a method) is the essential function of this module.
It takes a time expression and returns a pair of C<DateTime> objects representing the
endpoints of the corresponding interval and whether it was given a pair of dates.
If you are parsing an expression defining a point rather than an interval you should be
safe ignoring the second endpoing, but you should check the count to make sure the expression
didn't provide a second endpoint.
This code croaks when it cannot parse the expression, so exception handling is recommended.
=head1 SEE ALSO
L<App::JobLog::Command::parse>
=head1 AUTHOR
David F. Houghton <dfhoughton@gmail.com>