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.039';
3             # ABSTRACT: pretty printer for log
4              
5              
6 2     2   1339 use Exporter 'import';
  2         5  
  2         100  
7             our @EXPORT_OK = qw(
8             display
9             duration
10             single_interval
11             summary
12             wrap
13             );
14              
15 2     2   10 use Modern::Perl;
  2         5  
  2         22  
16 2         121 use App::JobLog::Config qw(
17             day_length
18             is_workday
19             precision
20 2     2   251 );
  2         4  
21 2     2   10 use App::JobLog::Log::Synopsis qw(collect :merge);
  2         5  
  2         340  
22 2     2   1575 use Text::WrapI18N qw();
  2         4931  
  2         51  
23 2     2   1581 use App::JobLog::TimeGrammar qw(parse);
  2         6  
  2         124  
24              
25 2     2   16 use constant TAG_COLUMN_LIMIT => 10;
  2         4  
  2         131  
26 2     2   10 use constant MARGIN => 5;
  2         4  
  2         1723  
27              
28             # minimum width of description column
29 2     2   12 use constant MIN_WIDTH => 20;
  2         4  
  2         110  
30 2     2   11 use constant DURATION_FORMAT => '%0.' . precision . 'f';
  2         5  
  2         11  
31              
32              
33             sub summary {
34 15     15 0 407 my ( $phrase, $test, $hidden, $do_notes ) = @_;
35              
36             # we skip flex days if the events are at all filtered
37 15   66     104 my $skip_flex = !$do_notes && ( $test || 0 );
38 15   100 197   129 $test //= sub { $_[0] };
  197         779  
39 15         123 my ( $start, $end ) = parse $phrase;
40 15         75 my $show_year = $start->year < $end->year;
41 15 100       136 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     62 $skip_flex = 1
      66        
45             unless $do_notes || _break_of_dawn($start) && _witching_hour($end);
46             }
47 15 100       156 my $method = $do_notes ? 'find_notes' : 'find_events';
48 15         133 my $events = App::JobLog::Log->new->$method( $start, $end );
49 15         336 my @days = @{ _days( $start, $end, $skip_flex, $do_notes ) };
  15         1794  
50             my @periods =
51 15 100       707 $hidden->{vacation} ? () : App::JobLog::Vacation->new->periods;
52              
53             # drop the vacation days that can't be relevant
54 15 100       75 unless ( $hidden->{vacation} ) {
55 13         117 my $e =
56             App::JobLog::Log::Event->new(
57             App::JobLog::Log::Line->new( time => $start ) );
58 13         70 $e->end = $end;
59 13         90 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         28 my @gathered;
70 15         45 for my $big_e (@$events) {
71 203         755 for my $e ( $big_e->split_days ) {
72 205 100       514 if ( $e = $test->($e) ) {
73 203   100     932 push @gathered, shift @days
74             while @days && $days[0]->end < $e->start;
75 203         18421 for my $d (@days) {
76 233 100       3492 if ( $e->intersects( $d->pseudo_event ) ) {
77 200         17393 push @{ $d->events }, $e;
  200         575  
78 200         793 last;
79             }
80              
81             # I believe these is_open bits are mistaken
82             # last if $e->is_open;
83 33 50       3171 unless ($do_notes) {
84 33 50       106 last if $d->start > $e->end;
85             }
86             }
87             }
88             }
89             }
90 15         179 unshift @days, @gathered;
91              
92             # add in vacation times
93 15         42 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         65 for ( my $i = 0 ; $i < @days ; $i++ ) {
118 11085         74822 my $d = $days[$i];
119 11085 100 100     29179 if ( $d->is_empty && !is_workday( $d->start ) ) {
120 3145         23163 splice @days, $i, 1;
121 3145         23471 $i--;
122             }
123             }
124              
125             # fix deferred flex time and ensure events are chronologically ordered
126 15         41 for my $d (@days) {
127 7940         15458 my $flex = $d->{deferred};
128 7940         9215 my @events = @{ $d->events };
  7940         19145  
129 7940 50       16058 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 7940 100       19683 $d->{events} = [ sort { $a->cmp($b) } @events ] if @events > 1;
  155         399  
138             }
139              
140 15         395 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     30 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   67 my ($date) = @_;
152 2   33     14 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   41 my ( $start, $end, $skip_flex, $doing_notes ) = @_;
158 15         25 my @days;
159 15         27 my $b1 = $start;
160 15         68 my $b2 = $start->clone->add( days => 1 )->truncate( to => 'day' );
161 15         23642 while ( $b2 < $end ) {
162 11070 100       11496106 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 11070         22319 $b1 = $b2;
170 11070         34932 $b2 = $b2->clone->add( days => 1 );
171             }
172 15 100       10113 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         3843 return \@days;
180             }
181              
182              
183             sub display {
184 15     15 1 313 my ( $days, $merge_level, $hidden, $screen_width, $show_year ) = @_;
185              
186 15 50       138 if (@$days) {
187 15         319 collect $_, $merge_level for @$days;
188 15         140 my @synopses = map { @{ $_->synopses } } @$days;
  7940         9353  
  7940         19672  
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     93 };
197 15   66     111 $show_year &&= $columns->{date};
198 15         90 my $format = _define_format( \@synopses, $columns, $screen_width );
199              
200             # keep track of various durations
201 15         232 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         68 for my $d (@$days) {
211 7940         22702 $d->times($times);
212 7940         64603 $d->display( $format, $columns, $screen_width, $show_year );
213             }
214              
215 15 100       261 unless ( $hidden->{totals} ) {
216             my ( $m1, $m2 ) =
217 13         66 ( length 'TOTAL HOURS', length duration( $times->{total} ) );
218 13         45 my @keys = keys %{ $times->{tags} };
  13         106  
219 13 100       80 push @keys, 'UNTAGGED' if $times->{untagged};
220 13 50       62 push @keys, 'VACATION' if $times->{vacation};
221 13         34 for my $tag (@keys) {
222 14         45 my $l = length $tag;
223 14 50       66 $m1 = $l if $l > $m1;
224             }
225 13         80 $format = sprintf " %%-%ds %%%ds\n", $m1, $m2;
226 13         68 printf $format, 'TOTAL HOURS', duration( $times->{total} );
227             printf $format, 'VACATION', duration( $times->{vacation} )
228 13 50       412 if $times->{vacation};
229 13 100       38 if ( %{ $times->{tags} } ) {
  13         126  
230             printf $format, 'UNTAGGED', duration( $times->{untagged} )
231 11 50       61 if $times->{untagged};
232 11         52 for my $key ( sort keys %{ $times->{tags} } ) {
  11         70  
233 12         65 my $d = $times->{tags}{$key};
234 12         52 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   34 my ( $synopses, $hash, $screen_width ) = @_;
248              
249             #determine maximum width of each column
250 15         34 my $widths;
251 15         73 for my $s (@$synopses) {
252 76 50       209 if ( $hash->{tags} ) {
253 76   50     405 my $w1 = $hash->{widths}{tags} || 0;
254 76         250 my $ts = $s->tag_string;
255 76 50 66     335 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         106 my $w2 = length $ts;
263 76 100       235 $hash->{widths}{tags} = $w2 if $w2 > $w1;
264             }
265 76 50       190 if ( $hash->{time} ) {
266 76   100     237 my $w1 = $hash->{widths}{time} || 0;
267 76         241 my $w2 = length $s->time_fmt;
268 76 100       331 $hash->{widths}{time} = $w2 if $w2 > $w1;
269             }
270 76 100       225 if ( $hash->{duration} ) {
271 44   100     219 my $w1 = $hash->{widths}{duration} || 0;
272 44         163 my $w2 = length duration( $s->duration );
273 44 100       202 $hash->{widths}{duration} = $w2 if $w2 > $w1;
274             }
275             }
276 15         50 my $margins = 0;
277 15 100 33     167 if ( $hash->{tags} && $hash->{widths}{tags} ) {
278 11         32 $margins++;
279 11         170 $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         62 $hash->{widths}{tags}++;
283             }
284 15 50 33     149 if ( $hash->{time} && $hash->{widths}{time} ) {
285 15         33 $margins++;
286 15         90 $hash->{formats}{time} = sprintf '%%%ds', $hash->{widths}{time};
287             }
288 15 50 66     134 if ( $hash->{duration} && $hash->{widths}{duration} ) {
289 13         23 $margins++;
290 13         79 $hash->{formats}{duration} = sprintf '%%%ds', $hash->{widths}{duration};
291             }
292 15 50       55 if ( $hash->{description} ) {
293 15 100       64 if ( $screen_width == -1 ) {
294 3         10 $hash->{formats}{description} = '%s';
295             }
296             else {
297 12         29 $margins++;
298 12         48 my $max_description = $screen_width;
299 12         65 for my $col (qw(time duration tags)) {
300 36   100     188 $max_description -= $hash->{widths}{$col} || 0;
301             }
302 12         53 $max_description -= $margins * 2; # left margins
303 12         32 $max_description -= MARGIN; # margin on the right
304 12 50       64 $max_description = MIN_WIDTH if $max_description < MIN_WIDTH;
305 12         85 $hash->{widths}{description} = $max_description;
306 12         94 $hash->{formats}{description} = sprintf '%%-%ds', $max_description;
307             }
308             }
309              
310 15         59 my $format = '';
311 15         41 for my $col (qw(time duration tags description)) {
312 60         119 my $f = $hash->{formats}{$col};
313 60 100       210 $format .= " $f" if $f;
314             }
315 15         50 return $format;
316             }
317              
318              
319             sub wrap {
320 24     24 1 76 my ( $text, $columns ) = @_;
321 24         56 my @ar;
322 24         106 eval {
323 24         82 $Text::WrapI18N::columns = $columns;
324 24         176 my $s = Text::WrapI18N::wrap( '', '', $text );
325 24         714692 @ar = $s =~ /^.*$/mg;
326             };
327 24 50       232 return \@ar unless $@;
328 0         0 return [$text];
329             }
330              
331              
332             sub single_interval {
333 15 100 66 15 0 858 $_[0] == MERGE_ADJACENT
334             || $_[0] == MERGE_ADJACENT_SAME_TAGS
335             || $_[0] == MERGE_NONE;
336             }
337              
338              
339 126     126 1 2461 sub duration { sprintf DURATION_FORMAT, $_[0] / ( 60 * 60 ) }
340              
341             1;
342              
343             __END__