tokuhirom's Blog

複雑なデータ構造の中にうまっているデータのパスをさがす方法

Perl5 の場合、Data::DPath をつかえば、

my $data = {
    foo => {
        bar => 'candy',
    },
};

のようなデータから、

say dpath('/foo/bar')->match($hashref);

などとして、candy を簡単にとりだすことができます。

しかし、ここで、たとえば以下のように複雑なデータになってしまった場合は、DPath をつくるのがまじめんどいかんじになります。

my $data = {
    foo => {
        bar => 'candy',
        boz => 3,
        iyan => {
            bakan => 7,
            yappo => 'candy',
            dan => [ 'suspender', 'hige', 'candy']
        },
    },
};

そんな場合、以下のようなコードをささっと書いてcandyをさがしましょう。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use 5.018000;
use autodie;
use Scalar::Util qw(refaddr);

package Data::Path {
    sub find_path {
        my ( $stuff, $value ) = @_;
        _apply( $value, {}, $stuff );
    }

    our @PATH;

    sub _apply {
        my $value = shift;
        my $seen = shift;

        my @retval;
        for my $arg (@_) {
            if(my $ref = ref $arg){
                my $refaddr = Scalar::Util::refaddr($arg);

                if ($seen->{$refaddr}++){
                    # noop
                }
                elsif($ref eq 'ARRAY'){
                    for (my $i=0; $i<@$arg; $i++) {
                        local @PATH = (@PATH, $i);
                        push @retval, _apply($value, $seen, $arg->[$i]);
                    }
                }
                elsif($ref eq 'HASH'){
                    for my $key (keys %$arg) {
                        local @PATH = (@PATH, $key);
                        push @retval, _apply($value, $seen, $arg->{$key});
                    }
                }
                elsif($ref eq 'REF' or $ref eq 'SCALAR'){
                    local @PATH = (@PATH, '$');
                    push @retval, _apply($value, $seen, ${$arg});
                }
            }
            else{
                if ($arg eq $value) {
                    push @retval, join("/", @PATH);
                }
            }
        }

        return @retval;
    }
}


my $data = {
    foo => {
        bar => 'candy',
        boz => 3,
        iyan => {
            bakan => 7,
            yappo => 'candy',
            dan => [ 0, 'hige', 'candy', 3, 2]
        },
    },
};
say join(" ", Data::Path::find_path($data, 'candy'));

実行すると、以下のように、DPath がえられます。

foo/iyan/yappo foo/iyan/dan/2 foo/bar

これで、キャンディがちらばってしまっても甘党のみなさんは安心ですね。

再帰的にデータをたぐるときに our をつかったダイナミックスコープで、現在位置までのパスを記録しておく方法はテストにでるのでおぼえておくと便利です。 (ダイナミックスコープがない言語の場合は、引数でひきまわしてがんばるかんじになります)