先日実施された第45回シェル芸勉強会に出席してきたので、その復習。 前回の45回はawkでゴリゴリ解いていくような問題が多かったのだけど、今回はいろんなコマンドを組み合わせて解いていくような問題が多めになっているらしい。

問題および模範解答はこちら。あと、問題を解くに当たって必要になるファイルは以下のコマンドで取得してくる。

git clone https://github.com/ryuichiueda/ShellGeiData

Q1.

csvファイル「data.csv」に、日別のトマト・バナナ・ピーマンの売れた個数が書かれているので、それぞれが記録されている最後の日の日付と個数を出力しろ、という問題。

[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < cat data.csv
2019/12/6,�g�}�g,3��
2019/11/23,�o�i�i,2��
2019/11/8,�s�[�}��,31��
2019/12/30,�g�}�g,4��
2019/11/2,�g�}�g,1��
2019/12/9,�o�i�i,4��
2019/12/21,�o�i�i,5��
2019/11/21,�s�[�}��,32��
2019/12/1,�g�}�g,7��

Shift-JISになっているので、まずはiconvなりnkfでUTF-8に変換し、そこから日付ごとにsortしてトマト・バナナ・ピーマンそれぞれの最後の行を取得してやれば良さそうだ。 sortにはGNU sortに用意されているバージョンソート(-V)を利用するのが楽そうなので、それで回答してみる。

nkf -w data.csv | sort -V | awk -F, '{a[$2]=$0}END{for(k in a)print a[k]}' | sort -V
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv
2019/12/6,トマト,3個
2019/11/23,バナナ,2個
2019/11/8,ピーマン,31個
2019/12/30,トマト,4個
2019/11/2,トマト,1個
2019/12/9,バナナ,4個
2019/12/21,バナナ,5個
2019/11/21,ピーマン,32個
2019/12/1,トマト,7個
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -V
2019/11/2,トマト,1個
2019/11/8,ピーマン,31個
2019/11/21,ピーマン,32個
2019/11/23,バナナ,2個
2019/12/1,トマト,7個
2019/12/6,トマト,3個
2019/12/9,バナナ,4個
2019/12/21,バナナ,5個
2019/12/30,トマト,4個

[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -V | awk -F, '{a[$2]=$0}END{for(k in a)print a[k]}'
2019/12/21,バナナ,5個
2019/12/30,トマト,4個
2019/11/21,ピーマン,32個

[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -V | awk -F, '{a[$2]=$0}END{for(k in a)print a[k]}' | sort -V
2019/11/21,ピーマン,32個
2019/12/21,バナナ,5個
2019/12/30,トマト,4個

その後、ebanさんがsort時にバージョンソート利用時に逆順にして条件に合致する先頭行だけを出力することで対処するという手法を編み出していた。 GNU sortではソート処理に使用する列と区切り文字を指定することができるので、それを利用した方法みたいだ。多分これが一番きれいな手法っぽい。

cd *a/*45;nkf -w data.csv | sort -rV | sort -t, -uk2,2
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < nkf -w data.csv | sort -rV | sort -t, -uk2,2
2019/12/30,トマト,4個
2019/12/21,バナナ,5個
2019/11/21,ピーマン,32個

Q2.

日経のデータダウンロードページから日経平均株価の日次csvファイルをダウンロードしてきて、そのCSVから毎月の終値の最高値と最低値を取得してくる。 ちなみに対象のcsvファイルは以下のコマンドで取得できる(wgetでいいんだけど、使ってるシェル芸botのDockerイメージに入ってないのでcurlで対処)。

curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv'
curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv' > nikkei_stock_average_daily_jp.csv

終値は2列目にあるので、これを月別に最高値、最低値を取得してけばいい。 あまりawkを使いたくないところだけど、こういうのはawk使っちゃったほうが早そうだ。以下、解答例。

curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv' | \
  nkf -w | \
  sed -e{1,\$}d -e's/"//g' | \
  awk -F, '
    {
        split($1,m,"/");
        mo=m[1]"-"m[2];
        if(b[mo]==""){b[mo]=a[mo]=$2};
        if($2>a[mo]){a[mo]=$2};
        if($2<b[mo]){b[mo]=$2};
    }
    END{
        for(k in a)print k,a[k],b[k]
    }' | sort -V
[DOCKER][root@196b32104fb3][/ShellGeiData/vol.45]
(`・ω・´) < curl -s 'https://indexes.nikkei.co.jp/nkave/historical/nikkei_stock_average_daily_jp.csv' | \ >   nkf -w | \
>   sed -e{1,\$}d -e's/"//g' | \
>   awk -F, '
>     {
>         split($1,m,"/");
>         mo=m[1]"-"m[2];
>         if(b[mo]==""){b[mo]=a[mo]=$2};
>         if($2>a[mo]){a[mo]=$2;}
>         if($2<b[mo]){b[mo]=$2} >     }
>     END{
>         for(k in a)print k,a[k],b[k]
>     }' | sort -V
2016-01 18450.98 16017.26
2016-02 17865.23 14952.61
2016-03 17233.75 16085.51
2016-04 17572.49 15715.36
2016-05 17234.98 16106.72
2016-06 16955.73 14952.02
...

もうちょい良いやり方がありそうだけど、ひとまずこれで良しとする。

Q3.

ファイル「flags_a」「flags_b」には旗の絵文字が入っているが、何箇所かに違いがある。 この2つのファイルを比較して、左上から数えて何番目に違いがあるかを出力するという問題。

「左上から数えて」というところに引っかかるけど、要は何文字目の値が違うかを比較してやればいいので、まず1行で1文字ごとになるように分解してやり、diffをしてやればいい。 旗の絵文字はUnicodeの結合絵文字になるので、普通にgrepとかで1文字を指定してしまうとうまくいかないため、(汎用性はないけど)2文字指定して対処する。あとは、diffのオプションで出力方式を切り替えてやる。

eval diff '--old-line-format="%dn: %L" --new-line-format="%dn: %L" --unchanged-line-format=' '<(grep -o .. flags_'{a,b}')'
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < eval diff '--old-line-format="%dn: %L" --new-line-format="%dn: %L" --unchanged-line-format=' '<(grep -o .. flags_'{a,b}')'
39:
65:

Q4.

「ワタナベ」のナベで使われている漢字がひたすら記述されているファイル「nabe」に対し、一文字ごとに改行を入れるという問題。 ワタナベのナベは漢字にかなり種類があって、Unicodeは異体字クラスタを利用しているため単純に一文字指定だとうまく処理できないのを利用した問題のようだ。

とりあえず、当日では以下のような方法で対処してみた。

cat /ShellGeiData/vol.45/nabe | sed 's/[亜-熙][^亜-熙]*/&\n/g'

その後、鳥海さんが書記素クラスタを利用した簡単な方法を披露。 さすがにPerlは強い…。

perl -C -nle 'print $& while (/\X/g)' nabe
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < perl -C -nle 'print $& while (/\X/g)' nabe
部
邊
邊
邊
邉
邉
邉
邊

その後、ふとgrepの-Pオプションでも書記素クラスタ使えるのではないかと思って試してみたところ、うまく動いた。 試してみるものである(´・ω・`)。

grep -Po '\X' nabe

Q5.

後半戦。

テキストファイル「message」から、回文になっている箇所をすべて抜き出すという問題。 まずすべての文字の組み合わせを抽出する必要があるので、そこから対処する。

ひとまず、以下のようにseqやawkを利用して文字の組み合わせを取得する。

cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'
きつ
きつつ
きつつき
きつつきと
きつつきとま
きつつきとまと
きつつきとまとへ
...

あとは、これらの中から回文を抽出してやればいい。 このあたりはもうperlを使ってしまったほうが楽そうだ。

cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'|perl -C -lne 'print $_ if ($_ eq reverse($_))'
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < cat message|(a=$(cat);seq -f 'echo '${a}'|grep -o .|awk "NR>%g{a=\$0=a\$0;print}"' 0 ${#a})|bash|grep -E '..+'|perl -C -lne 'print $_ if ($_ eq reverse($_))'
きつつき
つつ
とまと
とまと
けやぶやけ
やぶや
たけやぶやけた
けやぶやけ
やぶや
ゆんゆ
んゆん
おかしがすきすきすがしかお
かしがすきすきすがしか
しがすきすきすがし
がすきすきすが
すきす
すきすきす
きすき
すきす

なお、すべての組み合わせの抽出については鳥海さんが大変きれいな正規表現を残してくれてたので、それについても残しておく。

perl -C -lne '/..+(?{print$&})(?!)/' message

これを使って回文を抽出した場合が以下。 (多分もっと短く書けるんだろうけど)これだけでもだいぶ短くなった(やっぱperlはすごい)。

perl -C -lne '/..+(?{print$&})(?!)/' message|perl -C -lne 'print $_ if ($_ eq reverse($_))'

Q6.

Q5の回答が入っているファイル「message.ans」というファイルから、部分的な回文を除外するという問題。 部分的な回文と言われて何すれば良いのかよくわかってなかったのだけど、どうやらmessage.ans内から「きつつき」の一部である「つつ」に相当するような回文を消すという内容らしい。

つまりmessage.ans内の各文字列でgrepによる抽出を行い、一度しかヒットしないものだけを抽出すればいいということになる(複数回ヒットする回文は回文内の文字列である可能性が高いため)。 以下が回答。

cat message.ans | xargs -I@ grep -o @ message.ans  | sort | uniq -u
[DOCKER][root@0873ee07bf06][/ShellGeiData/vol.45]
(`・ω・´) < cat message.ans | xargs -I@ grep -o @ message.ans  | sort | uniq -u
おかしがすきすきすがしかお
きつつき
たけやぶやけた
とまと
ゆんゆ
んゆん

Q7.

x/yが割り切れない自然数の組x,y (x<y)について、echo x yからはじめて、計算結果(小数)を延々と出力しなさいという問題。 小数点以下を延々と計算し続けろという内容になる。楽しようとしてなんか良いコマンドないかと探してたのだけど見つからず、残念ながら解けなかった。

とりあえず模範解答を貼っておく。

echo 1 7 | awk '{print "0.";while(1){print int($1*10/$2);$1=($1*10)%$2}}' | tr -d \\n

Q8.

Q7の結果に対して、循環小数になっているのでその小数点以下の出力を途中で打ち切って小数何桁目から循環しているのかを出力するという問題。 こちらもちょっと解けなかったので、模範解答を貼る。

echo 1 7 | awk '{print "0.";while(1){printf int($1*10/$2);$1=($1*10)%$2;print " "$1}}' | awk '{b[NR-1]=$2;for(i=1;i<NR-1;i++){if(b[i]==b[NR-1]){print a, i,NR-2;exit}}a=a$1}'

いつものことではあるが、後半の問題が難易度が高いのであまり解けない。 今回は比較的前半の問題が易しめ(というよりいろんな解き方がある)だったように感じたので、いろいろな解き方を見れて勉強になった。