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.041';
3             # ABSTRACT: the code that lets us interact with the log
4              
5              
6 4     4   2730 use Modern::Perl;
  4         7  
  4         23  
7 4     4   466 use App::JobLog::Config qw(log init_file);
  4         7  
  4         284  
8 4     4   21 use App::JobLog::Log::Line;
  4         7  
  4         24  
9 4     4   3197 use IO::All -utf8;
  4         44684  
  4         36  
10 4     4   336 use autouse 'Carp' => qw(carp);
  4         10  
  4         30  
11 4     4   394 use autouse 'App::JobLog::Time' => qw(now);
  4         41  
  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   233 );
  4         8  
18 4     4   2879 no if $] >= 5.018, warnings => "experimental::smartmatch";
  4         21  
  4         28  
19              
20             # some stuff useful for searching log
21 4     4   752 use constant WINDOW => 30;
  4         7  
  4         341  
22 4     4   20 use constant LOW_LIM => 1 / 10;
  4         7  
  4         200  
23 4     4   19 use constant HIGH_LIM => 1 - LOW_LIM;
  4         7  
  4         168  
24              
25             # some indices
26 4     4   20 use constant IO => 0;
  4         7  
  4         161  
27 4     4   19 use constant FIRST_EVENT => 1;
  4         6  
  4         160  
28 4     4   18 use constant LAST_EVENT => 2;
  4         7  
  4         184  
29 4     4   19 use constant FIRST_INDEX => 3;
  4         7  
  4         173  
30 4     4   18 use constant LAST_INDEX => 4;
  4         6  
  4         166  
31              
32             # timestamp format
33 4     4   20 use constant TS => '%Y/%m/%d';
  4         7  
  4         24600  
34              
35              
36             sub new {
37 70     70 1 432740 my $class = shift;
38 70 50       283 $class = ref $class if ref $class;
39              
40             # touch log into existence
41 70 100       401 unless ( -e log ) {
42 3         13 init_file log;
43 3         322 my $fh = FileHandle->new( log, 'w' );
44 3         459 $fh->close;
45             }
46              
47             # using an array to make things a little snappier
48 70         309 my $self = bless [], $class;
49 70         283 $self->[IO] = io log;
50 70         49466 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         181 my (@lines);
65 1         19 while ( my $line = $self->[IO]->getline ) {
66 5         898 my $ll = App::JobLog::Log::Line->parse($line);
67 5 100       18 push @lines, $ll if $ll->is_beginning;
68             }
69 1         235 return \@lines;
70             }
71              
72              
73             sub all_events {
74 8     8 1 531 my ($self) = @_;
75              
76             # reopen log in sequential reading mode
77 8         76 $self->[IO] = io log;
78 8         1794 my ( @events, $previous );
79 8         72 while ( my $line = $self->[IO]->getline ) {
80 46         22969 my $ll = App::JobLog::Log::Line->parse($line);
81 46 100       158 if ( $ll->is_endpoint ) {
82 26 100       1125 $previous->end = $ll->time if $previous;
83 26 100       80 if ( $ll->is_beginning ) {
84 17         91 $previous = App::JobLog::Log::Event->new($ll);
85 17         97 push @events, $previous;
86             }
87             else {
88 9         58 $previous = undef;
89             }
90             }
91             }
92 8         1969 return \@events;
93             }
94              
95              
96             sub all_notes {
97 2     2 1 5 my ($self) = @_;
98              
99             # reopen log in sequential reading mode
100 2         9 $self->[IO] = io log;
101 2         393 my @notes;
102 2         24 while ( my $line = $self->[IO]->getline ) {
103 17         2671 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         465 return \@notes;
107             }
108              
109              
110             sub validate {
111 1     1 1 1177 my ($self) = @_;
112 1         3 my ( $i, $previous_event ) = (0);
113 1         2 my $errors = 0;
114 1         5 while ( my $line = $self->[IO][$i] ) {
115 9         1715 my $ll = App::JobLog::Log::Line->parse($line);
116 9 100       29 if ( $ll->is_malformed ) {
    100          
117 1         2 $errors++;
118 1         17 print STDERR "line $i -- '$line' -- is malformed; commenting out\n";
119 1         2 splice @{ $self->[IO] }, $i, 0,
  1         4  
120             App::JobLog::Log::Line->new( comment => 'ERROR; malformed line' );
121 1         189 $self->[IO][ ++$i ] = $ll->comment_out;
122             }
123             elsif ( $ll->is_event ) {
124 7 100       271 if ($previous_event) {
    50          
125 6 100 100     18 if ( DateTime->compare( $previous_event->time, $ll->time ) > 0 )
    100          
126             {
127 1         89 $errors++;
128 1         17 print STDERR
129             "line $i -- '$line' -- is out of order relative to the last event; commenting out\n";
130 1         2 splice @{ $self->[IO] }, $i, 0,
  1         5  
131             App::JobLog::Log::Line->new(
132             comment => 'ERROR; dates out of order' );
133 1         207 $self->[IO][ ++$i ] = $ll->comment_out;
134             }
135             elsif ( $previous_event->is_end && $ll->is_end ) {
136 1         3 $errors++;
137 1         52 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         5  
140             App::JobLog::Log::Line->new( comment =>
141             'ERROR; task end without corresponding beginning' );
142 1         315 $self->[IO][ ++$i ] = $ll->comment_out;
143             }
144             else {
145 4         6 $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         760 $i++;
162             }
163 1         126 return $errors;
164             }
165              
166              
167             sub first_event {
168 1915     1915 1 3139 my ($self) = @_;
169 1915 100       5006 return $self->[FIRST_EVENT], $self->[FIRST_INDEX] if $self->[FIRST_EVENT];
170 38         81 my $io = $self->[IO];
171 38         102 my ( $i, $e ) = 0;
172 38         143 while ( $i <= $#$io ) {
173 111         76273 my $line = $io->[$i];
174 111         17647 my $ll = App::JobLog::Log::Line->parse($line);
175 111 100       394 if ( $ll->is_endpoint ) {
176 71 100       3264 if ($e) {
177 34         138 $e->end = $ll->time;
178 34         132 last;
179             }
180             else {
181 37         245 $e = App::JobLog::Log::Event->new($ll);
182 37         116 $self->[FIRST_INDEX] = $i;
183             }
184             }
185 77         417 $i++;
186             }
187 38         483 $self->[FIRST_EVENT] = $e;
188 38         134 return $e, $self->[FIRST_INDEX];
189             }
190              
191              
192             sub last_ts {
193 272     272 1 505 my ($self) = @_;
194 272         429 my $io = $self->[IO];
195 272         784 my $i = $#$io;
196 272         306373 for ( ; $i >= 0 ; $i-- ) {
197 271         877 my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
198 271 50       1042 return ( $ll->time, $i ) if $ll->is_event;
199             }
200 1         4 return;
201             }
202              
203              
204             sub first_ts {
205 17     17 1 53 my ($self) = @_;
206 17         37 my $io = $self->[IO];
207 17         32 my $i = 0;
208 17         53 for ( my $lim = $#$io ; $i <= $lim ; $i++ ) {
209 33         1718 my $ll = App::JobLog::Log::Line->parse( $io->[$i] );
210 33 100       142 return ( $ll->time, $i ) if $ll->is_event;
211             }
212 0         0 return;
213             }
214              
215              
216             sub last_event {
217 2137     2137 1 6465 my ($self) = @_;
218 2137 100       7912 return $self->[LAST_EVENT], $self->[LAST_INDEX] if $self->[LAST_EVENT];
219 57         157 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         105 my @lines;
224 57         249 my $i = $#$io;
225 57         162435 for ( ; $i >= 0 ; $i-- ) {
226 67         587 my $line = $self->[IO][$i];
227 67         13301 my $ll = App::JobLog::Log::Line->parse($line);
228 67 100       360 if ( $ll->is_endpoint ) {
229 56         2974 push @lines, $ll;
230 56 100       235 last if $ll->is_beginning;
231             }
232             }
233 57 100       287 return () unless @lines;
234 51         541 my $e = App::JobLog::Log::Event->new( pop @lines );
235 51 100       256 $e->end = $lines[0]->time if @lines;
236 51         121 $self->[LAST_EVENT] = $e;
237 51         107 $self->[LAST_INDEX] = $i;
238 51         200 return $e, $i;
239             }
240              
241              
242             sub last_note {
243 5     5 1 298 my ($self) = @_;
244 5         12 my $io = $self->[IO];
245 5         17 for ( my $i = $#$io ; $i >= 0 ; $i-- ) {
246 9         3449 my $line = $io->[$i];
247 9         1614 my $ll = App::JobLog::Log::Line->parse($line);
248 9 100       32 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 2730 my ( $self, $event ) = @_;
256 19 100       86 if ( ref $event ) {
257 6 50       27 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         57 ($event) = $self->last_event;
270             }
271       0     return sub { }
272 19 50       153 unless $event;
273 19         96 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         83 my $e = $event;
278 47         148 my $end_time = $event->start;
279 47         83 $event = undef;
280 47         140 while ( --$index >= 0 ) {
281 61         199 my $line = $io->[$index];
282 61         8227 my $ll = App::JobLog::Log::Line->parse($line);
283 61 100       207 if ( $ll->is_beginning ) {
    100          
284 38         146 $event = App::JobLog::Log::Event->new($ll);
285 38         122 $event->end = $end_time;
286 38         88 last;
287             }
288             elsif ( $ll->is_end ) {
289 12         37 $end_time = $ll->time;
290             }
291             }
292 47         171 return $e;
293 19         376 };
294             }
295              
296              
297             sub find_events {
298 961     961 1 3903522 my ( $self, $start, $end ) = @_;
299 961         2227 my $io = $self->[IO];
300 961         3385 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     3489 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     3947 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       3932 return [] if DateTime->compare( $start_event->start, $end ) > 0;
313              
314             # narrow time range to that in log
315 958         89184 my $c1 = DateTime->compare( $start, $start_event->start ) <= 0;
316 958 100       83234 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     82935 return $self->all_events if $c1 && $c2;
321 952 100       2831 $start = $start_event->start if $c1;
322 952 100       2520 $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       2873 if ($c1) {
326 6         11 my ( $line, $previous, @events );
327 6         41 while ( my $line = $io->getline ) {
328 202         25776 chomp $line;
329 202         653 my $ll = App::JobLog::Log::Line->parse($line);
330 202 100       605 if ( $ll->is_endpoint ) {
331 160 100       6874 if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
332 6 100       586 $previous->end = $end if $previous->is_open;
333 6         44 last;
334             }
335 154 100 100     13871 if ( $previous && $previous->is_open ) {
336 145         410 $previous->end = $ll->time;
337             }
338 154 100       519 if ( $ll->is_beginning ) {
339 150         456 $previous = App::JobLog::Log::Event->new($ll);
340 150         749 push @events, $previous;
341             }
342             }
343             }
344 6         50 return \@events;
345             }
346              
347             # matters are likewise simple if what we want is at the end of the log
348 946 100       2334 if ($c2) {
349              
350             # must restart io
351 16         74 $io = $self->[IO] = io log;
352 16         3968 $io->backwards;
353 16         10260 my ( $line, $previous, @events );
354 16         127 while ( my $line = $io->getline ) {
355 37         9252 chomp $line;
356 37         270 my $ll = App::JobLog::Log::Line->parse($line);
357 37 100       130 if ( $ll->is_endpoint ) {
358 30         1304 my $e;
359 30 100       99 if ( $ll->is_beginning ) {
360 24         153 $e = App::JobLog::Log::Event->new($ll);
361 24 100       82 $e->end = $previous->time if $previous;
362 24         66 unshift @events, $e;
363             }
364 30 100       111 if ( DateTime->compare( $ll->time, $start ) <= 0 ) {
365 16 100       1626 $e->start = $start if $e;
366 16         91 last;
367             }
368 14         1369 $previous = $ll;
369             }
370             }
371 16         109 return \@events;
372             }
373              
374             # otherwise, do binary search for first event in range
375 930         3134 my ( undef, $i ) = $self->find_previous($start);
376 930         8418 return $self->_scan_from( $i, $start, $end );
377             }
378              
379              
380             sub find_notes {
381 17     17 1 16683 my ( $self, $start, $end ) = @_;
382 17         38 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     90 return [] unless $start_time && $end_time;
388              
389             # if the log concerns events before the time in question, return empty list
390 17 50       1535 return []
391             unless DateTime->compare( $start, $end_time ) <= 0;
392              
393             # likewise if it concerns events after
394 17 50       1627 return [] if DateTime->compare( $start_time, $end ) > 0;
395              
396             # narrow time range to that in log
397 17         1452 my $c1 = DateTime->compare( $start, $start_time ) <= 0;
398 17         1430 my $c2 = DateTime->compare( $end, $end_time ) >= 0;
399 17 100 100     1464 return $self->all_notes if $c1 && $c2;
400 16 100       50 $start = $start_time if $c1;
401 16 100       45 $end = $end_time if $c2;
402              
403             # matters are simple if what we want is at the start of the log
404 16 100       43 if ($c1) {
405 4         8 my ( $line, @notes );
406 4         20 while ( my $line = $io->getline ) {
407 180         20660 chomp $line;
408 180         597 my $ll = App::JobLog::Log::Line->parse($line);
409 180 100       562 if ( $ll->is_event ) {
410 143 100       5469 if ( DateTime->compare( $ll->time, $end ) >= 0 ) {
411 3         278 last;
412             }
413 140 100       12533 push @notes, App::JobLog::Log::Note->new($ll) if $ll->is_note;
414             }
415             }
416 4         99 return \@notes;
417             }
418              
419             # matters are likewise simple if what we want is at the end of the log
420 12 100       38 if ($c2) {
421              
422             # must restart io
423 6         25 $io = $self->[IO] = io log;
424 6         1340 $io->backwards;
425 6         511 my ( $line, @notes );
426 6         29 while ( my $line = $io->getline ) {
427 38         4700 chomp $line;
428 38         141 my $ll = App::JobLog::Log::Line->parse($line);
429 38 100       119 if ( $ll->is_event ) {
430 35         1387 $c2 = DateTime->compare( $ll->time, $start );
431 35 100       3226 last if $c2 < 0;
432 33 100       102 unshift @notes, App::JobLog::Log::Note->new($ll)
433             if $ll->is_note;
434 33 100       269 last unless $c2;
435             }
436             }
437 6         83 return \@notes;
438             }
439              
440             # otherwise, do binary search for first note in range
441 6         24 my $i =
442             $self->_find_previous_note( $start, $end_time, $bottom, $start_time,
443             $top );
444 6 50       25 return [] unless defined $i;
445 6         20 return $self->_scan_for_note_from( $i, $start, $end );
446             }
447              
448              
449             sub find_previous {
450 949     949 1 1861 my ( $self, $e ) = @_;
451 949         1813 my $io = $self->[IO];
452 949         2292 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     2910 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       3219 return ( $start_event, $top )
461             if DateTime->compare( $start_event->start, $e ) == 0;
462              
463             # likewise for the end time
464 948 50       83260 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       84311 return () unless $start_event->start < $e;
469              
470             # otherwise, do binary search for first event in range
471 948         83800 my ( $et, $eb ) = ( $start_event->start, $end_event->start );
472 948         1663 my $previous_index;
473 948         1428 OUTER: while (1) {
474 5067 100       15439 return $self->_scan_for_previous( $top, $e )
475             if $bottom - $top + 1 <= WINDOW / 2;
476 4148         9727 my $index = _estimate_index( $top, $bottom, $et, $eb, $e );
477 4148 100 100     18893 if ( defined $previous_index && $previous_index == $index ) {
478              
479             # search was too clever by half; we've entered an infinite loop
480 29         108 return $self->_scan_for_previous( $top, $e );
481             }
482 4119         5325 $previous_index = $index;
483 4119         5073 my $event;
484 4119         14770 for my $i ( $index .. $#$io ) {
485 5756         403318 my $line = $io->[$i];
486 5756         754298 my $ll = App::JobLog::Log::Line->parse($line);
487 5756 100       19036 if ( $ll->is_beginning ) {
488 4119         5968 my $do_next = 1;
489 4119         12260 for ( DateTime->compare( $ll->time, $e ) ) {
490 4119         373128 when ( $_ < 0 ) {
491 1837         2986 $top = $i;
492 1837         5447 $et = $ll->time;
493             }
494 2282         3938 when ( $_ > 0 ) {
495 2282         3457 $bottom = $i;
496 2282         6201 $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       24256 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   11 my ( $self, $e, $eb, $bottom, $et, $top ) = @_;
515 6         15 my $io = $self->[IO];
516              
517             # binary search for first note in range
518 6         8 my $previous_index;
519 6         10 OUTER: while (1) {
520 8 100       40 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     18 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         7 $previous_index = $index;
529 3         4 my $event;
530 3         12 for my $i ( $index .. $#$io ) {
531 3         302 my $line = $io->[$i];
532 3         536 my $ll = App::JobLog::Log::Line->parse($line);
533 3 50       14 if ( $ll->is_event ) {
534 3         122 for ( DateTime->compare( $ll->time, $e ) ) {
535 3         275 when ( $_ < 0 ) {
536 1         1 $top = $i;
537 1         5 $et = $ll->time;
538 1         6 next OUTER;
539             }
540 2         4 when ( $_ > 0 ) {
541 1         3 $bottom = $i;
542 1         4 $eb = $ll->time;
543 1         8 next OUTER;
544             }
545 1         2 default {
546              
547             # found beginning!!
548             # this should happen essentially never
549 1         6 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   2155 my ( $self, $i, $start, $end ) = @_;
561 930         1912 my $io = $self->[IO];
562              
563             # collect events
564 930         1365 my ( $previous, @events );
565 930         2974 for my $index ( $i .. $#$io ) {
566 9395         113923 my $line = $io->[$index];
567 9395         1280208 my $ll = App::JobLog::Log::Line->parse($line);
568 9395 100       29920 if ( $ll->is_endpoint ) {
569 7526 100       332594 if ($previous) {
570 5233 50       15247 $previous->end = $ll->time if $previous->is_open;
571 5233 100       15662 push @events, $previous
572             if DateTime->compare( $start, $previous->end ) < 0;
573             }
574 7526 100       492159 if ( $ll->is_beginning ) {
575 6163 100       16379 last if DateTime->compare( $ll->time, $end ) >= 0;
576 5233         469235 $previous = App::JobLog::Log::Event->new($ll);
577             }
578             else {
579 1363         8054 $previous = undef;
580             }
581             }
582             }
583 930 50 66     88966 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         11742 my @return = map { $_->overlap( $start, $end ) } @events;
  4846         13582  
590 930         9462 return \@return;
591             }
592              
593             sub _scan_for_note_from {
594 6     6   14 my ( $self, $i, $start, $end ) = @_;
595 6         13 my $io = $self->[IO];
596              
597             # collect notes
598 6         12 my @notes;
599 6         20 for my $index ( $i .. $#$io ) {
600 123         1192 my $line = $io->[$index];
601 123         18671 my $ll = App::JobLog::Log::Line->parse($line);
602 123 100       372 if ( $ll->is_event ) {
603 121 100       4628 last if $ll->time > $end;
604 115 100 100     10981 if ( $ll->is_note && $ll->time >= $start ) {
605 8         761 push @notes, App::JobLog::Log::Note->new($ll);
606             }
607             }
608             }
609 6         660 return \@notes;
610             }
611              
612             sub _scan_for_previous {
613 948     948   2022 my ( $self, $i, $e ) = @_;
614 948         1876 my $io = $self->[IO];
615              
616             # collect events
617 948         1206 my ( $previous, $previous_index );
618             OUTER: {
619 948         1833 for my $index ( $i .. $#$io ) {
  948         3389  
620 7139         109188 my $line = $io->[$index];
621 7139         822061 my $ll = App::JobLog::Log::Line->parse($line);
622 7139 100       22797 if ( $ll->is_endpoint ) {
623 5782 100 100     252487 $previous->end = $ll->time if $previous && $previous->is_open;
624 5782 100       47277 if ( $ll->time > $e ) {
625 934 50       88814 last if $previous;
626 0         0 $i--;
627 0         0 redo OUTER;
628             }
629 4848 100       456921 if ( $ll->is_beginning ) {
630 3964         13417 $previous = App::JobLog::Log::Event->new($ll);
631 3964         28672 $previous_index = $index;
632             }
633             }
634             }
635             }
636 948         10683 return $previous, $previous_index;
637             }
638              
639             sub _scan_for_previous_note {
640 6     6   16 my ( $self, $i, $e ) = @_;
641 6         13 my $io = $self->[IO];
642              
643             # collect events
644 6         11 my ( $previous, $previous_index );
645 6         21 for my $index ( $i .. $#$io ) {
646 33         660 my $line = $io->[$index];
647 33         5404 my $ll = App::JobLog::Log::Line->parse($line);
648 33 100       99 if ( $ll->is_event ) {
649 32 100       1284 last if $ll->time > $e;
650 26 100       2615 if ( $ll->is_note ) {
651 5         25 $previous = App::JobLog::Log::Note->new($ll);
652 5         24 $previous_index = $index;
653             }
654             }
655             }
656 6   66     691 return $previous_index // $i;
657             }
658              
659             # your generic O(log_n) complexity bisecting search
660             sub _estimate_index {
661 4151     4151   8366 my ( $top, $bottom, $et, $eb, $s ) = @_;
662 4151         6695 my $delta = $bottom - $top + 1;
663 4151         5595 my $i;
664 4151 100       9344 if ( $delta > WINDOW ) {
665 3782         12074 my $d1 = $s->epoch - $et->epoch;
666 3782         41019 my $d2 = $eb->epoch - $et->epoch;
667 3782         31967 my $fraction = $d1 / $d2;
668 3782 100       11547 if ( $fraction < LOW_LIM ) {
    100          
669 1312         2196 $fraction = LOW_LIM;
670             }
671             elsif ( $fraction > HIGH_LIM ) {
672 603         1186 $fraction = HIGH_LIM;
673             }
674 3782         11267 $i = sprintf '%.0f', $delta * $fraction;
675             }
676             else {
677 369         1177 $i = sprintf '%.0f', $delta / 2;
678             }
679 4151   50     9285 $i ||= 1;
680 4151         9803 return $top + $i;
681             }
682              
683              
684             sub append_event {
685 195     195 1 251539 my ( $self, @args ) = @_;
686 195 50       1746 my $current = @args == 1 ? $args[0] : App::JobLog::Log::Line->new(@args);
687 195         402 my $io = $self->[IO];
688 195         266 my $duration;
689 195 100       601 if ( $current->is_event ) {
690 194         7022 my ( $previous, $last_index ) = $self->last_event;
691 194 100       609 if ($previous) {
692              
693             # validation to prevent inconsistency
694 189 50 66     607 carp 'no currently open task'
695             if $current->is_end && $previous->is_closed;
696 189 50 33     566 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       705 $current->tags = $previous->tags if $current->tags_unspecified;
708              
709             # check for day change
710 189         602 my ($last_ts) = $self->last_ts;
711 189 100 66     801 if ( !$last_ts || _different_day( $last_ts, $current->time ) ) {
712 62         837 $io->append(
713             App::JobLog::Log::Line->new(
714             comment => $current->time->strftime(TS)
715             )
716             )->append("\n");
717             }
718 189 50       9642 if ( $previous->is_open ) {
719 189         513 $duration =
720             $current->time->subtract_datetime( $previous->start );
721 189 50       75382 $duration = undef unless $duration->in_units('days');
722             }
723             }
724             else {
725              
726             # first record in log
727 5         32 $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     6176 if ( $current->is_beginning ) {
    50          
736 193         1365 $self->[LAST_EVENT] = App::JobLog::Log::Event->new($current);
737 193         607 $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         19482 $io->append($current)->append("\n");
744 195         17951 $io->close; # flush contents
745 195         25283 return $duration;
746             }
747              
748              
749             sub append_note {
750 66     66 1 54312 my ( $self, @args ) = @_;
751 66         242 my $note = App::JobLog::Log::Line->new( time => now, @args );
752 66         313 $note->{note} = 1; # force this to be marked as a note
753 66         129 my $io = $self->[IO];
754              
755             # check for day change
756 66         235 my ($last_ts) = $self->last_ts;
757 66 100 100     284 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         2337 $io->append($note)->append("\n");
763 66         6373 $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   414 my ( $d1, $d2 ) = @_;
770 254   66     723 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     5 $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__