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   188 use strict;
  26         65  
  26         843  
4 26     26   166 use warnings;
  26         69  
  26         737  
5 26     26   140 use boolean qw(true false);
  26         59  
  26         178  
6              
7             our $VERSION = '0.05';
8              
9             sub for
10             {
11 1258     1258 0 3184 my ($duration, $date_strings, $present) = @_;
12              
13 1258 100 100     6178 if (@$date_strings == 1
14             && $date_strings->[0] =~ $duration->{for}{regex}
15             ) {
16 55         195 $$present = $duration->{for}{present};
17 55         178 return true;
18             }
19             else {
20 1203         3410 return false;
21             }
22             }
23              
24             sub first_to_last
25             {
26 1203     1203 0 3272 my ($duration, $date_strings, $extract) = @_;
27              
28 1203         2250 my %regexes = %{$duration->{first_to_last}{regexes}};
  1203         6675  
29              
30 1203 100 100     11871 if (@$date_strings == 2
      66        
31             && $date_strings->[0] =~ /^$regexes{first}$/
32             && $date_strings->[1] =~ /^$regexes{last}$/
33             ) {
34 27         98 $$extract = $regexes{extract};
35 27         117 return true;
36             }
37             else {
38 1176         3547 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   326 my $self = shift;
89 111         422 my ($date_strings, $indexes, $tokens, $chunks) = @_;
90              
91 111 50       382 return false unless @$date_strings == 2;
92              
93 111         374 my $duration = $self->{data}->{duration};
94              
95 111         237 my %regexes = %{$duration->{first_to_last}{regexes}};
  111         646  
96              
97 111         582 $regexes{first} = $anchor_regex{left}->($regexes{first});
98 111         602 $regexes{last} = $anchor_regex{right}->($regexes{last});
99              
100 111         1012 my $timespan_sep = $self->{data}->__timespan('literal');
101              
102 111         749 my @chunks;
103 111 100       1446 if ($date_strings->[0] =~ /(?=($regexes{first})$)/g) {
104 17         68 my $match = $1;
105 17         110 push @chunks, $extract_chunk->($date_strings->[0], $indexes->[0][0], pos $date_strings->[0], $match);
106             }
107 111 100       1115 if ($date_strings->[1] =~ /(?=^($regexes{last}))/g) {
108 17         60 my $match = $1;
109 17         89 push @chunks, $extract_chunk->($date_strings->[1], $indexes->[1][0], pos $date_strings->[1], $match);
110             }
111 111 100 66     614 if (@chunks == 2 && $has_timespan_sep->($tokens, \@chunks, $timespan_sep)) {
112 16         318 @$chunks = @chunks;
113 16         56 return true;
114             }
115             else {
116 95         315 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 4029 my ($duration, $date_strings, $extract, $adjust, $indexes) = @_;
179              
180 1176 100       3613 return false unless @$date_strings == 2;
181              
182 1012         2244 my ($entry, $target);
183 1012 100       3155 return false unless $duration_matches->($duration, $date_strings, \$entry, \$target);
184              
185 908         17486 my $data = $duration->{from_count_to_count};
186              
187             my $get_data = sub
188             {
189 1307     1307   3143 my ($types, $idents, $type) = @_;
190              
191 1307         3694 my $regex = $data->{regexes}{$idents->[0]};
192 1307         17818 my %regexes = (
193             left => qr/^.+? \s+ $regex$/x,
194             right => qr/^$regex \s+ .+$/x,
195             target => qr/^$data->{regexes}{$idents->[1]}$/,
196             );
197 1307         10574 my %extract = (
198             left => qr/^(.+?) \s+ $regex$/x,
199             right => qr/^$regex \s+ (.+)$/x,
200             );
201             my %adjust = (
202             left => sub
203             {
204 617         1901 my ($date_strings, $index, $complete) = @_;
205 617         5067 $date_strings->[$index] = "$complete $date_strings->[$index]";
206             },
207             right => sub
208             {
209 282         775 my ($date_strings, $index, $complete) = @_;
210 282         2098 $date_strings->[$index] .= " $complete";
211             },
212 1307         7925 );
213              
214 1307         10680 return (@regexes{@$types}, $extract{$type}, $adjust{$type});
215 908         5583 };
216              
217 908         6536 my @sets = (
218             [ [ qw( left target) ], [ $entry, $target ], 'left', [0,1] ],
219             [ [ qw(right target) ], [ $entry, $target ], 'right', [0,1] ],
220             );
221              
222 908         2026 my @new;
223 908         2504 foreach my $set (@sets) {
224 1816         3423 push @new, [ [ reverse @{$set->[0]} ], [ reverse @{$set->[1]} ], $set->[2], [ reverse @{$set->[3]} ] ];
  1816         4365  
  1816         3989  
  1816         5938  
225             }
226 908         2147 push @sets, @new;
227              
228 908         1955 foreach my $set (@sets) {
229 1307         3516 my ($regex_types, $idents, $type, $string_indexes) = @$set;
230              
231 1307         3257 my ($regex_from, $regex_to, $extract_regex, $adjust_code) = $get_data->($regex_types, $idents, $type);
232              
233 1307 100 100     18929 if ($date_strings->[0] =~ $regex_from
234             && $date_strings->[1] =~ $regex_to
235             ) {
236 899         2236 $$extract = $extract_regex;
237 899         1996 $$adjust = $adjust_code;
238 899         2671 @$indexes = @$string_indexes;
239 899         2758 return true;
240             }
241             }
242              
243 9         53 return false;
244             }
245              
246             sub _from_count_to_count_extract
247             {
248 95     95   303 my $self = shift;
249 95         366 my ($date_strings, $indexes, $tokens, $chunks) = @_;
250              
251 95 50       466 return false unless @$date_strings == 2;
252              
253 95         309 my $duration = $self->{data}->{duration};
254              
255 95         212 my ($entry, $target);
256 95 100       361 return false unless $duration_matches->($duration, $date_strings, \$entry, \$target);
257              
258 60         1270 my $data = $duration->{from_count_to_count};
259              
260             my $get_data = sub
261             {
262 88     88   237 my ($types, $idents) = @_;
263              
264 88         284 my $category = $data->{categories}{$idents->[0]};
265 88         233 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         6418 target => $data->{regexes}{$idents->[1]},
271             );
272              
273 88         4881 $regexes{entry} = qr/(?:$regexes{left}|$regexes{right})/;
274              
275 88         615 return @regexes{@$types};
276 60         392 };
277              
278 60         444 my $timespan_sep = $self->{data}->__timespan('literal');
279              
280 60         588 my @sets = (
281             [ [ qw(entry target) ], [ $entry, $target ] ],
282             );
283              
284 60         193 my @new;
285 60         208 foreach my $set (@sets) {
286 60         184 push @new, [ [ reverse @{$set->[0]} ], [ reverse @{$set->[1]} ] ];
  60         257  
  60         218  
287             }
288 60         176 push @sets, @new;
289              
290 60         196 foreach my $set (@sets) {
291 88         280 my ($regex_types, $idents) = @$set;
292              
293 88         249 my ($regex_from, $regex_to) = $get_data->($regex_types, $idents);
294              
295 88         293 $regex_from = $anchor_regex{left}->($regex_from);
296 88         529 $regex_to = $anchor_regex{right}->($regex_to);
297              
298 88         358 my @chunks;
299 88 100       10669 if ($date_strings->[0] =~ /(?=($regex_from)$)/g) {
300 57         307 my $match = $1;
301 57         396 push @chunks, $extract_chunk->($date_strings->[0], $indexes->[0][0], pos $date_strings->[0], $match);
302             }
303 88 100       8050 if ($date_strings->[1] =~ /(?=^($regex_to))/g) {
304 57         244 my $match = $1;
305 57         251 push @chunks, $extract_chunk->($date_strings->[1], $indexes->[1][0], pos $date_strings->[1], $match);
306             }
307 88 100 66     698 if (@chunks == 2 && $has_timespan_sep->($tokens, \@chunks, $timespan_sep)) {
308 47         985 @$chunks = @chunks;
309 47         125 return true;
310             }
311              
312 41         175 pos $date_strings->[0] = 0;
313 41         236 pos $date_strings->[1] = 0;
314             }
315              
316 13         87 return false;
317             }
318              
319             1;