File Coverage

blib/lib/App/TimeClock/Daily/Report.pm
Criterion Covered Total %
statement 69 70 98.5
branch 28 28 100.0
condition 11 12 91.6
subroutine 10 10 100.0
pod 2 2 100.0
total 120 122 98.3


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