File Coverage

blib/lib/DateTime/Format/Natural/Duration/Checks.pm
Criterion Covered Total %
statement 114 114 100.0
branch 26 28 92.8
condition 15 18 83.3
subroutine 10 10 100.0
pod n/a
total 165 170 97.0


line stmt bran cond sub pod time code
1             package DateTime::Format::Natural::Duration::Checks;
2              
3 26     26   294 use strict;
  26         149  
  26         1057  
4 26     26   159 use warnings;
  26         59  
  26         1489  
5 26     26   274 use boolean qw(true false);
  26         75  
  26         328  
6              
7             our $VERSION = '0.07';
8              
9             sub _for
10             {
11 1258     1258   4651 my ($duration, $date_strings, $present) = @_;
12              
13 1258 100 100     7909 if (@$date_strings == 1
14             && $date_strings->[0] =~ $duration->{for}{regex}
15             ) {
16 55         254 $$present = $duration->{for}{present};
17 55         239 return true;
18             }
19             else {
20 1203         4460 return false;
21             }
22             }
23              
24             sub _first_to_last
25             {
26 1203     1203   3894 my ($duration, $date_strings, $extract) = @_;
27              
28 1203         2585 my %regexes = %{$duration->{first_to_last}{regexes}};
  1203         10797  
29              
30 1203 100 100     17238 if (@$date_strings == 2
      66        
31             && $date_strings->[0] =~ /^$regexes{first}$/
32             && $date_strings->[1] =~ /^$regexes{last}$/
33             ) {
34 27         96 $$extract = $regexes{extract};
35 27         124 return true;
36             }
37             else {
38 1176         3975 return false;
39             }
40             }
41              
42             my %anchor_regex = (
43             left => sub { my $regex = shift; qr/(?:^|(?<=\s))$regex/ },
44             right => sub { my $regex = shift; qr/$regex(?:(?=\s)|$)/ },
45             both => sub { my $regex = shift; qr/(?:^|(?<=\s))$regex(?:(?=\s)|$)/ },
46             );
47              
48             my $extract_chunk = sub
49             {
50             my ($string, $base_index, $start_pos, $match) = @_;
51              
52             my $start_index = 0;
53              
54             if ($start_pos > 0
55             && $string =~ /^(.{0,$start_pos})\s+/
56             ) {
57             my $substring = $1;
58             $start_index++ while $substring =~ /\s+/g;
59             $start_index++; # final space
60             }
61             my @tokens = split /\s+/, $match;
62             my $end_index = $start_index + $#tokens;
63              
64             my $expression = join ' ', @tokens;
65              
66             return [ [ $base_index + $start_index, $base_index + $end_index ], $expression ];
67             };
68              
69             my $has_timespan_sep = sub
70             {
71             my ($tokens, $chunks, $timespan_sep) = @_;
72              
73             my ($left_index, $right_index) = ($chunks->[0]->[0][1], $chunks->[1]->[0][0]);
74              
75             if ($tokens->[$left_index + 1] =~ /^$timespan_sep$/i
76             && $tokens->[$right_index - 1] =~ /^$timespan_sep$/i
77             && $right_index - $left_index == 2
78             ) {
79             return true;
80             }
81             else {
82             return false;
83             }
84             };
85              
86             # invoked from Extract.pm
87             sub _first_to_last_extract
88             {
89 111     111   312 my $self = shift;
90 111         435 my ($date_strings, $indexes, $tokens, $chunks) = @_;
91              
92 111 50       522 return false unless @$date_strings == 2;
93              
94 111         477 my $duration = $self->{data}->{duration};
95              
96 111         254 my %regexes = %{$duration->{first_to_last}{regexes}};
  111         866  
97              
98 111         625 $regexes{first} = $anchor_regex{left}->($regexes{first});
99 111         579 $regexes{last} = $anchor_regex{right}->($regexes{last});
100              
101 111         1229 my $timespan_sep = $self->{data}->__timespan('literal');
102              
103 111         388 my @chunks;
104 111 100       1678 if ($date_strings->[0] =~ /(?=($regexes{first})$)/g) {
105 17         73 my $match = $1;
106 17         109 push @chunks, $extract_chunk->($date_strings->[0], $indexes->[0][0], pos $date_strings->[0], $match);
107             }
108 111 100       1553 if ($date_strings->[1] =~ /(?=^($regexes{last}))/g) {
109 17         77 my $match = $1;
110 17         66 push @chunks, $extract_chunk->($date_strings->[1], $indexes->[1][0], pos $date_strings->[1], $match);
111             }
112 111 100 66     696 if (@chunks == 2 && $has_timespan_sep->($tokens, \@chunks, $timespan_sep)) {
113 16         359 @$chunks = @chunks;
114 16         94 return true;
115             }
116             else {
117 95         421 return false;
118             }
119             }
120              
121             my $duration_matches = sub
122             {
123             my ($duration, $date_strings, $entry, $target) = @_;
124              
125             my $data = $duration->{from_count_to_count};
126              
127             my (@matches, %seen);
128             foreach my $ident (@{$data->{order}}) {
129             my $regex = $anchor_regex{both}->($data->{regexes}{$ident});
130             while ($date_strings->[0] =~ /(?=$regex)/g) {
131             my $pos = pos $date_strings->[0];
132             next if $seen{$pos};
133             push @matches, [ $ident, $pos ];
134             $seen{$pos} = true;
135             }
136             }
137             my @idents = map $_->[0], sort { $a->[1] <=> $b->[1] } @matches;
138              
139             my %categories;
140             foreach my $ident (@{$data->{order}}) {
141             my $category = $data->{categories}{$ident};
142             push @{$categories{$category}}, $ident;
143             }
144              
145             my $get_target = sub
146             {
147             my ($category, $target) = @_;
148             foreach my $ident (@{$categories{$category}}) {
149             my $regex = $anchor_regex{both}->($data->{regexes}{$ident});
150             if ($date_strings->[1] =~ $regex) {
151             $$target = $ident;
152             return true;
153             }
154             }
155             return false;
156             };
157              
158             if (@idents >= 2
159             && $data->{categories}{$idents[-1]} eq 'day'
160             && $data->{categories}{$idents[-2]} eq 'time'
161             && $get_target->($data->{categories}{$idents[-2]}, $target)
162             ) {
163             $$entry = $idents[-2];
164             return true;
165             }
166             elsif (@idents
167             && $get_target->($data->{categories}{$idents[-1]}, $target)
168             ) {
169             $$entry = $idents[-1];
170             return true;
171             }
172             else {
173             return false;
174             }
175             };
176              
177             sub _from_count_to_count
178             {
179 1176     1176   3741 my ($duration, $date_strings, $extract, $adjust, $indexes) = @_;
180              
181 1176 100       4409 return false unless @$date_strings == 2;
182              
183 1012         2979 my ($entry, $target);
184 1012 100       4069 return false unless $duration_matches->($duration, $date_strings, \$entry, \$target);
185              
186 908         23704 my $data = $duration->{from_count_to_count};
187              
188             my $get_data = sub
189             {
190 1307     1307   3864 my ($types, $idents, $type) = @_;
191              
192 1307         4458 my $regex = $data->{regexes}{$idents->[0]};
193 1307         28584 my %regexes = (
194             left => qr/^.+? \s+ $regex$/x,
195             right => qr/^$regex \s+ .+$/x,
196             target => qr/^$data->{regexes}{$idents->[1]}$/,
197             );
198 1307         15595 my %extract = (
199             left => qr/^(.+?) \s+ $regex$/x,
200             right => qr/^$regex \s+ (.+)$/x,
201             );
202             my %adjust = (
203             left => sub
204             {
205 617         1895 my ($date_strings, $index, $complete) = @_;
206 617         17259 $date_strings->[$index] = "$complete $date_strings->[$index]";
207             },
208             right => sub
209             {
210 282         915 my ($date_strings, $index, $complete) = @_;
211 282         2939 $date_strings->[$index] .= " $complete";
212             },
213 1307         10116 );
214              
215 1307         13068 return (@regexes{@$types}, $extract{$type}, $adjust{$type});
216 908         7693 };
217              
218 908         8961 my @sets = (
219             [ [ qw( left target) ], [ $entry, $target ], 'left', [0,1] ],
220             [ [ qw(right target) ], [ $entry, $target ], 'right', [0,1] ],
221             );
222              
223 908         2453 my @new;
224 908         3196 foreach my $set (@sets) {
225 1816         4488 push @new, [ [ reverse @{$set->[0]} ], [ reverse @{$set->[1]} ], $set->[2], [ reverse @{$set->[3]} ] ];
  1816         5827  
  1816         4780  
  1816         6866  
226             }
227 908         2584 push @sets, @new;
228              
229 908         2501 foreach my $set (@sets) {
230 1307         4191 my ($regex_types, $idents, $type, $string_indexes) = @$set;
231              
232 1307         9874 my ($regex_from, $regex_to, $extract_regex, $adjust_code) = $get_data->($regex_types, $idents, $type);
233              
234 1307 100 100     25998 if ($date_strings->[0] =~ $regex_from
235             && $date_strings->[1] =~ $regex_to
236             ) {
237 899         2469 $$extract = $extract_regex;
238 899         2048 $$adjust = $adjust_code;
239 899         3256 @$indexes = @$string_indexes;
240 899         3802 return true;
241             }
242             }
243              
244 9         55 return false;
245             }
246              
247             # invoked from Extract.pm
248             sub _from_count_to_count_extract
249             {
250 95     95   233 my $self = shift;
251 95         299 my ($date_strings, $indexes, $tokens, $chunks) = @_;
252              
253 95 50       388 return false unless @$date_strings == 2;
254              
255 95         316 my $duration = $self->{data}->{duration};
256              
257 95         336 my ($entry, $target);
258 95 100       444 return false unless $duration_matches->($duration, $date_strings, \$entry, \$target);
259              
260 60         1738 my $data = $duration->{from_count_to_count};
261              
262             my $get_data = sub
263             {
264 88     88   248 my ($types, $idents) = @_;
265              
266 88         407 my $category = $data->{categories}{$idents->[0]};
267 88         267 my $regex = $data->{regexes}{$idents->[0]};
268              
269             my %regexes = (
270             left => qr/$data->{extract}{left}{$category}\s+$regex/,
271             right => qr/$regex\s+$data->{extract}{right}{$category}/,
272 88         10951 target => $data->{regexes}{$idents->[1]},
273             );
274              
275 88         8663 $regexes{entry} = qr/(?:$regexes{left}|$regexes{right})/;
276              
277 88         846 return @regexes{@$types};
278 60         464 };
279              
280 60         653 my $timespan_sep = $self->{data}->__timespan('literal');
281              
282 60         466 my @sets = (
283             [ [ qw(entry target) ], [ $entry, $target ] ],
284             );
285              
286 60         180 my @new;
287 60         205 foreach my $set (@sets) {
288 60         170 push @new, [ [ reverse @{$set->[0]} ], [ reverse @{$set->[1]} ] ];
  60         310  
  60         278  
289             }
290 60         174 push @sets, @new;
291              
292 60         195 foreach my $set (@sets) {
293 88         311 my ($regex_types, $idents) = @$set;
294              
295 88         392 my ($regex_from, $regex_to) = $get_data->($regex_types, $idents);
296              
297 88         416 $regex_from = $anchor_regex{left}->($regex_from);
298 88         645 $regex_to = $anchor_regex{right}->($regex_to);
299              
300 88         413 my @chunks;
301 88 100       22768 if ($date_strings->[0] =~ /(?=($regex_from)$)/g) {
302 57         310 my $match = $1;
303 57         436 push @chunks, $extract_chunk->($date_strings->[0], $indexes->[0][0], pos $date_strings->[0], $match);
304             }
305 88 100       19614 if ($date_strings->[1] =~ /(?=^($regex_to))/g) {
306 57         264 my $match = $1;
307 57         332 push @chunks, $extract_chunk->($date_strings->[1], $indexes->[1][0], pos $date_strings->[1], $match);
308             }
309 88 100 66     876 if (@chunks == 2 && $has_timespan_sep->($tokens, \@chunks, $timespan_sep)) {
310 47         1090 @$chunks = @chunks;
311 47         171 return true;
312             }
313              
314 41         250 pos $date_strings->[0] = 0;
315 41         336 pos $date_strings->[1] = 0;
316             }
317              
318 13         124 return false;
319             }
320              
321             1;
322             __END__
323              
324             =head1 NAME
325              
326             DateTime::Format::Natural::Duration::Checks - Duration checks
327              
328             =head1 SYNOPSIS
329              
330             Please see the DateTime::Format::Natural documentation.
331              
332             =head1 DESCRIPTION
333              
334             The C<DateTime::Format::Natural::Duration::Checks> class contains functions
335             for matching a duration type (called from C<::Duration> and C<::Extract>).
336              
337             =head1 SEE ALSO
338              
339             L<DateTime::Format::Natural>
340              
341             =head1 AUTHOR
342              
343             Steven Schubiger <schubiger@cpan.org>
344              
345             =head1 LICENSE
346              
347             This program is free software; you may redistribute it and/or
348             modify it under the same terms as Perl itself.
349              
350             See L<http://dev.perl.org/licenses/>
351              
352             =cut