Perl - Compare two nested hash

2019-07-09 11:03发布

问题:

This is my scenario, where there are 2 hashes that have been decoded from 2 JSON files.

I have 2 complex hash,

$hash1 = {k1=> { k11 => v1, k12 => v2}, k2 => { k21 => [v1, v2, v3] }}
$hash2 = {k1=> { k11 => v1, k12 => v2}, k2 => { k21 => [v3, v2, v1] }}

I want to compare these 2 hash for equality, and used Compare of Data::Compare and is_deeply of Test::More. Both do not ignore the order of the array.
I want to compare ignoring the order of array values of key 'k21'.
My App populates the array from 'keys %hash' which gives random order.
Tried 'ignore_hash_keys' of Data::Compare, but my hash sometimes can be complex and don't want to ignore.

Key 'k21' can also sometimes have array of hashes.

$hash3 = {k1=> { k11 => v1}, k2 => { k21 => [{v3 => v31}, {v2 => v22}] }}

How do I compare such complex hash by ignoring the array order.

回答1:

You can use Test::Deep, which provides cmp_deeply. It's a lot more versatile than Test::More's is_deeply.

use Test::Deep;

my $hash1 = {
    k1 => { k11 => 'v1', k12 => 'v2' }, k2 => { k21 => [ 'v1', 'v2', 'v3' ] } };
my $hash2 = {
    k1 => { k11 => 'v1', k12 => 'v2' }, k2 => { k21 => bag( 'v3', 'v2', 'v1' ) } };

cmp_deeply( $hash1, $hash2, );

The trick is the bag() function, which ignores the order of elements.

This does a bag comparison, that is, it compares two arrays but ignores the order of the elements [...]


Update: From your comment:

How do I bag all array references inside hash dynamically

Some digging in the code of Test::Deep showed that it's possible to overwrite it. I looked at Test::Deep itself first, and found that there is a Test::Deep::Array, which deals with arrays. All of the packages that handle stuff inside of T::D have a descend method. So that's where we need to hook in.

Sub::Override is great to temporarily override stuff, instead of messing with typeglobs.

Basically all we need to do is replace the call to Test::Deep::arrayelementsonly in Test::Deep::Array::descend's final line with a call to bag(). The rest is simply copied (indentation is mine). For small monkey-patching a copy of the existing code with a slight modification is usually the easiest approach.

use Test::Deep;
use Test::Deep::Array;
use Sub::Override;

my $sub = Sub::Override->new(
    'Test::Deep::Array::descend' => sub {
        my $self = shift;
        my $got  = shift;

        my $exp = $self->{val};

        return 0 unless Test::Deep::descend( 
             $got, Test::Deep::arraylength( scalar @$exp ) );

        return 0 unless $self->test_class($got);

        return Test::Deep::descend( $got, Test::Deep::bag(@$exp) );
    }
);

my $hash1 = {
    k1 => { k11 => 'v1', k12 => 'v2' },
    k2 => { k21 => [ 'v1', 'v2', 'v3' ] }
};
my $hash2 = {
    k1 => { k11 => 'v1', k12 => 'v2' },
    k2 => { k21 => [ 'v3', 'v2', 'v1' ] }
};

cmp_deeply( $hash1, $hash2 );

This will make the test pass.

Make sure to reset the override by undefining $sub or letting it go out of scope, or you might have some weird surprises if the rest of your test suite also uses Test::Deep.