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.039';
3             # ABSTRACT: the code that lets us interact with the log
4              
5              
6 4     4   2820 use Modern::Perl;
  4         8  
  4         22  
7 4     4   484 use App::JobLog::Config qw(log init_file);
  4         6  
  4         207  
8 4     4   21 use App::JobLog::Log::Line;
  4         8  
  4         24  
9 4     4   3405 use IO::All -utf8;
  4         47146  
  4         39  
10 4     4   300 use autouse 'Carp' => qw(carp);
  4         7  
  4         30  
11 4     4   502 use autouse 'App::JobLog::Time' => qw(now);
  4         38  
  4         16  
12 4         56 use Class::Autouse qw(
13             App::JobLog::Log::Event
14             App::JobLog::Log::Note
15             DateTime
16             FileHandle
17 4     4   240 );
  4         7  
18 4     4   3195 no if $] >= 5.018, warnings => "experimental::smartmatch";
  4         22  
  4         28  
19              
20             # some stuff useful for searching log
21 4     4   696 use constant WINDOW => 30;
  4         7  
  4         275  
22 4     4   20 use constant LOW_LIM => 1 / 10;
  4         6  
  4         198  
23 4     4   19 use constant HIGH_LIM => 1 - LOW_LIM;
  4         5  
  4         171  
24              
25             # some indices
26 4     4   20 use constant IO => 0;
  4         7  
  4         165  
27 4     4   19 use constant FIRST_EVENT => 1;
  4         6  
  4         164  
28 4     4   18 use constant LAST_EVENT => 2;
  4         7  
  4         168  
29 4     4   18 use constant FIRST_INDEX => 3;
  4         6  
  4         164  
30 4     4   20 use constant LAST_INDEX => 4;
  4         5  
  4         361  
31              
32             # timestamp format
33 4     4   20 use constant TS => '%Y/%m/%d';
  4         8  
  4         26600  
34              
35              
36             sub new {
37 70     70 1 73730 my $class = shift;
38 70 50       266 $class = ref $class if ref $class;
39              
40             # touch log into existence
41 70 100       432 unless ( -e log ) {
42 3         13 init_file log;
43 3         316 my $fh = FileHandle->new( log, 'w' );
44 3         490 $fh->close;
45             }
46              
47             # using an array to make things a little snappier
48 70         332 my $self = bless [], $class;
49 70         305 $self->[IO] = io log;
50 70         49420 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         4 $self->[IO] = io log;
64 1         179 my (@lines);
65 1         53 while ( my $line = $self->[IO]->getline ) {
66 5         865 my $ll = App::JobLog::Log::Line->parse($line);
67 5 100       17 push @lines, $ll if $ll->is_beginning;
68             }
69 1         200 return \@lines;
70             }
71              
72              
73             sub all_events {
74 8     8 1 529 my ($self) = @_;
75              
76             # reopen log in sequential reading mode
77 8         33 $self->[IO] = io log;
78 8         1827 my ( @events, $previous );
79 8         74 while ( my $line = $self->[IO]->getline ) {
80 46         23634 my $ll = App::JobLog::Log::Line->parse($line);
81 46 100       154 if ( $ll->is_endpoint ) {
82 26 100       1111 $previous->end = $ll->time if $previous;
83 26 100       73 if ( $ll->is_beginning ) {
84 17         89 $previous = App::JobLog::Log::Event->new($ll);
85 17         99 push @events, $previous;
86             }
87             else {
88 9         58 $previous = undef;
89             }
90             }
91             }
92 8         1877 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         384 my @notes;
102 2         23 while ( my $line = $self->[IO]->getline ) {
103 17         2582 my $ll = App::JobLog::Log::Line->parse($line);
104 17 100       56 push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
105             }
106 2         412 return \@notes;
107             }
108              
109              
110             sub validate {
111 1     1 1 1265 my ($self) = @_;
112 1         3 my ( $i, $previous_event ) = (0);
113 1         3 my $errors = 0;
114 1         5 while ( my $line = $self->[IO][$i] ) {
115 9         1770 my $ll = App::JobLog::Log::Line->parse($line);
116 9 100       28 if ( $ll->is_malformed ) {
    100          
117 1         1 $errors++;
118 1         17 print STDERR "line $i -- '$line' -- is malformed; commenting out\n";
119 1         3 splice @{ $self->[IO] }, $i, 0,
  1         4  
120             App::JobLog::Log::Line->new( comment => 'ERROR; malformed line' );
121 1         192 $self->[IO][ ++$i ] = $ll->comment_out;
122             }
123             elsif ( $ll->is_event ) {
124 7 100       266 if ($previous_event) {
    50          
125 6 100 100     17 if ( DateTime->compare( $previous_event->time, $ll->time ) > 0 )
    100          
126             {
127 1         91 $errors++;
128 1         16 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         4  
131             App::JobLog::Log::Line->new(
132             comment => 'ERROR; dates out of order' );
133 1         202 $self->[IO][ ++$i ] = $ll->comment_out;
134             }
135             elsif ( $previous_event->is_end && $ll->is_end ) {
136 1         2 $errors++;
137 1         50 print STDERR
138             "line $i -- '$line' -- specifies the end of a task not yet begun; commenting out\n";
139 1         3 splice @{ $self->[IO] }, $i, 0,
  1         4  
140             App::JobLog::Log::Line->new( comment =>
141             'ERROR; task end without corresponding beginning' );
142 1         335 $self->[IO][ ++$i ] = $ll->comment_out;
143             }
144             else {
145 4         7 $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         3 $previous_event = $ll;
159             }
160             }
161 9         694 $i++;
162             }
163 1         127 return $errors;
164             }
165              
166              
167             sub first_event {
168 1915     1915 1 3008 my ($self) = @_;
169 1915 100       5036 return $self->[FIRST_EVENT], $self->[FIRST_INDEX] if $self->[FIRST_EVENT];
170 38         80 my $io = $self->[IO];
171 38         81 my ( $i, $e ) = 0;
172 38         123 while ( $i <= $#$io ) {
173 111         73505 my $line = $io->[$i];
174 111         17719 my $ll = App::JobLog::Log::Line->parse($line);
175 111 100       413 if ( $ll->is_endpoint ) {
176 71 100       3293 if ($e) {
177 34         128 $e->end = $ll->time;
178 34         143 last;
179             }
180             else {
181 37         213 $e = App::JobLog::Log::Event->new($ll);
182 37         100 $self->[FIRST_INDEX] = $i;
183             }
184             }
185 77         394 $i++;
186             }
187 38         496 $self->[FIRST_EVENT] = $e;
188 38         132 return $e, $self->[FIRST_INDEX];
189             }
190              
191              
192             sub last_ts {
193 272     272 1 433 my ($self) = @_;
194 272         404 my $io = $self->[IO];
195 272         804 my $i = $#$io;
196 272         328543 for ( ; $i >= 0 ; $i-- ) {
197 271         886 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 33 my ($self) = @_;
206 17         34 my $io = $self->[IO];
207 17         29 my $i = 0;
208 17         54 for ( my $lim = $#$io ; $i <= $lim ; $i++ ) {
209 33         1711 my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
210 33 100       144 return ( $ll->time, $i ) if $ll->is_event;
211             }
212 0         0 return;
213             }
214              
215              
216             sub last_event {
217 2137     2137 1 6224 my ($self) = @_;
218 2137 100       7764 return $self->[LAST_EVENT], $self->[LAST_INDEX] if $self->[LAST_EVENT];
219 57         136 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         139 my @lines;
224 57         263 my $i = $#$io;
225 57         88474 for ( ; $i >= 0 ; $i-- ) {
226 67         569 my $line = $self->[IO][$i];
227 67         12975 my $ll = App::JobLog::Log::Line->parse($line);
228 67 100       325 if ( $ll->is_endpoint ) {
229 56         3024 push @lines, $ll;
230 56 100       227 last if $ll->is_beginning;
231             }
232             }
233 57 100       261 return () unless @lines;
234 51         539 my $e = App::JobLog::Log::Event->new( pop @lines );
235 51 100       190 $e->end = $lines[0]->time if @lines;
236 51         122 $self->[LAST_EVENT] = $e;
237 51         111 $self->[LAST_INDEX] = $i;
238 51         202 return $e, $i;
239             }
240              
241              
242             sub last_note {
243 5     5 1 306 my ($self) = @_;
244 5         11 my $io = $self->[IO];
245 5         17 for ( my $i = $#$io ; $i >= 0 ; $i-- ) {
246 9         3813 my $line = $io->[$i];
247 9         1478 my $ll = App::JobLog::Log::Line->parse($line);
248 9 100       30 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 2636 my ( $self, $event ) = @_;
256 19 100       71 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         59 ($event) = $self->last_event;
270             }
271       0     return sub { }
272 19 50       150 unless $event;
273 19         82 my ( undef, $index, $io ) =
274             ( $self->find_previous( $event->start ), $self->[IO] );
275             return sub {
276 55 100   55   336 return () unless $event;
277 47         73 my $e = $event;
278 47         157 my $end_time = $event->start;
279 47         82 $event = undef;
280 47         124 while ( --$index >= 0 ) {
281 61         188 my $line = $io->[$index];
282 61         8184 my $ll = App::JobLog::Log::Line->parse($line);
283 61 100       221 if ( $ll->is_beginning ) {
    100          
284 38         138 $event = App::JobLog::Log::Event->new($ll);
285 38         126 $event->end = $end_time;
286 38         71 last;
287             }
288             elsif ( $ll->is_end ) {
289 12         30 $end_time = $ll->time;
290             }
291             }
292 47         160 return $e;
293 19         403 };
294             }
295              
296              
297             sub find_events {
298 961     961 1 3926486 my ( $self, $start, $end ) = @_;
299 961         2279 my $io = $self->[IO];
300 961         3512 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     3175 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     3759 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       3857 return [] if DateTime->compare( $start_event->start, $end ) > 0;
313              
314             # narrow time range to that in log
315 958         88234 my $c1 = DateTime->compare( $start, $start_event->start ) <= 0;
316 958 100       81923 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     82436 return $self->all_events if $c1 && $c2;
321 952 100       2826 $start = $start_event->start if $c1;
322 952 100       2223 $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       2336 if ($c1) {
326 6         14 my ( $line, $previous, @events );
327 6         39 while ( my $line = $io->getline ) {
328 202         25759 chomp $line;
329 202         655 my $ll = App::JobLog::Log::Line->parse($line);
330 202 100       630 if ( $ll->is_endpoint ) {
331 160 100       6835 if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
332 6 100       550 $previous->end = $end if $previous->is_open;
333 6         46 last;
334             }
335 154 100 100     13728 if ( $previous && $previous->is_open ) {
336 145         372 $previous->end = $ll->time;
337             }
338 154 100       530 if ( $ll->is_beginning ) {
339 150         429 $previous = App::JobLog::Log::Event->new($ll);
340 150         787 push @events, $previous;
341             }
342             }
343             }
344 6         52 return \@events;
345             }
346              
347             # matters are likewise simple if what we want is at the end of the log
348 946 100       2301 if ($c2) {
349              
350             # must restart io
351 16         73 $io = $self->[IO] = io log;
352 16         3802 $io->backwards;
353 16         9502 my ( $line, $previous, @events );
354 16         100 while ( my $line = $io->getline ) {
355 37         9154 chomp $line;
356 37         210 my $ll = App::JobLog::Log::Line->parse($line);
357 37 100       132 if ( $ll->is_endpoint ) {
358 30         1284 my $e;
359 30 100       94 if ( $ll->is_beginning ) {
360 24         147 $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       104 if ( DateTime->compare( $ll->time, $start ) <= 0 ) {
365 16 100       1496 $e->start = $start if $e;
366 16         80 last;
367             }
368 14         1365 $previous = $ll;
369             }
370             }
371 16         133 return \@events;
372             }
373              
374             # otherwise, do binary search for first event in range
375 930         2997 my ( undef, $i ) = $self->find_previous($start);
376 930         8680 return $self->_scan_from( $i, $start, $end );
377             }
378              
379              
380             sub find_notes {
381 17     17 1 16810 my ( $self, $start, $end ) = @_;
382 17         40 my $io = $self->[IO];
383 17         61 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     85 return [] unless $start_time && $end_time;
388              
389             # if the log concerns events before the time in question, return empty list
390 17 50       1556 return []
391             unless DateTime->compare( $start, $end_time ) <= 0;
392              
393             # likewise if it concerns events after
394 17 50       1602 return [] if DateTime->compare( $start_time, $end ) > 0;
395              
396             # narrow time range to that in log
397 17         1462 my $c1 = DateTime->compare( $start, $start_time ) <= 0;
398 17         1466 my $c2 = DateTime->compare( $end, $end_time ) >= 0;
399 17 100 100     1501 return $self->all_notes if $c1 && $c2;
400 16 100       48 $start = $start_time if $c1;
401 16 100       47 $end = $end_time if $c2;
402              
403             # matters are simple if what we want is at the start of the log
404 16 100       49 if ($c1) {
405 4         7 my ( $line, @notes );
406 4         22 while ( my $line = $io->getline ) {
407 180         21024 chomp $line;
408 180         623 my $ll = App::JobLog::Log::Line->parse($line);
409 180 100       545 if ( $ll->is_event ) {
410 143 100       5435 if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
411 3         349 last;
412             }
413 140 100       12724 push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
414             }
415             }
416 4         101 return \@notes;
417             }
418              
419             # matters are likewise simple if what we want is at the end of the log
420 12 100       33 if ($c2) {
421              
422             # must restart io
423 6         24 $io = $self->[IO] = io log;
424 6         1244 $io->backwards;
425 6         434 my ( $line, @notes );
426 6         31 while ( my $line = $io->getline ) {
427 38         4614 chomp $line;
428 38         137 my $ll = App::JobLog::Log::Line->parse($line);
429 38 100       136 if ( $ll->is_event ) {
430 35         1401 $c2 = DateTime->compare( $ll->time, $start );
431 35 100       3162 last if $c2 < 0;
432 33 100       94 unshift @notes, App::JobLog::Log::Note->new($ll)
433             if $ll->is_note;
434 33 100       275 last unless $c2;
435             }
436             }
437 6         88 return \@notes;
438             }
439              
440             # otherwise, do binary search for first note in range
441 6         27 my $i =
442             $self->_find_previous_note( $start, $end_time, $bottom, $start_time,
443             $top );
444 6 50       28 return [] unless defined $i;
445 6         18 return $self->_scan_for_note_from( $i, $start, $end );
446             }
447              
448              
449             sub find_previous {
450 949     949 1 1777 my ( $self, $e ) = @_;
451 949         1509 my $io = $self->[IO];
452 949         2280 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     2681 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       3171 return ( $start_event, $top )
461             if DateTime->compare( $start_event->start, $e ) == 0;
462              
463             # likewise for the end time
464 948 50       81961 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       82765 return () unless $start_event->start < $e;
469              
470             # otherwise, do binary search for first event in range
471 948         81899 my ( $et, $eb ) = ( $start_event->start, $end_event->start );
472 948         1631 my $previous_index;
473 948         1395 OUTER: while (1) {
474 5067 100       15959 return $self->_scan_for_previous( $top, $e )
475             if $bottom - $top + 1 <= WINDOW / 2;
476 4148         10020 my $index = _estimate_index( $top, $bottom, $et, $eb, $e );
477 4148 100 100     18816 if ( defined $previous_index && $previous_index == $index ) {
478              
479             # search was too clever by half; we've entered an infinite loop
480 29         107 return $self->_scan_for_previous( $top, $e );
481             }
482 4119         4825 $previous_index = $index;
483 4119         4507 my $event;
484 4119         13975 for my $i ( $index .. $#$io ) {
485 5756         403462 my $line = $io->[$i];
486 5756         763394 my $ll = App::JobLog::Log::Line->parse($line);
487 5756 100       17594 if ( $ll->is_beginning ) {
488 4119         6081 my $do_next = 1;
489 4119         12029 for ( DateTime->compare( $ll->time, $e ) ) {
490 4119         370797 when ( $_ < 0 ) {
491 1837         2383 $top = $i;
492 1837         5323 $et = $ll->time;
493             }
494 2282         3808 when ( $_ > 0 ) {
495 2282         2892 $bottom = $i;
496 2282         6076 $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       23413 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   15 my ( $self, $e, $eb, $bottom, $et, $top ) = @_;
515 6         13 my $io = $self->[IO];
516              
517             # binary search for first note in range
518 6         11 my $previous_index;
519 6         9 OUTER: while (1) {
520 8 100       43 return $self->_scan_for_previous_note( $top, $e )
521             if $bottom - $top + 1 <= WINDOW / 2;
522 3         13 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         4 my $event;
530 3         12 for my $i ( $index .. $#$io ) {
531 3         282 my $line = $io->[$i];
532 3         475 my $ll = App::JobLog::Log::Line->parse($line);
533 3 50       12 if ( $ll->is_event ) {
534 3         121 for ( DateTime->compare( $ll->time, $e ) ) {
535 3         268 when ( $_ < 0 ) {
536 1         3 $top = $i;
537 1         4 $et = $ll->time;
538 1         7 next OUTER;
539             }
540 2         5 when ( $_ > 0 ) {
541 1         3 $bottom = $i;
542 1         3 $eb = $ll->time;
543 1         6 next OUTER;
544             }
545 1         2 default {
546              
547             # found beginning!!
548             # this should happen essentially never
549 1         4 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   2089 my ( $self, $i, $start, $end ) = @_;
561 930         1685 my $io = $self->[IO];
562              
563             # collect events
564 930         1280 my ( $previous, @events );
565 930         2845 for my $index ( $i .. $#$io ) {
566 9395         113498 my $line = $io->[$index];
567 9395         1299713 my $ll = App::JobLog::Log::Line->parse($line);
568 9395 100       29667 if ( $ll->is_endpoint ) {
569 7526 100       317169 if ($previous) {
570 5233 50       15620 $previous->end = $ll->time if $previous->is_open;
571 5233 100       15580 push @events, $previous
572             if DateTime->compare( $start, $previous->end ) < 0;
573             }
574 7526 100       493001 if ( $ll->is_beginning ) {
575 6163 100       16069 last if DateTime->compare( $ll->time, $end ) >= 0;
576 5233         463166 $previous = App::JobLog::Log::Event->new($ll);
577             }
578             else {
579 1363         8051 $previous = undef;
580             }
581             }
582             }
583 930 50 66     89080 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         11704 my @return = map { $_->overlap( $start, $end ) } @events;
  4846         13005  
590 930         9575 return \@return;
591             }
592              
593             sub _scan_for_note_from {
594 6     6   16 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         1244 my $line = $io->[$index];
601 123         19245 my $ll = App::JobLog::Log::Line->parse($line);
602 123 100       443 if ( $ll->is_event ) {
603 121 100       4752 last if $ll->time > $end;
604 115 100 100     11378 if ( $ll->is_note && $ll->time >= $start ) {
605 8         783 push @notes, App::JobLog::Log::Note->new($ll);
606             }
607             }
608             }
609 6         655 return \@notes;
610             }
611              
612             sub _scan_for_previous {
613 948     948   1992 my ( $self, $i, $e ) = @_;
614 948         2322 my $io = $self->[IO];
615              
616             # collect events
617 948         1402 my ( $previous, $previous_index );
618             OUTER: {
619 948         1281 for my $index ( $i .. $#$io ) {
  948         3367  
620 7139         108026 my $line = $io->[$index];
621 7139         829521 my $ll = App::JobLog::Log::Line->parse($line);
622 7139 100       22192 if ( $ll->is_endpoint ) {
623 5782 100 100     250831 $previous->end = $ll->time if $previous && $previous->is_open;
624 5782 100       34557 if ( $ll->time > $e ) {
625 934 50       88361 last if $previous;
626 0         0 $i--;
627 0         0 redo OUTER;
628             }
629 4848 100       453895 if ( $ll->is_beginning ) {
630 3964         13262 $previous = App::JobLog::Log::Event->new($ll);
631 3964         28760 $previous_index = $index;
632             }
633             }
634             }
635             }
636 948         10863 return $previous, $previous_index;
637             }
638              
639             sub _scan_for_previous_note {
640 6     6   13 my ( $self, $i, $e ) = @_;
641 6         14 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         661 my $line = $io->[$index];
647 33         5214 my $ll = App::JobLog::Log::Line->parse($line);
648 33 100       100 if ( $ll->is_event ) {
649 32 100       1237 last if $ll->time > $e;
650 26 100       2587 if ( $ll->is_note ) {
651 5         20 $previous = App::JobLog::Log::Note->new($ll);
652 5         21 $previous_index = $index;
653             }
654             }
655             }
656 6   66     713 return $previous_index // $i;
657             }
658              
659             # your generic O(log_n) complexity bisecting search
660             sub _estimate_index {
661 4151     4151   7499 my ( $top, $bottom, $et, $eb, $s ) = @_;
662 4151         6064 my $delta = $bottom - $top + 1;
663 4151         4816 my $i;
664 4151 100       8303 if ( $delta > WINDOW ) {
665 3782         11160 my $d1 = $s->epoch - $et->epoch;
666 3782         40476 my $d2 = $eb->epoch - $et->epoch;
667 3782         31821 my $fraction = $d1 / $d2;
668 3782 100       10511 if ( $fraction < LOW_LIM ) {
    100          
669 1312         1837 $fraction = LOW_LIM;
670             }
671             elsif ( $fraction > HIGH_LIM ) {
672 603         1051 $fraction = HIGH_LIM;
673             }
674 3782         10222 $i = sprintf '%.0f', $delta * $fraction;
675             }
676             else {
677 369         1183 $i = sprintf '%.0f', $delta / 2;
678             }
679 4151   50     8148 $i ||= 1;
680 4151         10170 return $top + $i;
681             }
682              
683              
684             sub append_event {
685 195     195 1 258082 my ( $self, @args ) = @_;
686 195 50       1779 my $current = @args == 1 ? $args[0] : App::JobLog::Log::Line->new(@args);
687 195         368 my $io = $self->[IO];
688 195         261 my $duration;
689 195 100       579 if ( $current->is_event ) {
690 194         6940 my ( $previous, $last_index ) = $self->last_event;
691 194 100       554 if ($previous) {
692              
693             # validation to prevent inconsistency
694 189 50 66     603 carp 'no currently open task'
695             if $current->is_end && $previous->is_closed;
696 189 50 33     528 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       654 $current->tags = $previous->tags if $current->tags_unspecified;
708              
709             # check for day change
710 189         565 my ($last_ts) = $self->last_ts;
711 189 100 66     745 if ( !$last_ts || _different_day( $last_ts, $current->time ) ) {
712 62         810 $io->append(
713             App::JobLog::Log::Line->new(
714             comment => $current->time->strftime(TS)
715             )
716             )->append("\n");
717             }
718 189 50       9921 if ( $previous->is_open ) {
719 189         511 $duration =
720             $current->time->subtract_datetime( $previous->start );
721 189 50       76129 $duration = undef unless $duration->in_units('days');
722             }
723             }
724             else {
725              
726             # first record in log
727 5         27 $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     6254 if ( $current->is_beginning ) {
    50          
736 193         1302 $self->[LAST_EVENT] = App::JobLog::Log::Event->new($current);
737 193         641 $self->[LAST_INDEX] = @$io;
738             }
739             elsif ( $self->[LAST_EVENT] && $self->[LAST_EVENT]->is_open ) {
740 1         5 $self->[LAST_EVENT]->end = $current->time;
741             }
742             }
743 195         19773 $io->append($current)->append("\n");
744 195         18543 $io->close; # flush contents
745 195         25127 return $duration;
746             }
747              
748              
749             sub append_note {
750 66     66 1 54074 my ( $self, @args ) = @_;
751 66         293 my $note = App::JobLog::Log::Line->new( time => now, @args );
752 66         308 $note->{note} = 1; # force this to be marked as a note
753 66         128 my $io = $self->[IO];
754              
755             # check for day change
756 66         219 my ($last_ts) = $self->last_ts;
757 66 100 100     266 if ( !$last_ts || _different_day( $last_ts, $note->time ) ) {
758 3         37 $io->append(
759             App::JobLog::Log::Line->new( comment => $note->time->strftime(TS) )
760             )->append("\n");
761             }
762 66         2259 $io->append($note)->append("\n");
763 66         6243 $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   405 my ( $d1, $d2 ) = @_;
770 254   66     722 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         3 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__