File Coverage

blib/lib/App/JobLog/Log/Line.pm
Criterion Covered Total %
statement 157 165 95.1
branch 52 74 70.2
condition 10 25 40.0
subroutine 28 28 100.0
pod 21 22 95.4
total 268 314 85.3


line stmt bran cond sub pod time code
1             package App::JobLog::Log::Line;
2             $App::JobLog::Log::Line::VERSION = '1.041';
3             # ABSTRACT: encapsulates one line of log text
4              
5              
6 5     5   4652 use Modern::Perl;
  5         12727  
  5         29  
7 5     5   1576 use Class::Autouse qw{DateTime};
  5         6834  
  5         36  
8 5     5   2074 use autouse 'App::JobLog::Time' => qw(now tz);
  5         734  
  5         36  
9              
10             # represents a single non-comment line in the log
11             # not using Moose to keep CLI snappy
12              
13             # to_string method for convenience
14 5     5   2086 use overload '""' => \&to_string;
  5         1071  
  5         41  
15 5     5   401 use overload 'bool' => sub { 1 };
  5     4358   10  
  5         28  
  4358         20219  
16              
17             # some global variables for use in BNF regex
18             our ( $date, @tags, @description, $is_beginning, $is_note );
19              
20             # log line parser
21             our $re = qr{
22             ^ (?&ts) (?&non_ts) $
23             (?(DEFINE)
24 24472         180755 (? (\d{4}\s++\d++\s++\d++\s++\d++\s++\d++\s++\d++) (?{$date = $^N}) )
25             (? (?¬e) | (?&duration_mark) )
26             (? : (?: (?&done) | (?&event) ) )
27             (? DONE )
28 203         1301 (? (?&event) (?{$is_note = 1}) )
29 20363         143456 (? (?&tags) : (?&descriptions) (?{$is_beginning = 1}) )
30             (? (?:(?&tag)(\s++(?&tag))*+)?)
31 19704         446183 (? ((?:[^\s:\\]|(?&escaped))++) (?{push @tags, $^N}))
32             (? \\.)
33             (? (?: (?&description) (?: ; \s*+ (?&description) )*+ )? )
34 18545         89028 (? ((?:[^;\\]|(?&escaped))++) (?{push @description, $^N}))
35             )
36             }xi;
37              
38              
39             sub new {
40 568     568 1 139653 my ( $class, @args ) = @_;
41 568   33     2429 $class = ref $class || $class;
42 568         1688 my %opts = @args;
43              
44             # validate %opts
45 568         1185 my $self = bless {}, $class;
46 568 100       2470 if ( exists $opts{comment} ) {
    100          
    100          
    50          
47 73         204 $self->{comment} = $opts{comment};
48 73         137 delete $opts{comment};
49 73 50       199 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
50             }
51             elsif ( exists $opts{done} ) {
52 1         4 my $time = $opts{time};
53 1 50 33     5 die "invalid value for time: $time"
54             if $time && ref $time ne 'DateTime';
55 1   33     48 $self->{time} = $time || now;
56 1         38 $self->{done} = 1;
57 1         3 delete $opts{done};
58 1         2 delete $opts{time};
59 1 50       6 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
60             }
61             elsif ( exists $opts{time} ) {
62 493         819 my $time = $opts{time};
63 493 50 33     1579 die "invalid value for time: $time"
64             if $time && ref $time ne 'DateTime';
65 493         21396 $self->{time} = $time;
66 493         799 my $tags = $opts{tags};
67 493 50 66     1344 die 'invalid value for tags: ' . $tags
68             if defined $tags && ref $tags ne 'ARRAY';
69 493 100       1176 unless ($tags) {
70 486         804 $tags = [];
71 486         1108 $self->{tags_unspecified} = 1;
72             }
73 493         905 $self->{tags} = $tags;
74 493         847 my $description = $opts{description};
75 493 100       1494 if ( my $type = ref $description ) {
    100          
76 1 50       5 die 'invalid type for description: ' . $type
77             unless $type eq 'ARRAY';
78 1         4 $self->{description} = $description;
79             }
80             elsif ( defined $description ) {
81              
82             # normalize whitespace; this is useful for testing
83 401         1654 $description =~ s/^\s++|\s++$//g;
84 401         760 $description =~ s/\s++/ /g;
85            
86 401         887 $description = [$description];
87             }
88             else {
89 91         142 $description = [];
90             }
91 493         927 $self->{description} = $description;
92 493         1213 delete @opts{qw(time tags description)};
93 493 50       1454 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
94             }
95             elsif ( exists $opts{text} ) {
96 0 0       0 die 'text lines in log must be blank' if $opts{text} =~ /\S/;
97 0         0 $self->{text} = $opts{text} . '';
98 0         0 delete $opts{text};
99 0 0       0 die 'inconsistent arguments: ' . join( ', ', @args ) if keys %opts;
100             }
101 568         2228 return $self;
102             }
103              
104              
105             sub parse {
106 28745     28745 1 1189803 my ( $class, $text ) = @_;
107 28745         110358 my $obj = bless { text => $text }, $class;
108 28745 100       142192 if ( $text =~ /^\s*(?:#\s*+(.*?)\s*)?$/ ) {
109 4271 100       14200 if ( defined $1 ) {
110 4124         10571 $obj->{comment} = $1;
111 4124         8474 delete $obj->{text};
112             }
113 4271         12135 return $obj;
114             }
115 24474         54185 local ( $date, @tags, @description, $is_beginning, $is_note );
116 24474 100       176420 if ( $text =~ $re ) {
117              
118             # must use to_string to obtain text
119 24470         47686 delete $obj->{text};
120 24470         152146 my @time = split /\s++/, $date;
121 24470         90675 $date = DateTime->new(
122             year => $time[0],
123             month => $time[1],
124             day => $time[2],
125             hour => $time[3],
126             minute => $time[4],
127             second => $time[5],
128             time_zone => tz,
129             );
130 24470         10121202 $obj->{time} = $date;
131 24470 100       51668 if ($is_beginning) {
132 20363         34625 my %tags = map { $_ => 1 } @tags;
  19702         74669  
133             $obj->{tags} =
134 20363         53865 [ map { ( my $v = $_ ) =~ s/\\(.)/$1/g; $v } sort keys %tags ];
  19702         37914  
  19702         63791  
135             $obj->{description} = [
136             map {
137 20363         39507 ( my $v = $_ ) =~ s/\\(.)/$1/g;
  18545         84329  
138 18545         194437 $v =~ s/^\s++|\s++$//g;
139 18545         126583 $v =~ s/\s++/ /g;
140 18545         59037 $v
141             } @description
142             ];
143 20363 100       67000 $obj->{note} = 1 if $is_note;
144             }
145             else {
146 4107         8319 $obj->{done} = 1;
147             }
148 24470         121848 return $obj;
149             }
150             else {
151 4         57 $obj->{malformed} = 1;
152             }
153 4         24 return $obj;
154             }
155              
156              
157             sub clone {
158 1085     1085 1 1742 my ($self) = @_;
159 1085         2642 my $clone = bless {}, ref $self;
160 1085 50       2707 if ( $self->is_malformed ) {
    50          
161 0         0 $clone->{malformed} = 1;
162 0         0 $clone->text = $self->text;
163             }
164             elsif ( $self->is_event ) {
165 1085         40550 $clone->time = $self->time->clone;
166 1085 50       3761 if ( $self->is_beginning ) {
167 1085 50       2556 $clone->{note} = 1 if $self->is_note;
168 1085         1634 $clone->tags = [ @{ $self->tags } ];
  1085         2362  
169 1085         1667 $clone->description = [ @{ $self->description } ];
  1085         2300  
170             }
171             }
172             else {
173 0 0       0 $clone->comment = $self->comment if $self->is_comment;
174 0 0       0 $clone->text = $self->text if exists $self->{text};
175             }
176 1085         5026 return $clone;
177             }
178              
179              
180             sub to_string {
181 496     496 1 136270 my ($self) = @_;
182 496 100       1444 return $self->{text} if exists $self->{text};
183 495 100       1105 if ( $self->is_event ) {
    100          
184 412         15573 my $text = $self->time_stamp;
185 412 100       9138 $text .= $self->is_note ? '' : ':';
186 412 100       879 if ( $self->is_beginning ) {
187 410   50     847 $self->tags ||= [];
188 410         487 my %tags = map { $_ => 1 } @{ $self->tags };
  16         71  
  410         771  
189 410         1044 $text .= join ' ', map { s/([:\\\s])/\\$1/g; $_ } sort keys %tags;
  16         63  
  16         54  
190 410         615 $text .= ':';
191 410   50     780 $self->description ||= [];
192             $text .= join ';',
193 410         926 map { ( my $d = $_ ) =~ s/([;\\])/\\$1/g; $d }
  410         1286  
194 410         533 @{ $self->description };
  410         715  
195             }
196             else {
197 2         5 $text .= 'DONE';
198             }
199 412         3341 return $text;
200             }
201             elsif ( $self->is_comment ) {
202 82         235 return '# ' . $self->comment;
203             }
204             }
205              
206              
207             sub time_stamp {
208 412     412 1 756 my ( $self, $time ) = @_;
209 412   33     1494 $time ||= $self->time;
210 412         8448 return sprintf '%d %2s %2s %2s %2s %2s', $time->year, $time->month,
211             $time->day, $time->hour,
212             $time->minute, $time->second;
213             }
214              
215             # a bunch of attributes, here for convenience
216              
217              
218             sub text : lvalue {
219 2     2 1 9 $_[0]->{text};
220             }
221              
222              
223             sub tags : lvalue {
224 3630     3630 1 12100 $_[0]->{tags};
225             }
226              
227              
228             sub comment : lvalue {
229 83     83 1 743 $_[0]->{comment};
230             }
231              
232              
233             sub time : lvalue {
234 51322     51322 1 221258 $_[0]->{time};
235             }
236              
237              
238             sub description : lvalue {
239 3202     3202 1 11049 $_[0]->{description};
240             }
241              
242             # a bunch of tests
243              
244              
245 1096     1096 0 4893 sub is_malformed { exists $_[0]->{malformed} }
246              
247              
248 25550     25550 1 113602 sub is_beginning { exists $_[0]->{tags} }
249              
250              
251 221     221 1 1373 sub is_end { $_[0]->{done} }
252              
253              
254 1857     1857 1 7497 sub is_note { $_[0]->{note} }
255              
256              
257 2467     2467 1 11527 sub is_event { $_[0]->{time} }
258              
259              
260 16997 100   16997 1 66615 sub is_endpoint { $_[0]->{time} && !$_[0]->{note} }
261              
262              
263 85     85 1 423 sub is_comment { exists $_[0]->{comment} }
264              
265              
266 189     189 1 903 sub tags_unspecified { $_[0]->{tags_unspecified} }
267              
268              
269             sub is_blank {
270 1   33 1 1 11 !( $_[0]->is_malformed || $_[0]->is_comment || $_[0]->is_event );
271             }
272              
273             # some useful methods
274              
275              
276             sub comment_out {
277 3     3 1 4 my ($self) = @_;
278 3         10 my $text = $self->to_string;
279 3         17 delete $self->{$_} for keys %$self;
280 3         7 $self->{comment} = $text;
281 3         12 return $self;
282             }
283              
284              
285             sub all_tags {
286 436     436 1 2528 my ( $self, @tags ) = @_;
287 436 50       833 return unless $self->tags;
288 436         502 my %tags = map { $_ => 1 } @{ $self->{tags} };
  12         25  
  436         787  
289 436         675 for my $tag (@tags) {
290 7 100       25 return unless $tags{$tag};
291             }
292 435         1951 return 1;
293             }
294              
295              
296             sub exists_tag {
297 2     2 1 5 my ( $self, @tags ) = @_;
298 2 50       5 return unless $self->tags;
299 2         3 my %tags = map { $_ => 1 } @{ $self->{tags} };
  12         25  
  2         6  
300 2         6 for my $tag (@tags) {
301 2 100       11 return 1 if $tags{$tag};
302             }
303 1         6 return;
304             }
305              
306             1;
307              
308             __END__