16進数のテーブルを表示するワンライナーを考える

以下のような256個の16進2桁の数値を一覧表示したい、とする。

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F
30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F
50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F
60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F
70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F
80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F
90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F
A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF
B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF
C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF
D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF
E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF
F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF

スクリプトで書く

馬鹿丁寧にスクリプトを書くなら、以下のようなカンジになるだろうか。

#!/usr/bin/perl
use strict;
use warnings;

my @a = (0..9, 'A'..'F');
for my $i (@a) {
    for my $j (@a) {
        print "$i$j ";
    }
    print "\n";
}

敢えて文字列として処理してみている。数値を"%02X"で表現する方法もあるだろうけど。
各行の末尾にもスペースが入ってしまうのが気になったら、map関数なんかを使って

#!/usr/bin/perl
use strict;
use warnings;

my @a = (0..9, 'A'..'F');
for my $i (@a) {
    print join(" ", map { "$i$_" } @a), "\n";
}

とやるとキレイに表示できるんではないだろうか。

ワンライナーにしてみる

…と、ここまで短くなるとワンライナーにできそうな気がしますね。
単純に繋げてみると

$ perl -e 'my @a = (0..9, 'A'..'F'); for my $i (@a) { print join(" ", map { "$i$_" } @a), "\n"; }'

と。
ここから削る作業。
改行はコマンドラインスイッチ"-l"に任せよう

$ perl -le 'my @a = (0..9, 'A'..'F'); for my $i (@a) { print join(" ", map { "$i$_" } @a); }'

myは無くても大丈夫そうなので取り除いてしまおう

$ perl -le '@a = (0..9, 'A'..'F'); for $i (@a) { print join(" ", map { "$i$_" } @a); }'

@aへの代入は使うときに代入と同時に評価してもらうことにしよう

$ perl -le 'for $i (@a = (0..9, 'A'..'F')) { print join(" ", map { "$i$_" } @a); }'

やっぱり行末のスペースは気にしないことにしてjoinを使うのをやめよう

$ perl -le 'for $i (@a = (0..9, 'A'..'F')) { print map { "$i$_ " } @a; }'

map関数の第1引数の書式を変更しよう

$ perl -le 'for $i (@a = (0..9, 'A'..'F')) { print map "$i$_ ", @a; }'

さらに無くても大丈夫なクォートやセミコロンを削除しよう

$ perl -le 'for $i (@a = (0..9, A..F)) { print map "$i$_ ", @a }'

あとはスペースを詰めてしまえば

$ perl -le'for$i(@a=(0..9,A..F)){print map"$i$_ ",@a}'

と、素敵なワンライナーの出来上がりです。

for文を後置して使いたい

個人的な好みとして、ワンライナー中のfor文は式修飾子を使って後置の形で使いたい。

for $i (配列) {
    処理;
}

ではなく、

処理 for (配列)

と。


ただし、今回の場合ここで1つ問題がある。
式修飾子でfor文を回す場合、処理の中でのデフォルト変数が必ず'$_'になってしまう。
今回の問題はfor文の中でmap関数を使っていて、その中でもデフォルト変数'$_'を使うので、名前が衝突していてmap関数内で$_が上書きされてしまう。
ということはfor文内の処理の中でmap関数に入る前に他の名前の変数に$_を代入してやる必要がありそうだ。
とは言えセミコロンで区切ってしまうとまったく別の文としてfor文と分けられてしまう。
そんなときは普通、'and'や'&&'を使って文を繋げたりする。

$ perl -le'$i=$_ and print map"$i$_ ",@a for@a=(0..9,A..F)'

と、こんな具合に。

  • $i に $_ を代入
  • map関数で @a の各要素を$iに付け足した文字列を生成

という2つの動作をfor文の毎回のループ中で行うことになる。


…が。ここで実行結果をみてみると、正しく表示されないことに気付く。。

$ perl -le'$i=$_ and print map"$i$_ ",@a for@a=(0..9,A..F)'
10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 
20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 
30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 
40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 
50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 
60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 
70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F 
80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 
90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F 
A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF 
B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF 
C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF 
D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF 
E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF 
F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF 

なんと、最初の段(00〜0F)が消えているではないか。
何故かというと、($i=$_)の評価の結果が偽になってしまうとその後の'print map"$i$_ ",@a'が実行されないから。

$i = $_

という代入式は、代入された値そのものが評価される。1が代入されていれば1に、2が代入されていれば2に。
従って、0を代入した場合は0に、すなわちPerlでのFalseになってしまう。


ということで、このような代入処理を他の処理と繋げたい場合、安易に'and'や'&&'を使ってはいけない。
必ず次の処理に移って欲しい、という場合は代入結果にかかわらず確実に真値が'and'に渡るようにする必要がある。
例えば

$ perl -le'($i=$_)||1 and print map"$i$_ ",@a for@a=(0..9,A..F)'

と、より優先順位の高い論理演算から真値を返すよう誘導したり、

$ perl -le'($i=$_)?1:1 and print map"$i$_ ",@a for@a=(0..9,A..F)'

と、三項演算子で両方真値になるよう仕組んでみたり。

結論

ややこしい代入が必要ならfor文は後置せずに使った方がよい。

$ perl -le'for$i(@a=(0..9,A..F)){print map"$i$_ ",@a}'

結局これがベストだと思う。

追記

id:kazuhookuさんからブクマコメントとトラックバックをいただいた。ありがとうございます!
はてなブックマーク - 16進数のテーブルを表示するワンライナーを考える - すぎゃーんメモ

xor 使えばいいんじゃないか

なるほど…!!


論理和は左辺が偽値のときに右辺が評価され、論理積は左辺が真値のときに右辺が評価される。
排他的論理和の場合は…必ず左辺も右辺も評価される、のかな?
とにかく、排他的論理和xorを使えば、左辺の評価結果がどうあれ確実に次の処理に進めるらしい。
for文を後置させる場合は以下のように書けばよりすっきりしますね。

$ perl -le'($i=$_)^print map"$i$_ ",@a for@a=(0..9,A..F)'


そしてトラックバック

perl -e'printf"%02X%c",$_,$_+1&15?32:10for 0..255'

と書いた方が2バイト短いかなと思いました。まだ縮むんだろうなぁ。

perl golf - kazuhoのメモ置き場

なるほど…!
最初に数値からの"%02X"への変換を考えたときに、どうやって16個ずつで区切って改行したものかと思い悩み断念したのですが、そうやって条件分岐させてスペースと改行を書き分ければ良かったんですね。


while文を使ってみたらどうなるかな、と試してみましたが、結局1バイト長くなってしまいました。

perl -e'printf"%02X%s",$x++,$x&15?$":$/while$x<256'  

追々記

各行末尾にスペース入っちゃうけど、5バイト短くできた!

perl -e'$s="%02X "x16;printf"$s\n"x16,0..255'

追々々記

さらにブクマコメントいただいた!id:yzxさん、ありがとうございます ><
はてなブックマーク - 16進数のテーブルを表示するワンライナーを考える - すぎゃーんメモ

($i=$_)^printは$i=$_,printでいいんじゃないの

全然気がつきませんでした orz
カンマで区切れば代入もその後の処理も実行されるんですね。。。
というわけでfor文を後置させる場合はこれで。

perl -le'$i=$_,print map"$i$_ ",@a for@a=(0..9,A..F)'

ちょっと変な配列名にしてしまえばもう1バイト詰められる。

perl -le'$i=$_,print map"$i$_ ",@!for@!=(0..9,A..F)'

こうなれば通常のfor文を使う場合と長さは一緒だ!

追々々々記

長くなり過ぎたので別エントリでまとめました。
16進数のテーブルを表示するワンライナーを考える まとめ - すぎゃーんメモ