• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DrHyde / perl-modules-Data-Compare / 5163191184

pending completion
5163191184

Pull #25

github

web-flow
Merge 1286c7cd3 into 09bcd46b1
Pull Request #25: Bump cross-platform-actions/action from 0.13.0 to 0.14.0

125 of 138 relevant lines covered (90.58%)

424.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.43
/lib/Data/Compare.pm
1
# Data::Compare - compare perl data structures
2
# Author: Fabien Tassin <fta@sofaraway.org>
3
# updated by David Cantrell <david@cantrell.org.uk>
4

5
package Data::Compare;
6

7
use strict;
15✔
8
use warnings;
15✔
9

10
use vars qw(@ISA @EXPORT $VERSION $DEBUG %been_there);
15✔
11
use Exporter;
15✔
12
use Carp;
15✔
13
use Clone qw(clone);
15✔
14
use Scalar::Util qw(tainted);
15✔
15
use File::Find::Rule;
15✔
16

17
@ISA     = qw(Exporter);
18
@EXPORT  = qw(Compare);
19
$VERSION = 1.29;
20
$DEBUG   = $ENV{PERL_DATA_COMPARE_DEBUG} || 0;
21

22
my %handler;
23

24
use Cwd;
15✔
25

26
sub import {
27
  my $cwd = getcwd();
27✔
28
  register_plugins() unless(tainted getcwd() || !chdir $cwd);
27✔
29
  __PACKAGE__->export_to_level(1, @EXPORT);
27✔
30
}
31

32
# finds and registers plugins
33
sub register_plugins {
34
  foreach my $file (
28✔
35
    File::Find::Rule->file()->name('*.pm')->in(
36
      map { "$_/Data/Compare/Plugins" }
60✔
37
      grep { -d "$_/Data/Compare/Plugins" }
424✔
38
      @INC
39
    )
40
  ) {
41
    # all of this just to avoid loading the same plugin twice and
42
    # generating a pile of warnings. Grargh!
43
    $file =~ s!.*(Data/Compare/Plugins/.*)\.pm$!$1!;
60✔
44
    $file =~ s!/!::!g;
60✔
45
    # ignore badly named example from earlier version, oops
46
    next if($file eq 'Data::Compare::Plugins::Scalar-Properties');
60✔
47
    my $requires = eval "require $file";
60✔
48
    next if($requires eq '1'); # already loaded this plugin?
60✔
49

50
    # not an arrayref? bail
51
    if(ref($requires) ne 'ARRAY') {
14✔
52
      warn("$file isn't a valid Data::Compare plugin (didn't return arrayref)\n");
×
53
      return;
×
54
    }
55
    # coerce into arrayref of arrayrefs if necessary
56
    if(ref((@{$requires})[0]) ne 'ARRAY') { $requires = [$requires] }
14✔
57

58
    # register all the handlers
59
    foreach my $require (@{$requires}) {
14✔
60
      my($handler, $type1, $type2, $cruft) = reverse @{$require};
28✔
61
      $type2 = $type1 unless(defined($type2));
28✔
62
      ($type1, $type2) = sort($type1, $type2);
28✔
63
      if(!defined($type1) || ref($type1) ne '' || !defined($type2) || ref($type2) ne '') {
28✔
64
        warn("$file isn't a valid Data::Compare plugin (invalid type)\n");
×
65
      } elsif(defined($cruft)) {
66
        warn("$file isn't a valid Data::Compare plugin (extra data)\n");
×
67
      } elsif(ref($handler) ne 'CODE') {
68
        warn("$file isn't a valid Data::Compare plugin (no coderef)\n");
×
69
      } else {
70
        $handler{$type1}{$type2} = $handler;
28✔
71
      }
72
    }
73
  }
74
}
75

76
sub new {
77
  my $this = shift;
3✔
78
  my $class = ref($this) || $this;
3✔
79
  my $self = {};
3✔
80
  bless $self, $class;
3✔
81
  $self->{'x'} = shift;
3✔
82
  $self->{'y'} = shift;
3✔
83
  return $self;
3✔
84
}
85

86
sub Cmp {
87
  my $self = shift;
7✔
88

89
  croak "Usage: DataCompareObj->Cmp(x, y)" unless $#_ == 1 || $#_ == -1;
7✔
90
  my $x = shift || $self->{'x'};
7✔
91
  my $y = shift || $self->{'y'};
7✔
92

93
  return Compare($x, $y);
7✔
94
}
95

96
sub Compare {
97
  croak "Usage: Data::Compare::Compare(x, y, [opts])\n" unless $#_ == 1 || $#_ == 2;
1,101✔
98

99
  my $x = shift;
1,101✔
100
  my $y = shift;
1,101✔
101
  my $opts = {};
1,101✔
102
  if(@_) { $opts = clone(shift); }
1,101✔
103

104
  _Compare($x, $y, $opts);
1,101✔
105
}
106

107
sub _Compare {
108
  my($x, $y, $opts) = @_;
2,532✔
109
  my($xparent, $xpos, $yparent, $ypos) = map {
110
    $opts->{$_} || ''
2,532✔
111
  } qw(xparent xpos yparent ypos);
112

113
  my $rval = '';
2,532✔
114

115
  if(!exists($opts->{recursion_detector})) {
2,532✔
116
    %been_there = ();
1,101✔
117
    $opts->{recursion_detector} = 0;
1,101✔
118
  }
119
  $opts->{recursion_detector}++;
2,532✔
120

121
  warn "Yaroo! deep recursion!\n" if($opts->{recursion_detector} == 99);
2,532✔
122

123
  if(
2,532✔
124
    (ref($x) && exists($been_there{"$x-$xpos-$xparent"}) && $been_there{"$x-$xpos-$xparent"} > 1) ||
125
    (ref($y) && exists($been_there{"$y-$ypos-$yparent"}) && $been_there{"$y-$ypos-$yparent"} > 1)
126
  ) {
127
    $opts->{recursion_detector}--;
6✔
128
    return 1; # we bail as soon as possible, so if we've *not* bailed and have got here, say we're OK and go to the next sub-structure
6✔
129
  } else {
130
    $been_there{"$x-$xpos-$xparent"}++ if(ref($x));
2,526✔
131
    $been_there{"$y-$ypos-$yparent"}++ if(ref($y));
2,526✔
132

133
    $opts->{ignore_hash_keys} = { map {
134
      ($_, 1)
9✔
135
    } @{$opts->{ignore_hash_keys}} } if(ref($opts->{ignore_hash_keys}) eq 'ARRAY');
2,526✔
136

137
    my $refx = ref $x;
2,526✔
138
    my $refy = ref $y;
2,526✔
139

140
    if(exists($handler{$refx}) && exists($handler{$refx}{$refy})) {
2,526✔
141
      $rval = &{$handler{$refx}{$refy}}($x, $y, $opts);
14✔
142
    } elsif(exists($handler{$refy}) && exists($handler{$refy}{$refx})) {
143
      $rval = &{$handler{$refy}{$refx}}($x, $y, $opts);
4✔
144
    }
145

146
    elsif(!$refx && !$refy) { # both are scalars
147
      if(defined $x && defined $y) { # both are defined
1,216✔
148
        $rval = $x eq $y;
1,210✔
149
      } else { $rval = !(defined $x || defined $y); }
6✔
150
    }
151
    elsif ($refx ne $refy) { # not the same type
152
      $rval = 0;
8✔
153
    }
154
    elsif (Scalar::Util::refaddr($x) == Scalar::Util::refaddr($y)) { # exactly the same reference
155
      $rval = 1;
20✔
156
    }
157
    elsif ($refx eq 'SCALAR' || $refx eq 'REF') {
158
      $rval = _Compare(${$x}, ${$y}, $opts);
22✔
159
    }
160
    elsif ($refx eq 'ARRAY') {
161
      if ($#{$x} == $#{$y}) { # same length
1,137✔
162
        my $i = -1;
1,132✔
163
        $rval = 1;
1,132✔
164
        for (@$x) {
1,132✔
165
          $i++;
1,149✔
166
          $rval = 0 unless _Compare($x->[$i], $y->[$i], { %{$opts}, xparent => $x, xpos => $i, yparent => $y, ypos => $i});
1,149✔
167
        }
168
      }
169
      else {
170
        $rval = 0;
5✔
171
      }
172
    }
173
    elsif ($refx eq 'HASH') {
174
      my @kx = grep { !$opts->{ignore_hash_keys}->{$_} } keys %$x;
75✔
175
      my @ky = grep { !$opts->{ignore_hash_keys}->{$_} } keys %$y; # heh, KY
75✔
176
      $rval = 1;
75✔
177
      $rval = 0 unless scalar @kx == scalar @ky;
75✔
178

179
      for (@kx) {
75✔
180
        if(!exists($y->{$_})) {
239✔
181
            $rval = 0;
6✔
182
            last;
6✔
183
        }
184
        $rval = 0 unless _Compare($x->{$_}, $y->{$_}, { %{$opts}, xparent => $x, xpos => $_, yparent => $y, ypos => $_});
233✔
185
      }
186
    }
187
    elsif($refx eq 'Regexp') {
188
      $rval = _Compare($x.'', $y.'', $opts);
2✔
189
    }
190
    elsif ($refx eq 'CODE') {
191
      $rval = 0;
×
192
    }
193
    elsif ($refx eq 'GLOB') {
194
      $rval = 0;
1✔
195
    }
196
    else { # a package name (object blessed)
197
      my $type = Scalar::Util::reftype($x);
27✔
198
      if ($type eq 'HASH') {
27✔
199
        my %x = %$x;
21✔
200
        my %y = %$y;
21✔
201
        $rval = _Compare(\%x, \%y, { %{$opts}, xparent => $xparent, xpos => $xpos, yparent => $yparent, ypos => $ypos});
21✔
202
        $been_there{\%x."-$xpos-$xparent"}--; # decrement count for temp structures
21✔
203
        $been_there{\%y."-$ypos-$yparent"}--;
21✔
204
      }
205
      elsif ($type eq 'ARRAY') {
206
        my @x = @$x;
2✔
207
        my @y = @$y;
2✔
208
        $rval = _Compare(\@x, \@y, { %{$opts}, xparent => $xparent, xpos => $xpos, yparent => $yparent, ypos => $ypos});
2✔
209
        $been_there{\@x."-$xpos-$xparent"}--;
2✔
210
        $been_there{\@y."-$ypos-$yparent"}--;
2✔
211
      }
212
      elsif ($type eq 'SCALAR' || $type eq 'REF') {
213
        my $x = ${$x};
2✔
214
        my $y = ${$y};
2✔
215
        $rval = _Compare($x, $y, $opts);
2✔
216
        # $been_there{\$x}--;
217
        # $been_there{\$y}--;
218
      }
219
      elsif ($type eq 'GLOB') {
220
        $rval = 0;
1✔
221
      }
222
      elsif ($type eq 'CODE') {
223
        $rval = 0;
1✔
224
      }
225
      else {
226
        croak "Can't handle $type type.";
×
227
        $rval = 0;
×
228
      }
229
    }
230
  }
231
  $opts->{recursion_detector}--;
2,526✔
232
  return $rval;
2,526✔
233
}
234

235
sub plugins {
236
  return { map { (($_ eq '') ? '[scalar]' : $_, [map { $_ eq '' ? '[scalar]' : $_ } keys %{$handler{$_}}]) } keys %handler };
3✔
237
}
238

239
sub plugins_printable {
240
  my $r = "The following comparisons are available through plugins\n\n";
×
241
  foreach my $key (sort keys %handler) {
×
242
    foreach(sort keys %{$handler{$key}}) {
×
243
      $r .= join(":\t", map { $_ eq '' ? '[scalar]' : $_ } ($key, $_))."\n";
×
244
    }
245
  }
246
  return $r;
×
247
}
248

249
1;
250

251
=head1 NAME
252

253
Data::Compare - compare perl data structures
254

255
=head1 SYNOPSIS
256

257
    use Data::Compare;
258

259
    my $h1 = { 'foo' => [ 'bar', 'baz' ],  'FOO' => [ 'one', 'two' ] };
260
    my $h2 = { 'foo' => [ 'bar', 'barf' ], 'FOO' => [ 'one', 'two' ] };
261
    my @a1 = ('one', 'two');
262
    my @a2 = ('bar', 'baz');
263
    my %v = ( 'FOO', \@a1, 'foo', \@a2 );
264

265
    # simple procedural interface
266
    print 'structures of $h1 and \%v are ',
267
      Compare($h1, \%v) ? "" : "not ", "identical.\n";
268

269
    print 'structures of $h1 and $h2 are ',
270
      Compare($h1, $h2, { ignore_hash_keys => [qw(foo)] }) ? '' : 'not ',
271
      "close enough to identical.\n";
272

273
    # OO usage
274
    my $c = Data::Compare->new($h1, \%v);
275
    print 'structures of $h1 and \%v are ',
276
      $c->Cmp ? "" : "not ", "identical.\n";
277
    # or
278
    my $c = Data::Compare->new();
279
    print 'structures of $h and \%v are ',
280
      $c->Cmp($h1, \%v) ? "" : "not ", "identical.\n";
281

282
=head1 DESCRIPTION
283

284
Compare two perl data structures recursively. Returns 0 if the
285
structures differ, else returns 1.
286

287
A few data types are treated as special cases:
288

289
=over 4
290

291
=item Scalar::Properties objects
292

293
This has been moved into a plugin, although functionality remains the
294
same as with the previous version.  Full documentation is in
295
L<Data::Compare::Plugins::Scalar::Properties>.
296

297
=item Compiled regular expressions, eg qr/foo/
298

299
These are stringified before comparison, so the following will match:
300

301
    $r = qr/abc/i;
302
    $s = qr/abc/i;
303
    Compare($r, $s);
304

305
and the following won't, despite them matching *exactly* the same text:
306

307
    $r = qr/abc/i;
308
    $s = qr/[aA][bB][cC]/;
309
    Compare($r, $s);
310

311
Sorry, that's the best we can do.
312

313
=item CODE and GLOB references
314

315
These are assumed not to match unless the references are identical - ie,
316
both are references to the same thing.
317

318
=back
319

320
You may also customise how we compare structures by supplying options in
321
a hashref as a third parameter to the C<Compare()> function.  This is not
322
yet available through the OO-ish interface.  These options will be in
323
force for the *whole* of your comparison, so will apply to structures
324
that are lurking deep down in your data as well as at the top level, so
325
beware!
326

327
=over 4
328

329
=item ignore_hash_keys
330

331
an arrayref of strings. When comparing two hashes, any keys mentioned in
332
this list will be ignored.
333

334
=back
335

336
=head1 CIRCULAR STRUCTURES
337

338
Comparing a circular structure to itself returns true:
339

340
    $x = \$y;
341
    $y = \$x;
342
    Compare([$x, $y], [$x, $y]);
343

344
And on a sort-of-related note, if you try to compare insanely deeply nested
345
structures, the module will spit a warning.  For this to affect you, you need to go
346
around a hundred levels deep though, and if you do that you have bigger
347
problems which I can't help you with ;-)
348

349
=head1 PLUGINS
350

351
The module takes plug-ins so you can provide specialised routines for
352
comparing your own objects and data-types.  For details see
353
L<Data::Compare::Plugins>.
354

355
Plugins are *not* available when running in "taint" mode.  You may
356
also make it not load plugins by providing an empty list as the
357
argument to import() - ie, by doing this:
358

359
    use Data::Compare ();
360

361
A couple of functions are provided to examine what goodies have been
362
made available through plugins:
363

364
=over 4
365

366
=item plugins
367

368
Returns a structure (a hash ref) describing all the comparisons made
369
available through plugins.
370
This function is *not* exported, so should be called as Data::Compare::plugins().
371
It takes no parameters.
372

373
=item plugins_printable
374

375
Returns formatted text
376

377
=back
378

379
=head1 EXPORTS
380

381
For historical reasons, the Compare() function is exported.  If you
382
don't want this, then pass an empty list to import() as explained
383
under PLUGINS.  If you want no export but do want plugins, then pass
384
the empty list, and then call the register_plugins class method:
385

386
    use Data::Compare ();
387
    Data::Compare->register_plugins;
388

389
or you could call it as a function if that floats your boat.
390

391
=head1 SOURCE CODE REPOSITORY
392

393
L<git://github.com/DrHyde/perl-modules-Data-Compare.git>
394

395
=head1 BUGS
396

397
Plugin support is not quite finished (see the the Github
398
L<issue #5|http://github.com/DrHyde/perl-modules-Data-Compare/issues/5>
399
for details) but is usable. The missing bits are bells and whistles rather than
400
core functionality.
401

402
Plugins are unavailable if you can't change to the current directory.  This
403
might happen if you started your process as a priveleged user and then dropped
404
priveleges.  If this affects you, please supply a portable patch with tests.
405

406
Bug reports should be made on Github or by email.
407

408
=head1 AUTHOR
409

410
Fabien Tassin E<lt>fta@sofaraway.orgE<gt>
411

412
Portions by David Cantrell E<lt>david@cantrell.org.ukE<gt>
413

414
=head1 COPYRIGHT and LICENCE
415

416
Copyright (c) 1999-2001 Fabien Tassin. All rights reserved.
417
This program is free software; you can redistribute it and/or
418
modify it under the same terms as Perl itself.
419

420
Some parts copyright 2003 - 2023 David Cantrell.
421

422
Seeing that Fabien seems to have disappeared, David Cantrell has become
423
a co-maintainer so he can apply needed patches.  The licence, of course,
424
remains the same.  As the "perl licence" is "Artistic or GPL, your choice",
425
you can find them as the files ARTISTIC.txt and GPL2.txt in the
426
distribution.
427

428
=head1 SEE ALSO
429

430
L<Test::Deep::NoTest>
431

432
perl(1), perlref(1)
433

434
=cut
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc