一次元配列同士の比較のいろいろなやり方

問題

@a = (1,2,3); @b = (1,2,3);

のような配列があったとして、これらの中身が同じ(@a = @b) であることを確認したいと思います。
細かい条件として、値は正の数字のみ(負はない、)で並び順は気にしない、かつ同じ番号はないという事にします。

かんたんに考えると、次の二つの条件を満たせればいいはずです。

  1. 二つの要素の数が同じであること
  2. 先頭から比較して、末尾までの各項目の内容が等しいこと

素直にコーディングすると...

sub is_same($$){
	my($a,$b) = @_;
	
	# (1)二つの要素の数が同じであること
	return 0 if @$a != @$b;
	
	# (2)先頭から比較して、末尾までの内容が等しいこと
	for (0..$#$a){
		return 0 if $a->[$_] != $b->[$_];
	}
	
	return 1;
}

というサブルーチンとして問題なく書けます。

しかし、こんな単純なことは単純に済ませてしまいたい。
「配列@a と@b はいっしょ?」と聞くように「@a == @b ?」のように表現してやさしくしておきたいというのが今回のモチベーションです。

いろいろなやり方

このような場合にどんなやり方があるのか、yokohama.pm のみなさんにirc(irc.freenode.net の#yokohama.pm) で素朴な疑問をぶつけてみました。
さすがに、たくさんの答えをいただきました。
その答えの紹介をするとともに、実際に具体的なやり方を調べていってみましょう!

Array::Diff

19:01:05 tomyhero: typester先生の Array::Diff ではないでしょうか

perlsh を使って調べてみましょう。

[harupiyo@localhost ~]$ perlsh
main[2]$ use Array::Diff
main[2]$ Array::Diff->diff([1,2,3],[1,2,3])->count
0	# 違いがない
main[3]$ Array::Diff->diff([1,2,3],[1,9,3])->count
1	# 一個違う

count メソッドで違いの数が数えられますので0 であれば相違いないということになります。

List::Compare

19:14:12 nekokak: http://search.cpan.org/~jkeenan/List-Compare-0.37/lib/List/Compare.pmとかもつかえそげ(つかったことないけども

これはドキュメントがすごく大きいですね!*1
Array::Diff が軽量級とするなら、List::Compare は超ヘビー級といえます。

思わずびびってしまいますが、get_symdiff が使えそうです。

main[3]$ use List::Compare
main[4]$ @a = (1,2,3); @b = (1,2,3); @c = (1,3,5);
1
3
5
main[5]$ $lc = List::Compare->new(\@a,\@b)
main[8]$ $lc->get_symdiff
	# 違いはない
main[9]$ $lc = List::Compare->new(\@a,\@c)
main[10]$ $lc->get_symdiff
2	# 違いが発見された
5
is_deeply

19:02:50 typester: is_deeply \@a, \@b

こちらはTest::More のis_deeply ですね。
is_deeply は、単なる一次元配列だけでなく、配列やハッシュが入れ子になっているより複雑な構造も比較できます。

main[1]$ use Test::More qw/no_plan/
main[2]$ @a = (1,2,3); @b = (1,2,3); @c = (1,3,5);
1
3
5
main[3]$ is_deeply(\@a, \@b)
ok 1
1
main[4]$ is_deeply(\@a, \@c)
not ok 2	# 違う時はこんな風に怒られます
#   Failed test at (eval 7) line 1.
#     Structures begin differing at:
#          $got->[1] = '2'
#     $expected->[1] = '3'
0
eq_array

20:03:41 miyagawa: Test::More の eq_array
20:03:48 miyagawa: がまさにそれなのだがuseするとtestモードになってしまう

先ほどと同様にTest::More モジュールですが、更に適切なメソッドeq_array がありました。

ところで、確かにTest::More をuse したらtest モードになるという問題がありますね。is_deeply でも、use するときにqw/no_plan/ を指定しないとis_deeply が動いてくれないという問題でもありました。
miyagawa さんからはこの指摘の後で、require するか、use する場合でも何もインポートしないのであればtestモードにならないのでいいかなというアドバイスをいただきました。

試してみます。

[harupiyo@localhost ~]$ perlsh
main[j]$ require Test::More
1
main[2]$ Test::More::eq_array([1,2,3],[1,2,3])
1
main[3]$ exit

[harupiyo@localhost ~]$ perlsh
main[1]$ use Test::More
main[2]$ Test::More::eq_array([1,2,3],[1,2,3])
1
main[3]$ exit

いい感じで使えています。Test::More を使うならeq_array がいいですね。

join

19:03:23 nekokak: join(',',@aaa) eq join(',',@bbb)
19:05:48 nekokak: join("\0",@aaa) eq join("\0",@bbb)
19:05:50 nekokak: とかw

これは、(1,2,3) という配列を、'1,2,3' という文字列に変換して、文字列として比較しています。
配列の中身がソート済みであれば重複の要素があってもOK、イメージしやすく、簡潔でいいですね。
なお、区切り文字はカンマでなくても良いわけです。上では"\0"なんてのも指定していますし、下のように"nekokak" なんて文字列でも、あの子への淡い想いでもいいわけです。遊び心があります。

main[11]$ join('nekokak',(1,2,3)) eq join('nekokak',(1,2,3))
1
Data::Dumper, JSON

19:07:20 kan_fushihara: Data::Dumper; Dumper(@a) eq Dumper(@b)
19:07:37 nekokak: それをいったらJSONで(ry

こちらもDumper で変数の中身を文字列にして比較しています。
Dumper は一次元配列だけでなく、複雑な構造もきちんと書き出してくれますから、上で取り上げたis_deeply の性質も併せ持っています。

同様にJSON にもto_json メソッドがあり、これを使って文字列にできますのでData::Dumper 同様の書き方ができます。
ちょっとto_json の動きを確認しておきましょう。

main[33]$ use JSON
main[34]$ to_json([ 1, 2, [1,2,3], {4=>5,6=>[7,8]} ])
[1,2,[1,2,3],{"6":[7,8],"4":5}]

to_json を使えば上手くいきそうですね。

unique

19:07:10 zigorou_: Array::Utils qw(unique) で簡単に書けそう

unique は、渡された@a と@b の中から数の種類を選び出します。

main[12]$ unique(1,2,3)
1
3
2
main[13]$ unique(1,2,3,4,1,2,3)
4
1
3
2

表示される順番は変わっていますが、ちゃんとunique なものだけになっています。
で、これを使って、"@a と@b を合わせた種類の数"と、"@a の要素数" が等しくなれば良いはずです。
(重複した数字はないという条件ですので。)

よって、

scalar(unique(@a,@b)) == scalar(@a)

と書けます。

ちょっとやってみましょう。

main[55]$ @a = (1,2,3)
1
2
3
main[56]$ @b = (1,2,3)
1
2
3
main[57]$ unique(@a,@b)
1
3
2
main[58]$ scalar(@a)
3
main[59]$ scalar(unique(@a,@b))
3
main[61]$ scalar(unique(@a,@b)) == scalar(@a)
1	# マッチした
main[61]$ scalar(unique(@a,@b,9)) == scalar(@a)
	# マッチしない
main[63]$ unique(@a,@b) == @a
1	# この書き方でもOK

なお、== 演算子スカラーコンテキストですので、最後の例のように左右のscalar() は省略可能です。

難読unique

19:24:27 junichir_: CPAN 使わなければ、
19:24:30 junichir_: my %c;
19:24:30 junichir_: if (scalar( grep { ($c{$_}++ eq 1) } ( @a, @b )) eq scalar(@a) ) {
19:24:31 junichir_: }
19:24:37 junichir_: はい、カオス。

こういう難読なのも不思議度が増して好きですw
こちらも、unique と同様、ユニークなものの個数を数えるやり方です。
grep では、(@a,@b) から、ふたつある要素を抜き出しています。ふたつあるかどうかを管理するために、%c を使っています。

Array::Utils のunique もそうですがユニークなものの個数を数えるやり方は、あくまで重複がないことが保証されていて、かつ並び順は気にしなくても良い配列のみの比較しかできないということに注意してください。

おわりに

…さて、何で今回こんな配列の比較をやりたいと思ったのかと言うと、データベースに入っているデータが範囲指定した中で連続かどうか調べたいためでした。
たとえば100〜150番目のレコードが途中削除されたりせずに存在しているかどうかを調べたかったので、

@a = (100..150);
@b = (100〜150 を指定して取ってきたレコードの連番を配列にしたもの)

として、@a と@b が同じかを調べたかったというわけでした。

朝、シャワーを浴びながら「そういえばあれって…」と気づいたことがあります。
「取ってきたレコードの数が50かどうか調べればいいんだよな。」


…厳粛な事実に、湯気のなかでショボーンとしたのでした。

*1:Array::Diff に対するmiyagawa さんのレビュー (http://cpanratings.perl.org/dist/Array-Diff) にも、そのことが取り上げられています。