円周率の何桁目に日付表記が来るか?

数学の日にちなんで、円周率クイズというお題が結城先生のお題にあり、小飼さんがmath/perl - 日付のhyuki表現として Perl で解かれています。 ここではもちろん awk で計算してみたいと思います。

普通の awk では円周率を正確に計算することができません。 そこで、bc コマンドを使います。 awk は awk そのもので全てを処理させる言語というよりも他の処理からの出力に対して grep + αを行う方が awk の趣旨のようです。 これはThe A-Z of Programming Languages: AWK (英語) にも書かれています。

さて、bc コマンドで円周率を任意精度で求めるには以下のようにします。

$ echo "scale=10^3; 4*a(1)" | bc -l
3.141592653589793238462643383279502884197169399375105820974944592307\
81640628620899862803482534211706798214808651328230664709384460955058\
22317253594081284811174502841027019385211055596446229489549303819644\
28810975665933446128475648233786783165271201909145648566923460348610\
45432664821339360726024914127372458700660631558817488152092096282925\
40917153643678925903600113305305488204665213841469519415116094330572\
70365759591953092186117381932611793105118548074462379962749567351885\
75272489122793818301194912983367336244065664308602139494639522473719\
07021798609437027705392171762931767523846748184676694051320005681271\
45263560827785771342757789609173637178721468440901224953430146549585\
37105079227968925892354201995611212902196086403441815981362977477130\
99605187072113499999983729780499510597317328160963185950244594553469\
08302642522308253344685035261931188171010003137838752886587533208381\
42061717766914730359825349042875546873115956286388235378759375195778\
18577805321712268066130019278766111959092164201988

このままでは使いにくいので、awk のアクションで連接したものを使います。

日付に該当する部分のマッチングには正規表現を for 文で作成し、これを連接して使うことにします。 使いにくいデータは使いやすいデータにすればいいわけです。 具体的なコードは以下のようになります。

#! /usr/bin/gawk -f
# pi_date.awk

BEGIN {
    month = split("31 28 31 30 31 30 31 31 30 31 30 31", days);

    for (i = 1; i <= month; i++) {
        for (j = 1; j <= days[i]; j++) {
            month_day = month_day sprintf("%02d%02d|", i, j);
        }
    }
    sub(/^/, "(", month_day);
    sub(/\|$/, ")", month_day);

    for (i = 0; i <= 23; i++) {
        hour = hour sprintf("%02d|", i);
    }
    sub(/^/, "(", hour);
    sub(/\|$/, ")", hour);

    for (i = 0; i <= 59; i++) {
        min_sec = min_sec sprintf("%02d|", i);
    }
    sub(/^/, "(", min_sec);
    sub(/\|$/, ")", min_sec);

    regexp = month_day hour min_sec min_sec;
}

{
    sub(/3\./, "");
    gsub(/ /, "");
    sub(/\\/, "");
    pi = pi $0;
}

END {
    while (match(pi, regexp)) {
        if (match(pi, regexp)) {
            printf("%02d/%02d %02d:%02d:%02d @%d\n", \
                   substr(pi, RSTART,     2),
                   substr(pi, RSTART + 2, 2),
                   substr(pi, RSTART + 4, 2),
                   substr(pi, RSTART + 6, 2),
                   substr(pi, RSTART + 8, 2),
                   pre_ans + RSTART);
            pre_ans = pre_ans + RSTART + 10;
        }
        pi = substr(pi, RSTART + RLENGTH + 1);
    }
}

ちょっと長めですが、いくつかの部分に分割されているので、難しいところはありません。 先ほどの bc の結果をパイプで接続してみましょう。

$ echo "scale=10^3; 4*a(1)" | bc -l | nawk -f pi_date.awk
07/26 02:49:14 @287
09/17 15:36:43 @340
05/13 20:00:56 @596
09/01 22:49:53 @657
10/10 00:31:37 @852

つまり、小数点以下 287 桁目に最初の日付に該当する部分が来ます。

小飼さんの Perl だと GMP を使っていますが、xgawk であれば MPFR を使って円周率を求めることができます。

#! /usr/bin/gawk -f
# pi_date_xgawk.awk

@load mpfr

BEGIN {
    MPFR_PRECISION = 4000;

    pi = mpfr_const_pi();

    month = split("31 28 31 30 31 30 31 31 30 31 30 31", days);

    for (i = 1; i <= month; i++) {
        for (j = 1; j <= days[i]; j++) {
            month_day = month_day sprintf("%02d%02d|", i, j);
        }
    }
    sub(/^/, "(", month_day);
    sub(/\|$/, ")", month_day);

    for (i = 0; i <= 23; i++) {
        hour = hour sprintf("%02d|", i);
    }
    sub(/^/, "(", hour);
    sub(/\|$/, ")", hour);

    for (i = 0; i <= 59; i++) {
        min_sec = min_sec sprintf("%02d|", i);
    }
    sub(/^/, "(", min_sec);
    sub(/\|$/, ")", min_sec);

    regexp = month_day hour min_sec min_sec;

    sub(/3\./, "", pi);

    while (match(pi, regexp)) {
        if (match(pi, regexp)) {
            printf("%02d/%02d %02d:%02d:%02d @%d\n", \
                   substr(pi, RSTART,     2),
                   substr(pi, RSTART + 2, 2),
                   substr(pi, RSTART + 4, 2),
                   substr(pi, RSTART + 6, 2),
                   substr(pi, RSTART + 8, 2),
                   pre_ans + RSTART);
            pre_ans = pre_ans + RSTART + 10;
        }
        pi = substr(pi, RSTART + RLENGTH + 1);
    }
}

円周率を求める部分以外は全く同じルーチンで書かれています。 MPFR_PRECISION は多めに設定しています。 実行してみましょう。

$ xgawk -f pi_date_xgawk.awk
07/26 02:49:14 @287
09/17 15:36:43 @340
05/13 20:00:56 @596
09/01 22:49:53 @657
10/10 00:31:37 @852

ということで、円周率の算出まで含めて awk の枠内で行うことが可能です。

ちなみに、ここで自動的に生成している正規表現ですが、美しくありません。

(0101|0102|0103|0104|0105|0106|0107|0108|0109|0110|0111|0112|0113|0114|0115|\
0116|0117|0118|0119|0120|0121|0122|0123|0124|0125|0126|0127|0128|0129|0130|\
0131|0201|0202|0203|0204|0205|0206|0207|0208|0209|0210|0211|0212|0213|0214|\
0215|0216|0217|0218|0219|0220|0221|0222|0223|0224|0225|0226|0227|0228|0301|\
0302|0303|0304|0305|0306|0307|0308|0309|0310|0311|0312|0313|0314|0315|0316|\
0317|0318|0319|0320|0321|0322|0323|0324|0325|0326|0327|0328|0329|0330|0331|\
0401|0402|0403|0404|0405|0406|0407|0408|0409|0410|0411|0412|0413|0414|0415|\
0416|0417|0418|0419|0420|0421|0422|0423|0424|0425|0426|0427|0428|0429|0430|\
0501|0502|0503|0504|0505|0506|0507|0508|0509|0510|0511|0512|0513|0514|0515|\
0516|0517|0518|0519|0520|0521|0522|0523|0524|0525|0526|0527|0528|0529|0530|\
0531|0601|0602|0603|0604|0605|0606|0607|0608|0609|0610|0611|0612|0613|0614|\
0615|0616|0617|0618|0619|0620|0621|0622|0623|0624|0625|0626|0627|0628|0629|\
0630|0701|0702|0703|0704|0705|0706|0707|0708|0709|0710|0711|0712|0713|0714|\
0715|0716|0717|0718|0719|0720|0721|0722|0723|0724|0725|0726|0727|0728|0729|\
0730|0731|0801|0802|0803|0804|0805|0806|0807|0808|0809|0810|0811|0812|0813|\
0814|0815|0816|0817|0818|0819|0820|0821|0822|0823|0824|0825|0826|0827|0828|\
0829|0830|0831|0901|0902|0903|0904|0905|0906|0907|0908|0909|0910|0911|0912|\
0913|0914|0915|0916|0917|0918|0919|0920|0921|0922|0923|0924|0925|0926|0927|\
0928|0929|0930|1001|1002|1003|1004|1005|1006|1007|1008|1009|1010|1011|1012|\
1013|1014|1015|1016|1017|1018|1019|1020|1021|1022|1023|1024|1025|1026|1027|\
1028|1029|1030|1031|1101|1102|1103|1104|1105|1106|1107|1108|1109|1110|1111|\
1112|1113|1114|1115|1116|1117|1118|1119|1120|1121|1122|1123|1124|1125|1126|\
1127|1128|1129|1130|1201|1202|1203|1204|1205|1206|1207|1208|1209|1210|1211|\
1212|1213|1214|1215|1216|1217|1218|1219|1220|1221|1222|1223|1224|1225|1226|\
1227|1228|1229|1230|1231)(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
16|17|18|19|20|21|22|23)(00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|\
16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|\
40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59)(00|01|02|03|\
04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|\
28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|\
52|53|54|55|56|57|58|59)

考えればもう少し精査された正規表現が可能かもしれませんが、慣れないうちは場合の数で正規表現を生成するのも悪くないものです。 特に awk の正規表現は他の言語と比較するとリッチとは言えないので、自分が分かる範囲で無難に作っても良いでしょう。

tag_nawk.pngtag_nawk.pngtag_nawk.pngtag_nawk.png