File Coverage

blib/lib/App/JobLog/Log/Format.pm
Criterion Covered Total %
statement 174 201 86.5
branch 60 88 68.1
condition 35 61 57.3
subroutine 20 20 100.0
pod 3 5 60.0
total 292 375 77.8


line stmt bran cond sub pod time code
1             package App::JobLog::Log::Format;
2             $App::JobLog::Log::Format::VERSION = '1.041';
3             # ABSTRACT: pretty printer for log
4              
5              
6 2     2   1359 use Exporter 'import';
  2         5  
  2         103  
7             our @EXPORT_OK = qw(
8             display
9             duration
10             single_interval
11             summary
12             wrap
13             );
14              
15 2     2   12 use Modern::Perl;
  2         5  
  2         13  
16 2         123 use App::JobLog::Config qw(
17             day_length
18             is_workday
19             precision
20 2     2   236 );
  2         4  
21 2     2   9 use App::JobLog::Log::Synopsis qw(collect :merge);
  2         4  
  2         330  
22 2     2   1437 use Text::WrapI18N qw();
  2         4834  
  2         50  
23 2     2   1503 use App::JobLog::TimeGrammar qw(parse);
  2         5  
  2         122  
24              
25 2     2   16 use constant TAG_COLUMN_LIMIT => 10;
  2         2  
  2         125  
26 2     2   10 use constant MARGIN => 5;
  2         6  
  2         1739  
27              
28             # minimum width of description column
29 2     2   12 use constant MIN_WIDTH => 20;
  2         6  
  2         114  
30 2     2   12 use constant DURATION_FORMAT => '%0.' . precision . 'f';
  2         4  
  2         9  
31              
32              
33             sub summary {
34 15     15 0 413 my ( $phrase, $test, $hidden, $do_notes ) = @_;
35              
36             # we skip flex days if the events are at all filtered
37 15   66     115 my $skip_flex = !$do_notes && ( $test || 0 );
38 15   100 197   120 $test //= sub { $_[0] };
  197         734  
39 15         119 my ( $start, $end ) = parse $phrase;
40 15         57 my $show_year = $start->year < $end->year;
41 15 100       137 unless ($skip_flex) {
42              
43             # if we are chopping off any of the first and last days we ignore flex time
44 9 100 66     60 $skip_flex = 1
      66        
45             unless $do_notes || _break_of_dawn($start) && _witching_hour($end);
46             }
47 15 100       167 my $method = $do_notes ? 'find_notes' : 'find_events';
48 15         128 my $events = App::JobLog::Log->new->$method( $start, $end );
49 15         331 my @days = @{ _days( $start, $end, $skip_flex, $do_notes ) };
  15         1785  
50             my @periods =
51 15 100       728 $hidden->{vacation} ? () : App::JobLog::Vacation->new->periods;
52              
53             # drop the vacation days that can't be relevant
54 15 100       87 unless ( $hidden->{vacation} ) {
55 13         117 my $e =
56             App::JobLog::Log::Event->new(
57             App::JobLog::Log::Line->new( time => $start ) );
58 13         67 $e->end = $end;
59 13         84 for ( my $i = 0 ; $i < @periods ; $i++ ) {
60 0         0 my $p = $periods[$i];
61 0 0 0     0 if ( $skip_flex && $p->flex || !$p->conflicts($e) ) {
      0        
62 0         0 splice @periods, $i, 1;
63 0         0 $i--;
64             }
65             }
66             }
67              
68             # collect events into days
69 15         29 my @gathered;
70 15         108 for my $big_e (@$events) {
71 203         603 for my $e ( $big_e->split_days ) {
72 205 100       450 if ( $e = $test->($e) ) {
73 203   100     963 push @gathered, shift @days
74             while @days && $days[0]->end < $e->start;
75 203         18155 for my $d (@days) {
76 233 100       3280 if ( $e->intersects( $d->pseudo_event ) ) {
77 200         16825 push @{ $d->events }, $e;
  200         565  
78 200         840 last;
79             }
80              
81             # I believe these is_open bits are mistaken
82             # last if $e->is_open;
83 33 50       3111 unless ($do_notes) {
84 33 50       97 last if $d->start > $e->end;
85             }
86             }
87             }
88             }
89             }
90 15         213 unshift @days, @gathered;
91              
92             # add in vacation times
93 15         41 for my $p (@periods) {
94 0         0 for my $d (@days) {
95 0 0 0     0 if ( is_workday( $d->start ) && $p->conflicts( $d->pseudo_event ) )
96             {
97 0         0 my $clone = $p->clone;
98 0 0       0 if ( $clone->fixed ) {
99 0         0 push @{ $d->events }, $clone->overlap( $d->start, $d->end );
  0         0  
100             }
101             else {
102 0         0 $clone->start = $d->start->clone;
103 0 0       0 if ( $clone->flex ) {
104 0         0 $d->{deferred} = $clone;
105             }
106             else {
107 0         0 $clone->end =
108             $clone->start->clone->add( hours => day_length );
109 0         0 push @{ $d->vacation }, $clone;
  0         0  
110             }
111             }
112             }
113             }
114             }
115              
116             # delete empty days
117 15         72 for ( my $i = 0 ; $i < @days ; $i++ ) {
118 11169         77385 my $d = $days[$i];
119 11169 100 100     28164 if ( $d->is_empty && !is_workday( $d->start ) ) {
120 3169         24192 splice @days, $i, 1;
121 3169         24119 $i--;
122             }
123             }
124              
125             # fix deferred flex time and ensure events are chronologically ordered
126 15         49 for my $d (@days) {
127 8000         16615 my $flex = $d->{deferred};
128 8000         9334 my @events = @{ $d->events };
  8000         18216  
129 8000 50       17104 if ($flex) {
130 0         0 delete $d->{deferred};
131 0         0 my $tr = $d->time_remaining;
132 0 0       0 if ( $tr > 0 ) {
133 0         0 $flex->end = $flex->start->clone->add( seconds => $tr );
134 0         0 push @events, $flex;
135             }
136             }
137 8000 100       19421 $d->{events} = [ sort { $a->cmp($b) } @events ] if @events > 1;
  155         363  
138             }
139              
140 15         374 return \@days, $show_year;
141             }
142              
143             # whether the date is the first moment in its day
144             sub _break_of_dawn {
145 7     7   14 my ($date) = @_;
146 7   66     28 return $date->hour == 0 && $date->minute == 0 && $date->second == 0;
147             }
148              
149             # whether the date is the last moment in its day
150             sub _witching_hour {
151 2     2   55 my ($date) = @_;
152 2   33     13 return $date->hour == 23 && $date->minute == 59 && $date->second == 59;
153             }
154              
155             # create a list of days about which we wish to collect information
156             sub _days {
157 15     15   44 my ( $start, $end, $skip_flex, $doing_notes ) = @_;
158 15         22 my @days;
159 15         24 my $b1 = $start;
160 15         64 my $b2 = $start->clone->add( days => 1 )->truncate( to => 'day' );
161 15         23760 while ( $b2 < $end ) {
162 11154 100       11354758 push @days,
163             App::JobLog::Log::Day->new(
164             start => $b1,
165             end => $b2,
166             skip_flex => $skip_flex,
167             $doing_notes ? ( notes => 1 ) : (),
168             );
169 11154         21135 $b1 = $b2;
170 11154         32565 $b2 = $b2->clone->add( days => 1 );
171             }
172 15 100       9731 push @days,
173             App::JobLog::Log::Day->new(
174             start => $b1,
175             end => $end,
176             skip_flex => $skip_flex,
177             $doing_notes ? ( notes => 1 ) : (),
178             );
179 15         4267 return \@days;
180             }
181              
182              
183             sub display {
184 15     15 1 394 my ( $days, $merge_level, $hidden, $screen_width, $show_year ) = @_;
185              
186 15 50       64 if (@$days) {
187 15         344 collect $_, $merge_level for @$days;
188 15         138 my @synopses = map { @{ $_->synopses } } @$days;
  8000         8225  
  8000         18221  
189              
190             my $columns = {
191             time => single_interval($merge_level) && !$hidden->{time},
192             date => !$hidden->{date},
193             tags => !$hidden->{tags},
194             description => !$hidden->{description},
195             duration => !$hidden->{duration},
196 15   33     154 };
197 15   66     155 $show_year &&= $columns->{date};
198 15         133 my $format = _define_format( \@synopses, $columns, $screen_width );
199              
200             # keep track of various durations
201 15         242 my $times = {
202             total => 0,
203             untagged => 0,
204             expected => 0,
205             vacation => 0,
206             tags => {}
207             };
208              
209             # display synopses and add up durations
210 15         73 for my $d (@$days) {
211 8000         22676 $d->times($times);
212 8000         68245 $d->display( $format, $columns, $screen_width, $show_year );
213             }
214              
215 15 100       250 unless ( $hidden->{totals} ) {
216             my ( $m1, $m2 ) =
217 13         73 ( length 'TOTAL HOURS', length duration( $times->{total} ) );
218 13         83 my @keys = keys %{ $times->{tags} };
  13         150  
219 13 100       78 push @keys, 'UNTAGGED' if $times->{untagged};
220 13 50       53 push @keys, 'VACATION' if $times->{vacation};
221 13         71 for my $tag (@keys) {
222 14         31 my $l = length $tag;
223 14 50       78 $m1 = $l if $l > $m1;
224             }
225 13         71 $format = sprintf " %%-%ds %%%ds\n", $m1, $m2;
226 13         55 printf $format, 'TOTAL HOURS', duration( $times->{total} );
227             printf $format, 'VACATION', duration( $times->{vacation} )
228 13 50       446 if $times->{vacation};
229 13 100       34 if ( %{ $times->{tags} } ) {
  13         136  
230             printf $format, 'UNTAGGED', duration( $times->{untagged} )
231 11 50       63 if $times->{untagged};
232 11         31 for my $key ( sort keys %{ $times->{tags} } ) {
  11         72  
233 12         125 my $d = $times->{tags}{$key};
234 12         53 printf $format, $key, duration($d);
235             }
236             }
237             }
238             }
239             else {
240 0         0 say 'No events in interval specified.';
241             }
242             }
243              
244             # generate printf format for synopses
245             # returns format and wrap widths for tags and descriptions
246             sub _define_format {
247 15     15   44 my ( $synopses, $hash, $screen_width ) = @_;
248              
249             #determine maximum width of each column
250 15         47 my $widths;
251 15         82 for my $s (@$synopses) {
252 76 50       209 if ( $hash->{tags} ) {
253 76   50     422 my $w1 = $hash->{widths}{tags} || 0;
254 76         307 my $ts = $s->tag_string;
255 76 50 66     368 if ( $screen_width > -1 && length $ts > TAG_COLUMN_LIMIT ) {
256 0         0 my $wrapped = wrap( $ts, TAG_COLUMN_LIMIT );
257 0         0 $ts = '';
258 0         0 for my $line (@$wrapped) {
259 0 0       0 $ts = $line if length $line > length $ts;
260             }
261             }
262 76         118 my $w2 = length $ts;
263 76 100       221 $hash->{widths}{tags} = $w2 if $w2 > $w1;
264             }
265 76 50       196 if ( $hash->{time} ) {
266 76   100     272 my $w1 = $hash->{widths}{time} || 0;
267 76         259 my $w2 = length $s->time_fmt;
268 76 100       332 $hash->{widths}{time} = $w2 if $w2 > $w1;
269             }
270 76 100       233 if ( $hash->{duration} ) {
271 44   100     217 my $w1 = $hash->{widths}{duration} || 0;
272 44         156 my $w2 = length duration( $s->duration );
273 44 100       206 $hash->{widths}{duration} = $w2 if $w2 > $w1;
274             }
275             }
276 15         46 my $margins = 0;
277 15 100 33     186 if ( $hash->{tags} && $hash->{widths}{tags} ) {
278 11         39 $margins++;
279 11         148 $hash->{formats}{tags} = sprintf '%%-%ds', $hash->{widths}{tags};
280              
281             # there seems to be a bug in Text::Wrap that requires tinkering with the column width
282 11         50 $hash->{widths}{tags}++;
283             }
284 15 50 33     145 if ( $hash->{time} && $hash->{widths}{time} ) {
285 15         39 $margins++;
286 15         107 $hash->{formats}{time} = sprintf '%%%ds', $hash->{widths}{time};
287             }
288 15 50 66     131 if ( $hash->{duration} && $hash->{widths}{duration} ) {
289 13         22 $margins++;
290 13         68 $hash->{formats}{duration} = sprintf '%%%ds', $hash->{widths}{duration};
291             }
292 15 50       53 if ( $hash->{description} ) {
293 15 100       63 if ( $screen_width == -1 ) {
294 3         7 $hash->{formats}{description} = '%s';
295             }
296             else {
297 12         35 $margins++;
298 12         35 my $max_description = $screen_width;
299 12         76 for my $col (qw(time duration tags)) {
300 36   100     184 $max_description -= $hash->{widths}{$col} || 0;
301             }
302 12         31 $max_description -= $margins * 2; # left margins
303 12         37 $max_description -= MARGIN; # margin on the right
304 12 50       61 $max_description = MIN_WIDTH if $max_description < MIN_WIDTH;
305 12         50 $hash->{widths}{description} = $max_description;
306 12         109 $hash->{formats}{description} = sprintf '%%-%ds', $max_description;
307             }
308             }
309              
310 15         41 my $format = '';
311 15         37 for my $col (qw(time duration tags description)) {
312 60         120 my $f = $hash->{formats}{$col};
313 60 100       212 $format .= " $f" if $f;
314             }
315 15         81 return $format;
316             }
317              
318              
319             sub wrap {
320 24     24 1 92 my ( $text, $columns ) = @_;
321 24         59 my @ar;
322 24         82 eval {
323 24         86 $Text::WrapI18N::columns = $columns;
324 24         213 my $s = Text::WrapI18N::wrap( '', '', $text );
325 24         41746 @ar = $s =~ /^.*$/mg;
326             };
327 24 50       241 return \@ar unless $@;
328 0         0 return [$text];
329             }
330              
331              
332             sub single_interval {
333 15 100 66 15 0 895 $_[0] == MERGE_ADJACENT
334             || $_[0] == MERGE_ADJACENT_SAME_TAGS
335             || $_[0] == MERGE_NONE;
336             }
337              
338              
339 126     126 1 2584 sub duration { sprintf DURATION_FORMAT, $_[0] / ( 60 * 60 ) }
340              
341             1;
342              
343             __END__