File Coverage

blib/lib/Data/PatternCompare.pm
Criterion Covered Total %
statement 209 213 98.1
branch 107 116 92.2
condition 39 49 79.5
subroutine 26 28 92.8
pod 5 5 100.0
total 386 411 93.9


line stmt bran cond sub pod time code
1             package Data::PatternCompare;
2              
3 6     6   152692 use strict;
  6         12  
  6         253  
4 6     6   29 use warnings;
  6         52  
  6         179  
5              
6 6     6   7028 use POSIX;
  6         54525  
  6         42  
7 6     6   23390 use Scalar::Util qw(looks_like_number refaddr blessed);
  6         15  
  6         1133  
8 6     6   9635 use Scalar::Util::Numeric qw(isfloat);
  6         6049  
  6         1666  
9              
10             our $VERSION = '0.04';
11              
12             sub EMPTY_KEY() { "empty \x{c0}\x{de}" }
13              
14             our @EXPORT_OK = qw(any empty);
15             our $any = Data::PatternCompare::Any->new;
16             our @empty = (EMPTY_KEY, Data::PatternCompare::Empty->new);
17              
18             sub _any() {
19 0     0   0 $any
20             }
21              
22             sub _empty() {
23             @empty
24 0     0   0 }
25              
26             sub import_to {
27 6     6 1 13 my ($caller, @export) = @_;
28              
29 6     6   42 no strict 'refs';
  6         12  
  6         212  
30 6     6   31 no warnings 'redefine';
  6         11  
  6         17884  
31              
32 6         11707 for my $sub (@export) {
33 2         5 my $dst = $caller .'::'. $sub;
34 2         3 my $src = __PACKAGE__ .'::_'. $sub;
35              
36 2         1913 *$dst = *$src;
37             }
38             }
39              
40             sub import {
41 6     6   59 my $class = shift;
42 6         18 my $caller = caller;
43 6         11 my @export;
44 6         16 my %is_export_ok = map { $_ => 1 } @EXPORT_OK;
  12         44  
45              
46 6         23 for my $sub ( @_ ) {
47 2 50       13 push @export, $sub if $is_export_ok{$sub};
48             }
49              
50 6         24 import_to($caller, @export);
51             }
52              
53             sub new {
54 5     5 1 1161 my $class = shift;
55 5         15 my %params = @_;
56              
57 5         31 @params{qw(_dup_addr _dup_addra _dup_addrb)} = ({}, {}, {});
58 5   50     44 $params{'epsilon'} ||= POSIX::DBL_EPSILON;
59              
60 5         23 return bless(\%params, $class);
61             }
62              
63             sub _is_any {
64 11     11   17 my $val = shift;
65 11         24 my $class = blessed($val);
66              
67 11 100 66     37 if ($class && $class eq 'Data::PatternCompare::Any') {
68 2         14 return $class;
69             }
70              
71 9         30 return 0;
72             }
73              
74             sub _is_empty {
75 52     52   61 my $val = shift;
76              
77 52 100       118 if (ref $val eq 'ARRAY') {
78 28 100       92 return 0 unless defined $val->[1];
79              
80 12   100     53 my $blessed = blessed($val->[1]) || '';
81             return (
82 12   66     106 defined $val->[0] && $val->[0] eq EMPTY_KEY
83             && $blessed eq 'Data::PatternCompare::Empty'
84             );
85             } else {
86 24 100       88 return 0 unless defined $val->{+EMPTY_KEY};
87              
88 6   50     25 my $blessed = blessed($val->{+EMPTY_KEY}) || '';
89 6         24 return $blessed eq 'Data::PatternCompare::Empty';
90             }
91             }
92              
93             sub _match_ARRAY {
94 10     10   16 my ($self, $got, $expected) = @_;
95              
96 10 100       20 if (_is_empty($expected)) {
97 2         8 return scalar(@$got) == 0;
98             }
99              
100 8         40 for (my $i = 0; $i < scalar(@$expected); ++$i) {
101 7 100 66     17 if (_is_any($expected->[$i]) && !exists($got->[$i])) {
102 1         3 return 0;
103             }
104 6 100       24 return 0 unless $self->_pattern_match($got->[$i], $expected->[$i]);
105             }
106              
107 4         11 return 1;
108             }
109              
110             sub _match_HASH {
111 8     8   13 my ($self, $got, $expected) = @_;
112              
113 8 100       15 if (_is_empty($expected)) {
114 2         9 return scalar(keys %$got) == 0;
115             }
116              
117 6         20 for my $key ( keys %$expected ) {
118 4 100 66     11 if (_is_any($expected->{$key}) && !exists($got->{$key})) {
119 1         4 return 0;
120             }
121 3 100       10 return 0 unless $self->_pattern_match($got->{$key}, $expected->{$key});
122             }
123              
124 4         13 return 1;
125             }
126              
127             sub _pattern_match {
128 43     43   67 my ($self, $got, $expected) = @_;
129              
130 43         70 my $ref = ref($expected);
131 43 100       91 unless ($ref) {
132             # simple type
133 18 100 100     87 unless (defined $expected && defined $got) {
134 3 100 100     42 unless (defined $expected || defined $got) {
135 1         4 return 1;
136             }
137 2         5 return 0;
138             }
139              
140 15 100       63 if (looks_like_number($expected)) {
141 13 100       40 return 0 unless looks_like_number($got);
142              
143 12 100 100     89 if (isfloat($expected) || isfloat($got)) {
144 2         10 return abs($expected - $got) < $self->{'epsilon'};
145             }
146 10         52 return $expected == $got;
147             }
148              
149 2         7 return $expected eq $got;
150             }
151              
152 25         61 my $addr = refaddr($expected);
153 25         45 my $is_dup = $self->{'_dup_addr'};
154 25 100       65 if (exists $is_dup->{$addr}) {
155 1         16 die "Cycle in pattern: $expected";
156             }
157 24         63 $is_dup->{$addr} = 1;
158              
159 24         57 my $class = blessed($expected);
160 24 100       51 if ($class) {
161 4 50       18 return 1 if $class eq 'Data::PatternCompare::Any';
162              
163             return (
164 0   0     0 $class eq blessed($got) &&
165             $addr == refaddr($got)
166             );
167             }
168              
169 20         102 my $code = $self->can("_match_$ref");
170 20 100       64 die "Don't know how to match $ref type" unless $code;
171              
172 19 100       52 return 0 unless ref($got) eq $ref;
173              
174 18         60 return $self->$code($got, $expected);
175             }
176              
177             sub pattern_match {
178 34     34 1 15402 my $self = shift;
179              
180 34         45 my $res;
181 34         41 eval {
182 34         80 $res = $self->_pattern_match(@_);
183             };
184 34         90 $self->{'_dup_addr'} = {};
185 34 100       102 die $@ if $@;
186              
187 32         104 return $res;
188             }
189              
190             sub _compare_ARRAY {
191 9     9   12 my ($self, $pa, $pb) = @_;
192              
193 9         13 my @tmp = map { _is_empty($_) } ($pa, $pb);
  18         34  
194 9 100       25 if ($tmp[0] + $tmp[1]) {
195 3         11 return $tmp[1] - $tmp[0];
196             }
197              
198 6         9 my $sizea = scalar(@$pa);
199 6         9 my $sizeb = scalar(@$pb);
200              
201 6 100       19 unless ($sizea eq $sizeb) {
202 1 50       7 return $sizea > $sizeb ? -1 : 1;
203             }
204              
205 5         17 for (my $i = 0; $i < $sizea; ++$i) {
206 6         29 my $res = $self->_compare_pattern($pa->[$i], $pb->[$i]);
207              
208 4 100       19 return $res if $res;
209             }
210              
211 2         7 return 0;
212             }
213              
214             sub _compare_HASH {
215 8     8   10 my ($self, $pa, $pb) = @_;
216              
217 8         12 my @tmp = map { _is_empty($_) } ($pa, $pb);
  16         27  
218 8 100       21 if ($tmp[0] + $tmp[1]) {
219 3         12 return $tmp[1] - $tmp[0];
220             }
221              
222 5         11 my $sizea = scalar keys(%$pa);
223 5         10 my $sizeb = scalar keys(%$pb);
224              
225 5 100       13 unless ($sizea eq $sizeb) {
226 1 50       7 return $sizea > $sizeb ? -1 : 1;
227             }
228              
229 4         13 for my $key ( keys %$pa ) {
230 4 100       11 next unless exists $pb->{$key};
231              
232 3         10 my $res = $self->_compare_pattern($pa->{$key}, $pb->{$key});
233              
234 3 100       13 return $res if $res;
235             }
236              
237 3         14 return 0;
238             }
239              
240             sub _compare_pattern {
241 34     34   49 my ($self, $pa, $pb) = @_;
242              
243 34         53 my $refa = ref($pa);
244 34         49 my $refb = ref($pb);
245 34         49 my @tmp = grep { $_ } ($refa, $refb);
  68         157  
246 34         42 my $cnt = scalar(@tmp);
247              
248             # simple type - equal
249 34 100       80 return 0 unless $cnt;
250              
251             # 1 ref
252 27 100       57 if ($cnt == 1) {
253             # any ref (including any) is wider than simple type
254 5 100       42 return $refb ? -1 : 1;
255             }
256              
257 22         50 my $addra = refaddr($pa);
258 22         33 my $addrb = refaddr($pb);
259 22         33 my $classa = blessed($pa);
260 22         33 my $classb = blessed($pb);
261              
262 22         36 my $is_dupa = $self->{'_dup_addra'};
263 22         28 my $is_dupb = $self->{'_dup_addrb'};
264 22 100 100     111 if (exists $is_dupa->{$addra} || exists $is_dupb->{$addrb}) {
265 2         17 die "Cycle in pattern";
266             }
267 20         46 $is_dupa->{$addra} = 1;
268 20         34 $is_dupb->{$addrb} = 1;
269              
270 20 100       30 @tmp = grep { $_ && $_ eq 'Data::PatternCompare::Any' } ($classa, $classb);
  40         110  
271 20         26 $cnt = scalar @tmp;
272              
273             # 1 "any"
274 20 50       39 if ($cnt == 1) {
275 0 0       0 return $classb eq 'Data::PatternCompare::Any' ? -1 : 1;
276             }
277              
278             # both are "any"
279 20 100       36 return 0 if $cnt == 2;
280              
281             # different types, no reason to go deeper
282 19 100       43 return 0 unless $refa eq $refb;
283              
284 18         116 my $code = __PACKAGE__->can("_compare_$refa");
285 18 100       58 die "Don't know how to compare $refa type" unless $code;
286              
287 17         34 return $self->$code($pa, $pb);
288             }
289              
290             sub compare_pattern {
291 25     25 1 13890 my $self = shift;
292              
293 25         28 my $res;
294 25         32 eval {
295 25         60 $res = $self->_compare_pattern(@_);
296             };
297 25         64 $self->{'_dup_addra'} = {};
298 25         54 $self->{'_dup_addrb'} = {};
299              
300 25 100       71 die $@ if $@;
301              
302 22         74 return $res;
303             }
304              
305             sub _eq_ARRAY {
306 9     9   14 my ($self, $got, $expected) = @_;
307              
308 9 100       46 return 0 unless scalar(@$got) == scalar(@$expected);
309              
310 6         18 for (my $i = 0; $i < scalar(@$expected); ++$i) {
311 7 100       25 return 0 unless $self->_eq_pattern($got->[$i], $expected->[$i]);
312             }
313              
314 3         8 return 1;
315             }
316              
317             sub _eq_HASH {
318 7     7   14 my ($self, $got, $expected) = @_;
319              
320 7 100       22 return 0 unless scalar(keys %$got) == scalar(keys %$expected);
321              
322 5         12 for my $key ( keys %$expected ) {
323 4 100       10 return 0 unless $self->_eq_pattern($got->{$key}, $expected->{$key});
324             }
325              
326 3         8 return 1;
327             }
328              
329             sub _eq_pattern {
330 43     43   58 my ($self, $got, $expected) = @_;
331              
332 43         61 my $ref = ref($expected);
333 43 100       86 unless ($ref) {
334             # simple type
335 16 100 100     70 unless (defined $expected && defined $got) {
336 3 100 100     39 unless (defined $expected || defined $got) {
337 1         3 return 1;
338             }
339 2         7 return 0;
340             }
341              
342 13 100       41 if (looks_like_number($expected)) {
343 10 50       23 return 0 unless looks_like_number($got);
344              
345 10 100 100     69 if (isfloat($expected) || isfloat($got)) {
346 2         9 return abs($expected - $got) < $self->{'epsilon'};
347             }
348 8         34 return $expected == $got;
349             }
350              
351 3         13 return $expected eq $got;
352             }
353              
354 27         50 my $addr = refaddr($expected);
355 27         39 my $is_dup = $self->{'_dup_addr'};
356 27 100       68 if (exists $is_dup->{$addr}) {
357 1         11 die "Cycle in pattern: $expected";
358             }
359 26         62 $is_dup->{$addr} = 1;
360              
361 26         52 my $class = blessed($expected);
362 26 100       48 if ($class) {
363 9   100     34 my $got_blessed = blessed($got) || '';
364 9   100     27 my $got_addr = refaddr($got) || 0;
365             return (
366 9   66     43 $class eq $got_blessed &&
367             $addr == $got_addr
368             );
369             }
370              
371 17         60 my $code = $self->can("_eq_$ref");
372 17 50       48 die "Don't know how to eq $ref type" unless $code;
373              
374 17 100       41 return 0 unless ref($got) eq $ref;
375              
376 16         36 return $self->$code($got, $expected);
377             }
378              
379             sub eq_pattern {
380 32     32 1 11607 my $self = shift;
381              
382 32         43 my $res;
383 32         38 eval {
384 32         68 $res = $self->_eq_pattern(@_);
385             };
386 32         80 $self->{'_dup_addr'} = {};
387              
388 32 100       81 die $@ if $@;
389              
390 31         99 return $res;
391             }
392              
393             package Data::PatternCompare::Any;
394              
395 6     6   19 sub new { bless({}); }
396              
397             package Data::PatternCompare::Empty;
398              
399 6     6   22 sub new { bless({}); }
400              
401             42;
402              
403             __END__