File Coverage

blib/lib/App/JobLog/Log.pm
Criterion Covered Total %
statement 429 456 94.0
branch 194 232 83.6
condition 50 74 67.5
subroutine 42 46 91.3
pod 20 20 100.0
total 735 828 88.7


line stmt bran cond sub pod time code
1             package App::JobLog::Log;
2             $App::JobLog::Log::VERSION = '1.040';
3             # ABSTRACT: the code that lets us interact with the log
4              
5              
6 4     4   2727 use Modern::Perl;
  4         7  
  4         22  
7 4     4   469 use App::JobLog::Config qw(log init_file);
  4         7  
  4         197  
8 4     4   20 use App::JobLog::Log::Line;
  4         8  
  4         22  
9 4     4   3161 use IO::All -utf8;
  4         44486  
  4         47  
10 4     4   284 use autouse 'Carp' => qw(carp);
  4         8  
  4         27  
11 4     4   435 use autouse 'App::JobLog::Time' => qw(now);
  4         39  
  4         17  
12 4         26 use Class::Autouse qw(
13             App::JobLog::Log::Event
14             App::JobLog::Log::Note
15             DateTime
16             FileHandle
17 4     4   241 );
  4         7  
18 4     4   2894 no if $] >= 5.018, warnings => "experimental::smartmatch";
  4         23  
  4         28  
19              
20             # some stuff useful for searching log
21 4     4   688 use constant WINDOW => 30;
  4         8  
  4         266  
22 4     4   19 use constant LOW_LIM => 1 / 10;
  4         7  
  4         191  
23 4     4   18 use constant HIGH_LIM => 1 - LOW_LIM;
  4         5  
  4         167  
24              
25             # some indices
26 4     4   20 use constant IO => 0;
  4         7  
  4         161  
27 4     4   20 use constant FIRST_EVENT => 1;
  4         5  
  4         160  
28 4     4   18 use constant LAST_EVENT => 2;
  4         7  
  4         163  
29 4     4   18 use constant FIRST_INDEX => 3;
  4         7  
  4         159  
30 4     4   17 use constant LAST_INDEX => 4;
  4         6  
  4         163  
31              
32             # timestamp format
33 4     4   17 use constant TS => '%Y/%m/%d';
  4         8  
  4         24392  
34              
35              
36             sub new {
37 70     70 1 72448 my $class = shift;
38 70 50       267 $class = ref $class if ref $class;
39              
40             # touch log into existence
41 70 100       431 unless ( -e log ) {
42 3         14 init_file log;
43 3         243 my $fh = FileHandle->new( log, 'w' );
44 3         469 $fh->close;
45             }
46              
47             # using an array to make things a little snappier
48 70         411 my $self = bless [], $class;
49 70         339 $self->[IO] = io log;
50 70         50802 return $self;
51             }
52              
53              
54             sub lines {
55 0     0 1 0 [ shift->[IO]->getlines ];
56             }
57              
58              
59             sub all_taglines {
60 1     1 1 3 my ($self) = @_;
61              
62             # reopen log in sequential reading mode
63 1         5 $self->[IO] = io log;
64 1         189 my (@lines);
65 1         20 while ( my $line = $self->[IO]->getline ) {
66 5         945 my $ll = App::JobLog::Log::Line->parse($line);
67 5 100       21 push @lines, $ll if $ll->is_beginning;
68             }
69 1         254 return \@lines;
70             }
71              
72              
73             sub all_events {
74 8     8 1 629 my ($self) = @_;
75              
76             # reopen log in sequential reading mode
77 8         34 $self->[IO] = io log;
78 8         1957 my ( @events, $previous );
79 8         80 while ( my $line = $self->[IO]->getline ) {
80 46         23241 my $ll = App::JobLog::Log::Line->parse($line);
81 46 100       151 if ( $ll->is_endpoint ) {
82 26 100       1120 $previous->end = $ll->time if $previous;
83 26 100       76 if ( $ll->is_beginning ) {
84 17         91 $previous = App::JobLog::Log::Event->new($ll);
85 17         100 push @events, $previous;
86             }
87             else {
88 9         58 $previous = undef;
89             }
90             }
91             }
92 8         2007 return \@events;
93             }
94              
95              
96             sub all_notes {
97 2     2 1 6 my ($self) = @_;
98              
99             # reopen log in sequential reading mode
100 2         8 $self->[IO] = io log;
101 2         391 my @notes;
102 2         23 while ( my $line = $self->[IO]->getline ) {
103 17         2597 my $ll = App::JobLog::Log::Line->parse($line);
104 17 100       61 push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
105             }
106 2         480 return \@notes;
107             }
108              
109              
110             sub validate {
111 1     1 1 1199 my ($self) = @_;
112 1         3 my ( $i, $previous_event ) = (0);
113 1         2 my $errors = 0;
114 1         7 while ( my $line = $self->[IO][$i] ) {
115 9         1853 my $ll = App::JobLog::Log::Line->parse($line);
116 9 100       27 if ( $ll->is_malformed ) {
    100          
117 1         3 $errors++;
118 1         18 print STDERR "line $i -- '$line' -- is malformed; commenting out\n";
119 1         3 splice @{ $self->[IO] }, $i, 0,
  1         6  
120             App::JobLog::Log::Line->new( comment => 'ERROR; malformed line' );
121 1         208 $self->[IO][ ++$i ] = $ll->comment_out;
122             }
123             elsif ( $ll->is_event ) {
124 7 100       353 if ($previous_event) {
    50          
125 6 100 100     18 if ( DateTime->compare( $previous_event->time, $ll->time ) > 0 )
    100          
126             {
127 1         92 $errors++;
128 1         17 print STDERR
129             "line $i -- '$line' -- is out of order relative to the last event; commenting out\n";
130 1         3 splice @{ $self->[IO] }, $i, 0,
  1         6  
131             App::JobLog::Log::Line->new(
132             comment => 'ERROR; dates out of order' );
133 1         216 $self->[IO][ ++$i ] = $ll->comment_out;
134             }
135             elsif ( $previous_event->is_end && $ll->is_end ) {
136 1         2 $errors++;
137 1         62 print STDERR
138             "line $i -- '$line' -- specifies the end of a task not yet begun; commenting out\n";
139 1         2 splice @{ $self->[IO] }, $i, 0,
  1         5  
140             App::JobLog::Log::Line->new( comment =>
141             'ERROR; task end without corresponding beginning' );
142 1         402 $self->[IO][ ++$i ] = $ll->comment_out;
143             }
144             else {
145 4         9 $previous_event = $ll;
146             }
147             }
148             elsif ( $ll->is_end ) {
149 0         0 $errors++;
150 0         0 print STDERR
151             "line $i -- '$line' -- specifies the end of a task not yet begun; commenting out\n";
152 0         0 splice @{ $self->[IO] }, $i, 0,
  0         0  
153             App::JobLog::Log::Line->new( comment =>
154             'ERROR; task end without corresponding beginning' );
155 0         0 $self->[IO][ ++$i ] = $ll->comment_out;
156             }
157             else {
158 1         2 $previous_event = $ll;
159             }
160             }
161 9         698 $i++;
162             }
163 1         135 return $errors;
164             }
165              
166              
167             sub first_event {
168 1915     1915 1 3197 my ($self) = @_;
169 1915 100       5440 return $self->[FIRST_EVENT], $self->[FIRST_INDEX] if $self->[FIRST_EVENT];
170 38         87 my $io = $self->[IO];
171 38         80 my ( $i, $e ) = 0;
172 38         133 while ( $i <= $#$io ) {
173 111         74927 my $line = $io->[$i];
174 111         17378 my $ll = App::JobLog::Log::Line->parse($line);
175 111 100       358 if ( $ll->is_endpoint ) {
176 71 100       3274 if ($e) {
177 34         123 $e->end = $ll->time;
178 34         125 last;
179             }
180             else {
181 37         206 $e = App::JobLog::Log::Event->new($ll);
182 37         106 $self->[FIRST_INDEX] = $i;
183             }
184             }
185 77         419 $i++;
186             }
187 38         494 $self->[FIRST_EVENT] = $e;
188 38         142 return $e, $self->[FIRST_INDEX];
189             }
190              
191              
192             sub last_ts {
193 272     272 1 449 my ($self) = @_;
194 272         426 my $io = $self->[IO];
195 272         805 my $i = $#$io;
196 272         315848 for ( ; $i >= 0 ; $i-- ) {
197 271         873 my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
198 271 50       1022 return ( $ll->time, $i ) if $ll->is_event;
199             }
200 1         4 return;
201             }
202              
203              
204             sub first_ts {
205 17     17 1 35 my ($self) = @_;
206 17         39 my $io = $self->[IO];
207 17         28 my $i = 0;
208 17         56 for ( my $lim = $#$io ; $i <= $lim ; $i++ ) {
209 33         1667 my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
210 33 100       141 return ( $ll->time, $i ) if $ll->is_event;
211             }
212 0         0 return;
213             }
214              
215              
216             sub last_event {
217 2137     2137 1 5583 my ($self) = @_;
218 2137 100       8270 return $self->[LAST_EVENT], $self->[LAST_INDEX] if $self->[LAST_EVENT];
219 57         140 my $io = $self->[IO];
220              
221             # was hoping to use IO::All::backwards for this, but seems to be broken
222             # uncertain how to handle utf8 issue with File::ReadBackwards
223 57         133 my @lines;
224 57         255 my $i = $#$io;
225 57         86761 for ( ; $i >= 0 ; $i-- ) {
226 67         595 my $line = $self->[IO][$i];
227 67         13274 my $ll = App::JobLog::Log::Line->parse($line);
228 67 100       321 if ( $ll->is_endpoint ) {
229 56         3114 push @lines, $ll;
230 56 100       256 last if $ll->is_beginning;
231             }
232             }
233 57 100       277 return () unless @lines;
234 51         559 my $e = App::JobLog::Log::Event->new( pop @lines );
235 51 100       205 $e->end = $lines[0]->time if @lines;
236 51         127 $self->[LAST_EVENT] = $e;
237 51         109 $self->[LAST_INDEX] = $i;
238 51         208 return $e, $i;
239             }
240              
241              
242             sub last_note {
243 5     5 1 378 my ($self) = @_;
244 5         13 my $io = $self->[IO];
245 5         16 for ( my $i = $#$io ; $i >= 0 ; $i-- ) {
246 9         3605 my $line = $io->[$i];
247 9         1482 my $ll = App::JobLog::Log::Line->parse($line);
248 9 100       31 return ( App::JobLog::Log::Note->new($ll), $i ) if $ll->is_note;
249             }
250 0         0 return ();
251             }
252              
253              
254             sub reverse_iterator {
255 19     19 1 2883 my ( $self, $event ) = @_;
256 19 100       80 if ( ref $event ) {
257 6 50       25 if ( $event->isa('DateTime') ) {
258 0         0 my $events =
259             $self->find_events( $event, $self->first_event->start );
260 0 0       0 if (@$events) {
261 0         0 $event = $events->[$#$events];
262             }
263             else {
264 0         0 $event = undef;
265             }
266             }
267             }
268             else {
269 13         56 ($event) = $self->last_event;
270             }
271       0     return sub { }
272 19 50       157 unless $event;
273 19         95 my ( undef, $index, $io ) =
274             ( $self->find_previous( $event->start ), $self->[IO] );
275             return sub {
276 55 100   55   356 return () unless $event;
277 47         76 my $e = $event;
278 47         150 my $end_time = $event->start;
279 47         79 $event = undef;
280 47         140 while ( --$index >= 0 ) {
281 61         193 my $line = $io->[$index];
282 61         8169 my $ll = App::JobLog::Log::Line->parse($line);
283 61 100       211 if ( $ll->is_beginning ) {
    100          
284 38         144 $event = App::JobLog::Log::Event->new($ll);
285 38         134 $event->end = $end_time;
286 38         77 last;
287             }
288             elsif ( $ll->is_end ) {
289 12         33 $end_time = $ll->time;
290             }
291             }
292 47         160 return $e;
293 19         374 };
294             }
295              
296              
297             sub find_events {
298 961     961 1 3907432 my ( $self, $start, $end ) = @_;
299 961         1984 my $io = $self->[IO];
300 961         3231 my ( $end_event, $bottom, $start_event, $top ) =
301             ( $self->last_event, $self->first_event );
302              
303             # if the log is empty, return empty list
304 961 100 66     3051 return [] unless $start_event && $end_event;
305              
306             # if the log concerns events before the time in question, return empty list
307 960 100 100     4185 return []
308             unless $end_event->is_open
309             || DateTime->compare( $start, $end_event->end ) < 0;
310              
311             # likewise if it concerns events after
312 958 50       4029 return [] if DateTime->compare( $start_event->start, $end ) > 0;
313              
314             # narrow time range to that in log
315 958         89147 my $c1 = DateTime->compare( $start, $start_event->start ) <= 0;
316 958 100       81894 my $c2 =
317             $end_event->is_open
318             ? DateTime->compare( $end, $end_event->start ) >= 0
319             : DateTime->compare( $end, $end_event->end ) >= 0;
320 958 100 100     82025 return $self->all_events if $c1 && $c2;
321 952 100       2729 $start = $start_event->start if $c1;
322 952 100       2554 $end = $end_event->end if $c2;
323              
324             # matters are simple if what we want is at the start of the log
325 952 100       2144 if ($c1) {
326 6         11 my ( $line, $previous, @events );
327 6         45 while ( my $line = $io->getline ) {
328 202         26239 chomp $line;
329 202         652 my $ll = App::JobLog::Log::Line->parse($line);
330 202 100       607 if ( $ll->is_endpoint ) {
331 160 100       6770 if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
332 6 100       592 $previous->end = $end if $previous->is_open;
333 6         43 last;
334             }
335 154 100 100     14774 if ( $previous && $previous->is_open ) {
336 145         388 $previous->end = $ll->time;
337             }
338 154 100       515 if ( $ll->is_beginning ) {
339 150         468 $previous = App::JobLog::Log::Event->new($ll);
340 150         775 push @events, $previous;
341             }
342             }
343             }
344 6         57 return \@events;
345             }
346              
347             # matters are likewise simple if what we want is at the end of the log
348 946 100       2343 if ($c2) {
349              
350             # must restart io
351 16         62 $io = $self->[IO] = io log;
352 16         3852 $io->backwards;
353 16         10489 my ( $line, $previous, @events );
354 16         129 while ( my $line = $io->getline ) {
355 37         9404 chomp $line;
356 37         207 my $ll = App::JobLog::Log::Line->parse($line);
357 37 100       128 if ( $ll->is_endpoint ) {
358 30         1438 my $e;
359 30 100       95 if ( $ll->is_beginning ) {
360 24         149 $e = App::JobLog::Log::Event->new($ll);
361 24 100       84 $e->end = $previous->time if $previous;
362 24         63 unshift @events, $e;
363             }
364 30 100       106 if ( DateTime->compare( $ll->time, $start ) <= 0 ) {
365 16 100       1616 $e->start = $start if $e;
366 16         93 last;
367             }
368 14         1397 $previous = $ll;
369             }
370             }
371 16         127 return \@events;
372             }
373              
374             # otherwise, do binary search for first event in range
375 930         2800 my ( undef, $i ) = $self->find_previous($start);
376 930         8723 return $self->_scan_from( $i, $start, $end );
377             }
378              
379              
380             sub find_notes {
381 17     17 1 15845 my ( $self, $start, $end ) = @_;
382 17         41 my $io = $self->[IO];
383 17         68 my ( $end_time, $bottom, $start_time, $top ) =
384             ( $self->last_ts, $self->first_ts );
385              
386             # if the log is empty, return empty list
387 17 50 33     89 return [] unless $start_time && $end_time;
388              
389             # if the log concerns events before the time in question, return empty list
390 17 50       1546 return []
391             unless DateTime->compare( $start, $end_time ) <= 0;
392              
393             # likewise if it concerns events after
394 17 50       1705 return [] if DateTime->compare( $start_time, $end ) > 0;
395              
396             # narrow time range to that in log
397 17         1546 my $c1 = DateTime->compare( $start, $start_time ) <= 0;
398 17         1593 my $c2 = DateTime->compare( $end, $end_time ) >= 0;
399 17 100 100     1528 return $self->all_notes if $c1 && $c2;
400 16 100       50 $start = $start_time if $c1;
401 16 100       48 $end = $end_time if $c2;
402              
403             # matters are simple if what we want is at the start of the log
404 16 100       47 if ($c1) {
405 4         9 my ( $line, @notes );
406 4         20 while ( my $line = $io->getline ) {
407 180         22855 chomp $line;
408 180         644 my $ll = App::JobLog::Log::Line->parse($line);
409 180 100       579 if ( $ll->is_event ) {
410 143 100       5906 if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
411 3         311 last;
412             }
413 140 100       13940 push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
414             }
415             }
416 4         107 return \@notes;
417             }
418              
419             # matters are likewise simple if what we want is at the end of the log
420 12 100       36 if ($c2) {
421              
422             # must restart io
423 6         25 $io = $self->[IO] = io log;
424 6         1283 $io->backwards;
425 6         517 my ( $line, @notes );
426 6         31 while ( my $line = $io->getline ) {
427 38         4600 chomp $line;
428 38         139 my $ll = App::JobLog::Log::Line->parse($line);
429 38 100       114 if ( $ll->is_event ) {
430 35         1406 $c2 = DateTime->compare( $ll->time, $start );
431 35 100       3188 last if $c2 < 0;
432 33 100       97 unshift @notes, App::JobLog::Log::Note->new($ll)
433             if $ll->is_note;
434 33 100       272 last unless $c2;
435             }
436             }
437 6         88 return \@notes;
438             }
439              
440             # otherwise, do binary search for first note in range
441 6         22 my $i =
442             $self->_find_previous_note( $start, $end_time, $bottom, $start_time,
443             $top );
444 6 50       23 return [] unless defined $i;
445 6         24 return $self->_scan_for_note_from( $i, $start, $end );
446             }
447              
448              
449             sub find_previous {
450 949     949 1 1743 my ( $self, $e ) = @_;
451 949         1819 my $io = $self->[IO];
452 949         2551 my ( $end_event, $bottom, $start_event, $top ) =
453             ( $self->last_event, $self->first_event );
454              
455             # if the log is empty, return empty list
456 949 50 33     2841 return () unless $start_event && $end_event;
457              
458             # if the start time (improbably but fortuitously) happens to be what we're
459             # looking for, return it
460 949 100       3072 return ( $start_event, $top )
461             if DateTime->compare( $start_event->start, $e ) == 0;
462              
463             # likewise for the end time
464 948 50       82428 return ( $end_event, $bottom ) if $end_event->start < $e;
465              
466             # return the empty list if the event in question precede the first
467             # event in the log
468 948 50       82692 return () unless $start_event->start < $e;
469              
470             # otherwise, do binary search for first event in range
471 948         82614 my ( $et, $eb ) = ( $start_event->start, $end_event->start );
472 948         1551 my $previous_index;
473 948         1496 OUTER: while (1) {
474 5067 100       15549 return $self->_scan_for_previous( $top, $e )
475             if $bottom - $top + 1 <= WINDOW / 2;
476 4148         9339 my $index = _estimate_index( $top, $bottom, $et, $eb, $e );
477 4148 100 100     18834 if ( defined $previous_index && $previous_index == $index ) {
478              
479             # search was too clever by half; we've entered an infinite loop
480 29         106 return $self->_scan_for_previous( $top, $e );
481             }
482 4119         4834 $previous_index = $index;
483 4119         4660 my $event;
484 4119         14539 for my $i ( $index .. $#$io ) {
485 5756         400924 my $line = $io->[$i];
486 5756         756937 my $ll = App::JobLog::Log::Line->parse($line);
487 5756 100       18787 if ( $ll->is_beginning ) {
488 4119         5714 my $do_next = 1;
489 4119         11773 for ( DateTime->compare( $ll->time, $e ) ) {
490 4119         371522 when ( $_ < 0 ) {
491 1837         2588 $top = $i;
492 1837         5221 $et = $ll->time;
493             }
494 2282         3381 when ( $_ > 0 ) {
495 2282         3384 $bottom = $i;
496 2282         6346 $eb = $ll->time;
497             }
498 0         0 default {
499              
500             # found beginning!!
501             # this should happen essentially never
502 0         0 $do_next = 0;
503             }
504             }
505 4119 50       23546 next OUTER if $do_next;
506 0         0 return $self->_scan_for_previous( $i, $e );
507             }
508             }
509             }
510             }
511              
512              
513             sub _find_previous_note {
514 6     6   14 my ( $self, $e, $eb, $bottom, $et, $top ) = @_;
515 6         14 my $io = $self->[IO];
516              
517             # binary search for first note in range
518 6         9 my $previous_index;
519 6         8 OUTER: while (1) {
520 8 100       54 return $self->_scan_for_previous_note( $top, $e )
521             if $bottom - $top + 1 <= WINDOW / 2;
522 3         11 my $index = _estimate_index( $top, $bottom, $et, $eb, $e );
523 3 50 66     17 if ( defined $previous_index && $previous_index == $index ) {
524              
525             # search was too clever by half; we've entered an infinite loop
526 0         0 return $self->_scan_for_previous_note( $top, $e );
527             }
528 3         6 $previous_index = $index;
529 3         5 my $event;
530 3         11 for my $i ( $index .. $#$io ) {
531 3         282 my $line = $io->[$i];
532 3         530 my $ll = App::JobLog::Log::Line->parse($line);
533 3 50       12 if ( $ll->is_event ) {
534 3         124 for ( DateTime->compare( $ll->time, $e ) ) {
535 3         277 when ( $_ < 0 ) {
536 1         2 $top = $i;
537 1         4 $et = $ll->time;
538 1         8 next OUTER;
539             }
540 2         6 when ( $_ > 0 ) {
541 1         2 $bottom = $i;
542 1         4 $eb = $ll->time;
543 1         5 next OUTER;
544             }
545 1         2 default {
546              
547             # found beginning!!
548             # this should happen essentially never
549 1         5 return $self->_scan_for_previous_note( $i, $e );
550             }
551             }
552             }
553             }
554             }
555             }
556              
557             # now that we're close to the section of the log we want, we
558             # scan it sequentially
559             sub _scan_from {
560 930     930   2152 my ( $self, $i, $start, $end ) = @_;
561 930         1713 my $io = $self->[IO];
562              
563             # collect events
564 930         1338 my ( $previous, @events );
565 930         3482 for my $index ( $i .. $#$io ) {
566 9395         112722 my $line = $io->[$index];
567 9395         1288411 my $ll = App::JobLog::Log::Line->parse($line);
568 9395 100       28197 if ( $ll->is_endpoint ) {
569 7526 100       319400 if ($previous) {
570 5233 50       16123 $previous->end = $ll->time if $previous->is_open;
571 5233 100       15942 push @events, $previous
572             if DateTime->compare( $start, $previous->end ) < 0;
573             }
574 7526 100       493486 if ( $ll->is_beginning ) {
575 6163 100       15851 last if DateTime->compare( $ll->time, $end ) >= 0;
576 5233         464140 $previous = App::JobLog::Log::Event->new($ll);
577             }
578             else {
579 1363         7931 $previous = undef;
580             }
581             }
582             }
583 930 50 66     88802 push @events, $previous
      33        
584             if $previous
585             && $previous->is_open
586             && DateTime->compare( $previous->start, $end ) < 0;
587              
588             # return only overlap
589 930         11577 my @return = map { $_->overlap( $start, $end ) } @events;
  4846         13615  
590 930         9361 return \@return;
591             }
592              
593             sub _scan_for_note_from {
594 6     6   15 my ( $self, $i, $start, $end ) = @_;
595 6         14 my $io = $self->[IO];
596              
597             # collect notes
598 6         10 my @notes;
599 6         21 for my $index ( $i .. $#$io ) {
600 123         1255 my $line = $io->[$index];
601 123         19401 my $ll = App::JobLog::Log::Line->parse($line);
602 123 100       388 if ( $ll->is_event ) {
603 121 100       4888 last if $ll->time > $end;
604 115 100 100     11171 if ( $ll->is_note && $ll->time >= $start ) {
605 8         809 push @notes, App::JobLog::Log::Note->new($ll);
606             }
607             }
608             }
609 6         653 return \@notes;
610             }
611              
612             sub _scan_for_previous {
613 948     948   1607 my ( $self, $i, $e ) = @_;
614 948         1625 my $io = $self->[IO];
615              
616             # collect events
617 948         1290 my ( $previous, $previous_index );
618             OUTER: {
619 948         1174 for my $index ( $i .. $#$io ) {
  948         3535  
620 7139         107942 my $line = $io->[$index];
621 7139         825045 my $ll = App::JobLog::Log::Line->parse($line);
622 7139 100       22499 if ( $ll->is_endpoint ) {
623 5782 100 100     250834 $previous->end = $ll->time if $previous && $previous->is_open;
624 5782 100       34310 if ( $ll->time > $e ) {
625 934 50       88363 last if $previous;
626 0         0 $i--;
627 0         0 redo OUTER;
628             }
629 4848 100       456069 if ( $ll->is_beginning ) {
630 3964         13545 $previous = App::JobLog::Log::Event->new($ll);
631 3964         28616 $previous_index = $index;
632             }
633             }
634             }
635             }
636 948         10759 return $previous, $previous_index;
637             }
638              
639             sub _scan_for_previous_note {
640 6     6   14 my ( $self, $i, $e ) = @_;
641 6         12 my $io = $self->[IO];
642              
643             # collect events
644 6         9 my ( $previous, $previous_index );
645 6         21 for my $index ( $i .. $#$io ) {
646 33         663 my $line = $io->[$index];
647 33         5245 my $ll = App::JobLog::Log::Line->parse($line);
648 33 100       106 if ( $ll->is_event ) {
649 32 100       1257 last if $ll->time > $e;
650 26 100       2611 if ( $ll->is_note ) {
651 5         22 $previous = App::JobLog::Log::Note->new($ll);
652 5         23 $previous_index = $index;
653             }
654             }
655             }
656 6   66     651 return $previous_index // $i;
657             }
658              
659             # your generic O(log_n) complexity bisecting search
660             sub _estimate_index {
661 4151     4151   7274 my ( $top, $bottom, $et, $eb, $s ) = @_;
662 4151         6509 my $delta = $bottom - $top + 1;
663 4151         4841 my $i;
664 4151 100       8983 if ( $delta > WINDOW ) {
665 3782         11156 my $d1 = $s->epoch - $et->epoch;
666 3782         41058 my $d2 = $eb->epoch - $et->epoch;
667 3782         32371 my $fraction = $d1 / $d2;
668 3782 100       10425 if ( $fraction < LOW_LIM ) {
    100          
669 1312         1893 $fraction = LOW_LIM;
670             }
671             elsif ( $fraction > HIGH_LIM ) {
672 603         1027 $fraction = HIGH_LIM;
673             }
674 3782         10725 $i = sprintf '%.0f', $delta * $fraction;
675             }
676             else {
677 369         1145 $i = sprintf '%.0f', $delta / 2;
678             }
679 4151   50     9705 $i ||= 1;
680 4151         10248 return $top + $i;
681             }
682              
683              
684             sub append_event {
685 195     195 1 263170 my ( $self, @args ) = @_;
686 195 50       1831 my $current = @args == 1 ? $args[0] : App::JobLog::Log::Line->new(@args);
687 195         393 my $io = $self->[IO];
688 195         243 my $duration;
689 195 100       614 if ( $current->is_event ) {
690 194         7163 my ( $previous, $last_index ) = $self->last_event;
691 194 100       583 if ($previous) {
692              
693             # validation to prevent inconsistency
694 189 50 66     625 carp 'no currently open task'
695             if $current->is_end && $previous->is_closed;
696 189 50 33     537 if (
      66        
697             $current->is_beginning
698             && ( $current->time < $previous->start
699             || $previous->is_closed && $current->time < $previous->end )
700             )
701             {
702 0         0 carp
703             'attempting to append event to log younger than last event in log';
704             }
705              
706             # apply default tags
707 189 100       706 $current->tags = $previous->tags if $current->tags_unspecified;
708              
709             # check for day change
710 189         519 my ($last_ts) = $self->last_ts;
711 189 100 66     741 if ( !$last_ts || _different_day( $last_ts, $current->time ) ) {
712 62         914 $io->append(
713             App::JobLog::Log::Line->new(
714             comment => $current->time->strftime(TS)
715             )
716             )->append("\n");
717             }
718 189 50       10294 if ( $previous->is_open ) {
719 189         520 $duration =
720             $current->time->subtract_datetime( $previous->start );
721 189 50       79321 $duration = undef unless $duration->in_units('days');
722             }
723             }
724             else {
725              
726             # first record in log
727 5         31 $io->append(
728             App::JobLog::Log::Line->new(
729             comment => $current->time->strftime(TS)
730             )
731             )->append("\n");
732             }
733              
734             # cache last event; useful during debugging
735 194 100 33     6744 if ( $current->is_beginning ) {
    50          
736 193         1283 $self->[LAST_EVENT] = App::JobLog::Log::Event->new($current);
737 193         583 $self->[LAST_INDEX] = @$io;
738             }
739             elsif ( $self->[LAST_EVENT] && $self->[LAST_EVENT]->is_open ) {
740 1         4 $self->[LAST_EVENT]->end = $current->time;
741             }
742             }
743 195         19488 $io->append($current)->append("\n");
744 195         19049 $io->close; # flush contents
745 195         26517 return $duration;
746             }
747              
748              
749             sub append_note {
750 66     66 1 57596 my ( $self, @args ) = @_;
751 66         265 my $note = App::JobLog::Log::Line->new( time => now, @args );
752 66         321 $note->{note} = 1; # force this to be marked as a note
753 66         141 my $io = $self->[IO];
754              
755             # check for day change
756 66         240 my ($last_ts) = $self->last_ts;
757 66 100 100     279 if ( !$last_ts || _different_day( $last_ts, $note->time ) ) {
758 3         42 $io->append(
759             App::JobLog::Log::Line->new( comment => $note->time->strftime(TS) )
760             )->append("\n");
761             }
762 66         2405 $io->append($note)->append("\n");
763 66         6517 $io->close; # flush contents
764             }
765              
766             # a test to determine whether two DateTime objects
767             # represent different days
768             sub _different_day {
769 254     254   470 my ( $d1, $d2 ) = @_;
770 254   66     795 return !( $d1->day == $d2->day
771             && $d1->month == $d2->month
772             && $d1->year == $d2->year );
773             }
774              
775              
776             sub close {
777 1     1 1 7 my ($self) = @_;
778 1         4 my $io = $self->[IO];
779 1 50 33     4 $io->close if $io && $io->is_open;
780             }
781              
782              
783             sub insert {
784 0     0 1   my ( $self, $index, @lines ) = @_;
785              
786             # silently return unless some content to insert has been provided
787 0 0         return unless @lines;
788 0 0         my $comment =
    0          
    0          
789             App::JobLog::Log::Line->new( comment => 'the following '
790             . ( @lines == 1 ? '' : scalar(@lines) . ' ' ) . 'line'
791             . ( @lines == 1 ? '' : 's' ) . ' ha'
792             . ( @lines == 1 ? 's' : 've' )
793             . ' been inserted by '
794             . __PACKAGE__
795             . ' rather than having been appended' );
796 0           splice @{ $self->[IO] }, $index, 0, $comment, @lines;
  0            
797             }
798              
799              
800             sub replace {
801 0     0 1   my ( $self, $index, $line ) = @_;
802 0 0 0       carp 'expected integer and log line'
803             unless $index =~ /^\d++$/ && ref $line eq 'App::JobLog::Log::Line';
804 0           $self->[IO][$index] = $line;
805             }
806              
807             1;
808              
809             __END__