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 0 3 0.0
total 165 173 95.3


line stmt bran cond sub pod time code
1             package DateTime::Format::Natural::Duration::Checks;
2              
3 26     26   186 use strict;
  26         66  
  26         757  
4 26     26   178 use warnings;
  26         69  
  26         719  
5 26     26   142 use boolean qw(true false);
  26         73  
  26         160  
6              
7             our $VERSION = '0.05';
8              
9             sub for
10             {
11 1258     1258 0 3678 my ($duration, $date_strings, $present) = @_;
12              
13 1258 100 100     5575 if (@$date_strings == 1
14             && $date_strings->[0] =~ $duration->{for}{regex}
15             ) {
16 55         152 $$present = $duration->{for}{present};
17 55         173 return true;
18             }
19             else {
20 1203         3526 return false;
21             }
22             }
23              
24             sub first_to_last
25             {
26 1203     1203 0 3024 my ($duration, $date_strings, $extract) = @_;
27              
28 1203         2359 my %regexes = %{$duration->{first_to_last}{regexes}};
  1203         6255  
29              
30 1203 100 100     11185 if (@$date_strings == 2
      66        
31             && $date_strings->[0] =~ /^$regexes{first}$/
32             && $date_strings->[1] =~ /^$regexes{last}$/
33             ) {
34 27         113 $$extract = $regexes{extract};
35 27         109 return true;
36             }
37             else {
38 1176         3121 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             sub _first_to_last_extract
87             {
88 111     111   300 my $self = shift;
89 111         361 my ($date_strings, $indexes, $tokens, $chunks) = @_;
90              
91 111 50       377 return false unless @$date_strings == 2;
92              
93 111         329 my $duration = $self->{data}->{duration};
94              
95 111         230 my %regexes = %{$duration->{first_to_last}{regexes}};
  111         724  
96              
97 111         667 $regexes{first} = $anchor_regex{left}->($regexes{first});
98 111         567 $regexes{last} = $anchor_regex{right}->($regexes{last});
99              
100 111         1053 my $timespan_sep = $self->{data}->__timespan('literal');
101              
102 111         621 my @chunks;
103 111 100       1470 if ($date_strings->[0] =~ /(?=($regexes{first})$)/g) {
104 17         68 my $match = $1;
105 17         122 push @chunks, $extract_chunk->($date_strings->[0], $indexes->[0][0], pos $date_strings->[0], $match);
106             }
107 111 100       1126 if ($date_strings->[1] =~ /(?=^($regexes{last}))/g) {
108 17         78 my $match = $1;
109 17         99 push @chunks, $extract_chunk->($date_strings->[1], $indexes->[1][0], pos $date_strings->[1], $match);
110             }
111 111 100 66     759 if (@chunks == 2 && $has_timespan_sep->($tokens, \@chunks, $timespan_sep)) {
112 16         289 @$chunks = @chunks;
113 16         67 return true;
114             }
115             else {
116 95         418 return false;
117             }
118             }
119              
120             my $duration_matches = sub
121             {
122             my ($duration, $date_strings, $entry, $target) = @_;
123              
124             my $data = $duration->{from_count_to_count};
125              
126             my (@matches, %seen);
127             foreach my $ident (@{$data->{order}}) {
128             my $regex = $anchor_regex{both}->($data->{regexes}{$ident});
129             while ($date_strings->[0] =~ /(?=$regex)/g) {
130             my $pos = pos $date_strings->[0];
131             next if $seen{$pos};
132             push @matches, [ $ident, $pos ];
133             $seen{$pos} = true;
134             }
135             }
136             my @idents = map $_->[0], sort { $a->[1] <=> $b->[1] } @matches;
137              
138             my %categories;
139             foreach my $ident (@{$data->{order}}) {
140             my $category = $data->{categories}{$ident};
141             push @{$categories{$category}}, $ident;
142             }
143              
144             my $get_target = sub
145             {
146             my ($category, $target) = @_;
147             foreach my $ident (@{$categories{$category}}) {
148             my $regex = $anchor_regex{both}->($data->{regexes}{$ident});
149             if ($date_strings->[1] =~ $regex) {
150             $$target = $ident;
151             return true;
152             }
153             }
154             return false;
155             };
156              
157             if (@idents >= 2
158             && $data->{categories}{$idents[-1]} eq 'day'
159             && $data->{categories}{$idents[-2]} eq 'time'
160             && $get_target->($data->{categories}{$idents[-2]}, $target)
161             ) {
162             $$entry = $idents[-2];
163             return true;
164             }
165             elsif (@idents
166             && $get_target->($data->{categories}{$idents[-1]}, $target)
167             ) {
168             $$entry = $idents[-1];
169             return true;
170             }
171             else {
172             return false;
173             }
174             };
175              
176             sub from_count_to_count
177             {
178 1176     1176 0 3355 my ($duration, $date_strings, $extract, $adjust, $indexes) = @_;
179              
180 1176 100       3605 return false unless @$date_strings == 2;
181              
182 1012         2037 my ($entry, $target);
183 1012 100       2836 return false unless $duration_matches->($duration, $date_strings, \$entry, \$target);
184              
185 908         16434 my $data = $duration->{from_count_to_count};
186              
187             my $get_data = sub
188             {
189 1307     1307   2791 my ($types, $idents, $type) = @_;
190              
191 1307         3138 my $regex = $data->{regexes}{$idents->[0]};
192 1307         15396 my %regexes = (
193             left => qr/^.+? \s+ $regex$/x,
194             right => qr/^$regex \s+ .+$/x,
195             target => qr/^$data->{regexes}{$idents->[1]}$/,
196             );
197 1307         9701 my %extract = (
198             left => qr/^(.+?) \s+ $regex$/x,
199             right => qr/^$regex \s+ (.+)$/x,
200             );
201             my %adjust = (
202             left => sub
203             {
204 617         2164 my ($date_strings, $index, $complete) = @_;
205 617         4659 $date_strings->[$index] = "$complete $date_strings->[$index]";
206             },
207             right => sub
208             {
209 282         736 my ($date_strings, $index, $complete) = @_;
210 282         2117 $date_strings->[$index] .= " $complete";
211             },
212 1307         6804 );
213              
214 1307         9538 return (@regexes{@$types}, $extract{$type}, $adjust{$type});
215 908         4842 };
216              
217 908         5515 my @sets = (
218             [ [ qw( left target) ], [ $entry, $target ], 'left', [0,1] ],
219             [ [ qw(right target) ], [ $entry, $target ], 'right', [0,1] ],
220             );
221              
222 908         2068 my @new;
223 908         2727 foreach my $set (@sets) {
224 1816         3099 push @new, [ [ reverse @{$set->[0]} ], [ reverse @{$set->[1]} ], $set->[2], [ reverse @{$set->[3]} ] ];
  1816         4045  
  1816         3911  
  1816         5004  
225             }
226 908         2055 push @sets, @new;
227              
228 908         1887 foreach my $set (@sets) {
229 1307         3823 my ($regex_types, $idents, $type, $string_indexes) = @$set;
230              
231 1307         3154 my ($regex_from, $regex_to, $extract_regex, $adjust_code) = $get_data->($regex_types, $idents, $type);
232              
233 1307 100 100     17787 if ($date_strings->[0] =~ $regex_from
234             && $date_strings->[1] =~ $regex_to
235             ) {
236 899         2174 $$extract = $extract_regex;
237 899         1567 $$adjust = $adjust_code;
238 899         2479 @$indexes = @$string_indexes;
239 899         2728 return true;
240             }
241             }
242              
243 9         48 return false;
244             }
245              
246             sub _from_count_to_count_extract
247             {
248 95     95   359 my $self = shift;
249 95         330 my ($date_strings, $indexes, $tokens, $chunks) = @_;
250              
251 95 50       308 return false unless @$date_strings == 2;
252              
253 95         339 my $duration = $self->{data}->{duration};
254              
255 95         246 my ($entry, $target);
256 95 100       409 return false unless $duration_matches->($duration, $date_strings, \$entry, \$target);
257              
258 60         1331 my $data = $duration->{from_count_to_count};
259              
260             my $get_data = sub
261             {
262 88     88   411 my ($types, $idents) = @_;
263              
264 88         317 my $category = $data->{categories}{$idents->[0]};
265 88         234 my $regex = $data->{regexes}{$idents->[0]};
266              
267             my %regexes = (
268             left => qr/$data->{extract}{left}{$category}\s+$regex/,
269             right => qr/$regex\s+$data->{extract}{right}{$category}/,
270 88         6925 target => $data->{regexes}{$idents->[1]},
271             );
272              
273 88         4940 $regexes{entry} = qr/(?:$regexes{left}|$regexes{right})/;
274              
275 88         686 return @regexes{@$types};
276 60         515 };
277              
278 60         529 my $timespan_sep = $self->{data}->__timespan('literal');
279              
280 60         760 my @sets = (
281             [ [ qw(entry target) ], [ $entry, $target ] ],
282             );
283              
284 60         191 my @new;
285 60         214 foreach my $set (@sets) {
286 60         143 push @new, [ [ reverse @{$set->[0]} ], [ reverse @{$set->[1]} ] ];
  60         190  
  60         212  
287             }
288 60         142 push @sets, @new;
289              
290 60         179 foreach my $set (@sets) {
291 88         290 my ($regex_types, $idents) = @$set;
292              
293 88         259 my ($regex_from, $regex_to) = $get_data->($regex_types, $idents);
294              
295 88         284 $regex_from = $anchor_regex{left}->($regex_from);
296 88         507 $regex_to = $anchor_regex{right}->($regex_to);
297              
298 88         391 my @chunks;
299 88 100       11011 if ($date_strings->[0] =~ /(?=($regex_from)$)/g) {
300 57         310 my $match = $1;
301 57         336 push @chunks, $extract_chunk->($date_strings->[0], $indexes->[0][0], pos $date_strings->[0], $match);
302             }
303 88 100       8987 if ($date_strings->[1] =~ /(?=^($regex_to))/g) {
304 57         259 my $match = $1;
305 57         279 push @chunks, $extract_chunk->($date_strings->[1], $indexes->[1][0], pos $date_strings->[1], $match);
306             }
307 88 100 66     728 if (@chunks == 2 && $has_timespan_sep->($tokens, \@chunks, $timespan_sep)) {
308 47         930 @$chunks = @chunks;
309 47         133 return true;
310             }
311              
312 41         160 pos $date_strings->[0] = 0;
313 41         239 pos $date_strings->[1] = 0;
314             }
315              
316 13         77 return false;
317             }
318              
319             1;