App-JobLog

 view release on metacpan or  search on metacpan

lib/App/JobLog/Log.pm  view on Meta::CPAN

use constant WINDOW   => 30;
use constant LOW_LIM  => 1 / 10;
use constant HIGH_LIM => 1 - LOW_LIM;

# some indices
use constant IO          => 0;
use constant FIRST_EVENT => 1;
use constant LAST_EVENT  => 2;
use constant FIRST_INDEX => 3;
use constant LAST_INDEX  => 4;

# timestamp format
use constant TS => '%Y/%m/%d';


sub new {
    my $class = shift;
    $class = ref $class if ref $class;

    # touch log into existence
    unless ( -e log ) {
        init_file log;
        my $fh = FileHandle->new( log, 'w' );
        $fh->close;
    }

    # using an array to make things a little snappier
    my $self = bless [], $class;
    $self->[IO] = io log;
    return $self;
}


sub lines {
  [ shift->[IO]->getlines ];
}


sub all_taglines {
    my ($self) = @_;

    # reopen log in sequential reading mode
    $self->[IO] = io log;
    my (@lines);
    while ( my $line = $self->[IO]->getline ) {
        my $ll = App::JobLog::Log::Line->parse($line);
        push @lines, $ll if $ll->is_beginning;
    }
    return \@lines;
}


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;
            }
        }
    }
    return \@events;
}


sub all_notes {
    my ($self) = @_;

    # reopen log in sequential reading mode
    $self->[IO] = io log;
    my @notes;
    while ( my $line = $self->[IO]->getline ) {
        my $ll = App::JobLog::Log::Line->parse($line);
        push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
    }
    return \@notes;
}


sub validate {
    my ($self) = @_;
    my ( $i, $previous_event ) = (0);
    my $errors = 0;
    while ( my $line = $self->[IO][$i] ) {
        my $ll = App::JobLog::Log::Line->parse($line);
        if ( $ll->is_malformed ) {
            $errors++;
            print STDERR "line $i -- '$line' -- is malformed; commenting out\n";
            splice @{ $self->[IO] }, $i, 0,
              App::JobLog::Log::Line->new( comment => 'ERROR; malformed line' );
            $self->[IO][ ++$i ] = $ll->comment_out;
        }
        elsif ( $ll->is_event ) {
            if ($previous_event) {
                if ( DateTime->compare( $previous_event->time, $ll->time ) > 0 )
                {
                    $errors++;
                    print STDERR
"line $i -- '$line' -- is out of order relative to the last event; commenting out\n";
                    splice @{ $self->[IO] }, $i, 0,
                      App::JobLog::Log::Line->new(
                        comment => 'ERROR; dates out of order' );
                    $self->[IO][ ++$i ] = $ll->comment_out;
                }
                elsif ( $previous_event->is_end && $ll->is_end ) {
                    $errors++;
                    print STDERR
"line $i -- '$line' -- specifies the end of a task not yet begun; commenting out\n";
                    splice @{ $self->[IO] }, $i, 0,
                      App::JobLog::Log::Line->new( comment =>
                          'ERROR; task end without corresponding beginning' );
                    $self->[IO][ ++$i ] = $ll->comment_out;
                }
                else {
                    $previous_event = $ll;
                }
            }
            elsif ( $ll->is_end ) {
                $errors++;
                print STDERR
"line $i -- '$line' -- specifies the end of a task not yet begun; commenting out\n";
                splice @{ $self->[IO] }, $i, 0,
                  App::JobLog::Log::Line->new( comment =>
                      'ERROR; task end without corresponding beginning' );
                $self->[IO][ ++$i ] = $ll->comment_out;
            }
            else {
                $previous_event = $ll;
            }
        }
        $i++;
    }
    return $errors;
}


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++;
    }
    $self->[FIRST_EVENT] = $e;
    return $e, $self->[FIRST_INDEX];
}


sub last_ts {
    my ($self) = @_;
    my $io     = $self->[IO];
    my $i      = $#$io;
    for ( ; $i >= 0 ; $i-- ) {
        my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
        return ( $ll->time, $i ) if $ll->is_event;
    }
    return;
}


sub first_ts {
    my ($self) = @_;
    my $io     = $self->[IO];
    my $i      = 0;
    for ( my $lim = $#$io ; $i <= $lim ; $i++ ) {
        my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
        return ( $ll->time, $i ) if $ll->is_event;
    }
    return;
}


sub last_event {
    my ($self) = @_;
    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;
}


sub last_note {
    my ($self) = @_;
    my $io = $self->[IO];
    for ( my $i = $#$io ; $i >= 0 ; $i-- ) {
        my $line = $io->[$i];
        my $ll   = App::JobLog::Log::Line->parse($line);
        return ( App::JobLog::Log::Note->new($ll), $i ) if $ll->is_note;
    }
    return ();
}


sub reverse_iterator {
    my ( $self, $event ) = @_;
    if ( ref $event ) {
        if ( $event->isa('DateTime') ) {
            my $events =
              $self->find_events( $event, $self->first_event->start );
            if (@$events) {
                $event = $events->[$#$events];
            }
            else {
                $event = undef;
            }
        }
    }
    else {
        ($event) = $self->last_event;
    }
    return sub { }
      unless $event;
    my ( undef, $index, $io ) =
      ( $self->find_previous( $event->start ), $self->[IO] );
    return sub {
        return () unless $event;
        my $e        = $event;
        my $end_time = $event->start;
        $event = undef;
        while ( --$index >= 0 ) {
            my $line = $io->[$index];
            my $ll   = App::JobLog::Log::Line->parse($line);
            if ( $ll->is_beginning ) {
                $event = App::JobLog::Log::Event->new($ll);
                $event->end = $end_time;
                last;
            }
            elsif ( $ll->is_end ) {
                $end_time = $ll->time;
            }
        }
        return $e;
    };
}


sub find_events {
    my ( $self, $start, $end ) = @_;
    my $io = $self->[IO];
    my ( $end_event, $bottom, $start_event, $top ) =
      ( $self->last_event, $self->first_event );

    # if the log is empty, return empty list
    return [] unless $start_event && $end_event;

    # if the log concerns events before the time in question, return empty list
    return []
      unless $end_event->is_open
          || DateTime->compare( $start, $end_event->end ) < 0;

    # likewise if it concerns events after
    return [] if DateTime->compare( $start_event->start, $end ) > 0;

    # narrow time range to that in log
    my $c1 = DateTime->compare( $start, $start_event->start ) <= 0;
    my $c2 =
      $end_event->is_open
      ? DateTime->compare( $end, $end_event->start ) >= 0
      : DateTime->compare( $end, $end_event->end ) >= 0;
    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;
                }
            }
        }
        return \@events;
    }

    # 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;
                }
                $previous = $ll;
            }
        }
        return \@events;
    }

    # otherwise, do binary search for first event in range
    my ( undef, $i ) = $self->find_previous($start);
    return $self->_scan_from( $i, $start, $end );
}


sub find_notes {
    my ( $self, $start, $end ) = @_;
    my $io = $self->[IO];
    my ( $end_time, $bottom, $start_time, $top ) =
      ( $self->last_ts, $self->first_ts );

    # if the log is empty, return empty list
    return [] unless $start_time && $end_time;

    # if the log concerns events before the time in question, return empty list
    return []
      unless DateTime->compare( $start, $end_time ) <= 0;

    # likewise if it concerns events after
    return [] if DateTime->compare( $start_time, $end ) > 0;

    # narrow time range to that in log
    my $c1 = DateTime->compare( $start, $start_time ) <= 0;
    my $c2 = DateTime->compare( $end,   $end_time ) >= 0;
    return $self->all_notes if $c1 && $c2;
    $start = $start_time if $c1;
    $end   = $end_time   if $c2;

    # matters are simple if what we want is at the start of the log
    if ($c1) {
        my ( $line, @notes );
        while ( my $line = $io->getline ) {
            chomp $line;
            my $ll = App::JobLog::Log::Line->parse($line);
            if ( $ll->is_event ) {
                if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
                    last;
                }
                push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
            }
        }
        return \@notes;
    }

lib/App/JobLog/Log.pm  view on Meta::CPAN

        }
    }
}


sub _find_previous_note {
    my ( $self, $e, $eb, $bottom, $et, $top ) = @_;
    my $io = $self->[IO];

    # binary search for first note in range
    my $previous_index;
  OUTER: while (1) {
        return $self->_scan_for_previous_note( $top, $e )
          if $bottom - $top + 1 <= WINDOW / 2;
        my $index = _estimate_index( $top, $bottom, $et, $eb, $e );
        if ( defined $previous_index && $previous_index == $index ) {

            # search was too clever by half; we've entered an infinite loop
            return $self->_scan_for_previous_note( $top, $e );
        }
        $previous_index = $index;
        my $event;
        for my $i ( $index .. $#$io ) {
            my $line = $io->[$i];
            my $ll   = App::JobLog::Log::Line->parse($line);
            if ( $ll->is_event ) {
                for ( DateTime->compare( $ll->time, $e ) ) {
                    when ( $_ < 0 ) {
                        $top = $i;
                        $et  = $ll->time;
                        next OUTER;
                    }
                    when ( $_ > 0 ) {
                        $bottom = $i;
                        $eb     = $ll->time;
                        next OUTER;
                    }
                    default {

                        # found beginning!!
                        # this should happen essentially never
                        return $self->_scan_for_previous_note( $i, $e );
                    }
                }
            }
        }
    }
}

# now that we're close to the section of the log we want, we
# 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 {
                $previous = undef;
            }
        }
    }
    push @events, $previous
      if $previous
          && $previous->is_open
          && DateTime->compare( $previous->start, $end ) < 0;

    # return only overlap
    my @return = map { $_->overlap( $start, $end ) } @events;
    return \@return;
}

sub _scan_for_note_from {
    my ( $self, $i, $start, $end ) = @_;
    my $io = $self->[IO];

    # collect notes
    my @notes;
    for my $index ( $i .. $#$io ) {
        my $line = $io->[$index];
        my $ll   = App::JobLog::Log::Line->parse($line);
        if ( $ll->is_event ) {
            last if $ll->time > $end;
            if ( $ll->is_note && $ll->time >= $start ) {
                push @notes, App::JobLog::Log::Note->new($ll);
            }
        }
    }
    return \@notes;
}

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;
                }
            }
        }
    }
    return $previous, $previous_index;
}

sub _scan_for_previous_note {
    my ( $self, $i, $e ) = @_;
    my $io = $self->[IO];

    # collect events
    my ( $previous, $previous_index );
    for my $index ( $i .. $#$io ) {
        my $line = $io->[$index];
        my $ll   = App::JobLog::Log::Line->parse($line);
        if ( $ll->is_event ) {
            last if $ll->time > $e;
            if ( $ll->is_note ) {
                $previous       = App::JobLog::Log::Note->new($ll);
                $previous_index = $index;
            }
        }
    }
    return $previous_index // $i;
}

# your generic O(log_n) complexity bisecting search
sub _estimate_index {
    my ( $top, $bottom, $et, $eb, $s ) = @_;
    my $delta = $bottom - $top + 1;
    my $i;
    if ( $delta > WINDOW ) {
        my $d1       = $s->epoch - $et->epoch;
        my $d2       = $eb->epoch - $et->epoch;
        my $fraction = $d1 / $d2;
        if ( $fraction < LOW_LIM ) {
            $fraction = LOW_LIM;
        }
        elsif ( $fraction > HIGH_LIM ) {
            $fraction = HIGH_LIM;
        }
        $i = sprintf '%.0f', $delta * $fraction;
    }
    else {
        $i = sprintf '%.0f', $delta / 2;
    }
    $i ||= 1;
    return $top + $i;
}



( run in 0.688 second using v1.01-cache-2.11-cpan-d8267643d1d )