File Coverage

blib/lib/App/TimeClock/Daily/Report.pm
Criterion Covered Total %
statement 75 76 98.6
branch 28 28 100.0
condition 11 12 91.6
subroutine 12 12 100.0
pod 2 2 100.0
total 128 130 98.4


line stmt bran cond sub pod time code
1             package App::TimeClock::Daily::Report;
2              
3 4     4   101796 use strict;
  4         7  
  4         91  
4 4     4   15 use warnings;
  4         9  
  4         97  
5              
6 4     4   805 use POSIX qw(difftime strftime);
  4         9031  
  4         19  
7 4     4   2986 use Time::Local;
  4         3581  
  4         2944  
8              
9             my $EOL_RE = qr/[\r\n]+\z/;
10              
11             =head1 NAME
12              
13             App::TimeClock::Daily::Report
14              
15             =head1 DESCRIPTION
16              
17             Can parse the timelog and generate a report using an instance of a
18             L.
19              
20             =head2 METHODS
21              
22             =over
23              
24             =item new($timelog, $printer)
25              
26             Initializes a new L object.
27              
28             Two parameters are required:
29              
30             =over
31              
32             =item B<$timelog>
33              
34             Must point to a timelog file. Will die if not.
35              
36             =item B<$printer>
37              
38             An object derived from L. Will die if not.
39              
40             =back
41              
42             =cut
43             sub new {
44 21 100   21 1 2734 die "must supply (timelog, printer) arguments to constructor" if $#_ != 2;
45 19         27 my $class = shift;
46 19         50 my $self = {
47             timelog => shift,
48             printer => shift
49             };
50 19 100 66     494 die "timelog ($self->{timelog}) does not exist" unless -f $self->{timelog} and -r $self->{timelog};
51             die "printer is not a PrinterInterface" unless ref $self->{printer} and
52 18 100 100     214 UNIVERSAL::can($self->{printer},'isa') and $self->{printer}->isa("App::TimeClock::Daily::PrinterInterface");
      100        
53 15         46 bless $self, $class;
54             };
55              
56              
57             =item _timelocal()
58              
59             Returns a time (seconds since epoch) from a date and time.
60              
61             =cut
62             sub _timelocal {
63 89     89   1347 my ($self, $date, $time) = @_;
64 89         219 my ($year, $mon, $mday) = split(/\//, $date);
65 89         144 my ($hours, $min, $sec ) = split(/:/, $time);
66              
67 89         212 return timelocal($sec, $min, $hours, $mday, $mon-1, $year);
68             };
69              
70              
71             =item _get_report_time()
72              
73             Returns the time when the report was executed.
74              
75             =cut
76 8 100   8   167 sub _get_report_time { $_[0]->{_report_time} || time }
77              
78              
79             =item _set_report_time()
80              
81             Sets the time when the report is executed.
82              
83             Two parameters are required:
84              
85             =over
86              
87             =item B<$date>
88              
89             The date as a string in the following format YYYY/MM/DD
90              
91             =item B<$time>
92              
93             The time as a string in the following format HH:MM:SS
94              
95             =back
96              
97             =cut
98 13     13   67 sub _set_report_time { $_[0]->{_report_time} = $_[0]->_timelocal($_[1], $_[2]) }
99              
100              
101             =item _read_lines()
102              
103             Reads a set of check in and check out lines.
104              
105             If end of file is reached after reading the check in line, then
106             reading of the check out line is skipped.
107              
108             =cut
109             sub _read_lines {
110              
111 41     41   8845 my ($self, $file) = (@_);
112 41         32 my ($iline, $oline) = (undef, undef);
113              
114 41 100       103 die "Prematurely end of file." if eof($file);
115              
116 40         28 do {
117 41         408 ($iline = <$file>) =~ s/$EOL_RE//g;
118             } while ($iline =~ /^[bh] /); # Just skip unsupported codes (#13)
119              
120 40 100       105 die "Expected check in in line $." unless $iline =~ /^i /;
121              
122 39 100       83 if (not eof($file)) {
123 36         178 ($oline = <$file>) =~ s/$EOL_RE//g;
124 36 100       97 die "Excepted check out in line $." unless $oline =~ /^[oO] /; # Note: O is treated as o
125             }
126              
127 38         78 return ($iline, $oline);
128             }
129              
130              
131             =item _parse_lines()
132              
133             Parses a set of check in and check out lines.
134              
135             The lines are split on space and should contain the following four
136             fields:
137              
138             =over
139              
140             =item state
141              
142             is either 'i' - check in or 'o' - check out.
143              
144             =cut
145              
146             =item date
147              
148             is formatted as YYYY/MM//DD
149              
150             =cut
151              
152             =item time
153              
154             is formatted as HH:MM:SS
155              
156             =cut
157              
158             =item project
159              
160             is then name of the project/task and is only required when checking in.
161              
162             =cut
163              
164             =back
165              
166             =cut
167             sub _parse_lines {
168 38     38   55 my ($self, $file) = (@_);
169 38         47 my ($iline, $oline) = $self->_read_lines($file);
170              
171 38         125 my ($idate, $itime, $iproject) = (split(/ /, $iline, 4))[1..3];
172 38 100       117 my ($odate, $otime, $oproject) = (defined $oline) ? (split(/ /, $oline, 4))[1..3] :
173             (strftime("%Y/%m/%d", localtime($self->_get_report_time)),
174             strftime("%H:%M:%S", localtime($self->_get_report_time)), "DANGLING");
175            
176 38         86 return ($idate, $itime, $iproject, $odate, $otime, $oproject);
177             }
178              
179              
180             =item execute()
181              
182             Opens the timelog file starts parsing it, looping over each day and
183             calling print_day() for each.
184              
185             =cut
186             sub execute {
187 13     13 1 631 my $self = shift;
188              
189 4 100   4   30 open (my $file, "<:encoding(UTF-8)", $self->{timelog}) or die "$!\n";
  4         4  
  4         28  
  13         367  
190              
191 12         24417 my %projects;
192 12         10 my ($current_project, $current_date, $work, $day_hours);
193 0         0 my ($day_start, $day_end);
194 12         18 my ($work_year_to_date, $day_count) = (0,0);
195              
196 12         27 $day_hours = 0;
197              
198 12         49 $self->{printer}->print_header;
199              
200 12         178 while (not eof($file)) {
201 38         152 my ($idate, $itime, $iproject, $odate, $otime, $oproject) = $self->_parse_lines($file);
202              
203 38 100       90 if (not defined $current_date) {
    100          
204             # First check in, set the current date and start time
205 11         13 $current_date = $idate;
206 11         11 $day_start = $itime;
207             } elsif ($current_date ne $idate) {
208             # It's a new day, print the current day, update totals and reset variables
209 7         30 $self->{printer}->print_day($current_date, $day_start, $day_end, $day_hours, %projects);
210              
211 7         10 $work_year_to_date += $day_hours;
212 7         9 $day_count++;
213              
214 7         9 $day_hours = 0;
215 7         9 $current_date = $idate;
216 7         7 $day_start = $itime;
217 7         10 %projects = ();
218 7         8 $day_end = "";
219             }
220              
221 38         27 $current_project = $iproject;
222 38         61 $work = difftime($self->_timelocal($odate, $otime), $self->_timelocal($idate, $itime)) / 60 / 60;
223 38         998 $day_hours += $work;
224 38         38 $day_end = $otime;
225 38         64 $projects{$current_project} += $work;
226              
227 38 100 100     169 if (defined $oproject && $oproject eq "DANGLING") {
228 3         11 $projects{"$current_project (NOT checked out)"} = $projects{$current_project};
229 3         16 delete $projects{$current_project};
230             }
231             }
232              
233             # Print the last day (in the loop we're only printing when date changes)
234 12 100       29 if (defined $current_date) {
235 11         50 $self->{printer}->print_day($current_date, $day_start, $day_end, $day_hours, %projects);
236 11         13 $work_year_to_date += $day_hours;
237 11         11 $day_count++;
238             }
239              
240 12         28 $self->{printer}->print_footer($work_year_to_date, $day_count);
241             };
242             1;
243              
244             =back
245              
246             =for text
247             =encoding utf-8
248             =end
249              
250             =head1 AUTHOR
251              
252             Søren Lund, C<< >>
253              
254             =head1 SEE ALSO
255              
256             L
257              
258             =head1 COPYRIGHT
259              
260             Copyright (C) 2012-2015 Søren Lund
261              
262             This file is part of App::TimeClock.
263              
264             App::TimeClock is free software: you can redistribute it and/or modify
265             it under the terms of the GNU General Public License as published by
266             the Free Software Foundation, either version 3 of the License, or
267             (at your option) any later version.
268              
269             App::TimeClock is distributed in the hope that it will be useful,
270             but WITHOUT ANY WARRANTY; without even the implied warranty of
271             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
272             GNU General Public License for more details.
273              
274             You should have received a copy of the GNU General Public License
275             along with App::TimeClock. If not, see .