はじめに
fishシェルスクリプトの練習のため,言語処理100本ノックに挑戦してみました.
シェルからawk
等を呼ぶと趣旨が変わってしまうので,できる限りfishに組み込みのコマンドで解くことにします.
ただし,連番生成のためのseq
は使ってもよいものとしました.
(わざわざカウンタ付きwhileループするのも冗長なので...)
参考にした記事: 言語処理100本ノック with Python(第1章)
第1章: 準備運動
お題となる文字列を変数input
に格納したところからスタートします.
set -l input # まずローカル変数inputを生成.以下,これを使いまわす.
00. 文字列の逆順
文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.
set input 'stressed'
string join '' (string split '' $input)[-1..1]
文字列→文字の配列はstring split ''
,文字の配列→文字列はstring join ''
で変換.
fishのコマンド置換()
で複数行を出力すると配列に展開されるので,それをその場で[-1..1]
でひっくり返します.
01. 「パタトクカシーー」
「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.
set input 'パタトクカシー'
string join '' (string split '' $input)[1 3 5 7]
前の問題とほぼ同じ.
02. 「パトカー」+「タクシー」=「パタトクカシーー」
「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
set input 'パトカー' 'タクシー'
for i in (seq 1 (string length $input[1]))
string sub -s $i -l 1 $input
end | string join ''
変数input
を配列として,「パトカー」,「タクシー」を格納した状態からスタート.文字数の分だけfor
ループを回して処理します.
string sub
が配列input
の各要素を処理してくれるので,for
ブロックからの出力をstring join
でくっつけると完成.
03. 円周率
"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
set input 'Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.'
echo (string length (string match -ar '\w+' $input))
単語への分解はstring match
で実行.前の問題のstring sub
もそうでしたが,string
は複数の文字列を同時に処理してくれるのでそのままstring length
に渡すと完成.
04. 元素記号
"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.
set input 'Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.'
set -l elem (string match -ar '\w+' $input)
for i in (seq 1 (count $elem))
set elem_(contains $i 1 5 6 7 8 9 15 16 19
and string sub -s 1 -l 1 $elem[$i]
or string sub -s 1 -l 2 $elem[$i]) $i
end
fishに辞書型なんてものは存在しない1のでelem_元素名
という変数を作って擬似的な辞書とします.上記を実行した環境ではecho $elem_Al
で13
が得られます.
05. n-gram
与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.
function n-gram -a num
set -e argv[1]
if test "$num" -le (count $argv)
string join - $argv[1..$num]
n-gram $num $argv[2..-1]
end
end
set input "I am an NLPer"
n-gram 2 (string match -ar '\w+' $input) # 単語bi-gram
n-gram 2 (string split '' $input) # 文字bi-gram
指定された数の要素だけ配列から取り出して再帰でループ.単語n-gramのために区切り文字が必要なので,ここでは-
を使いました.
06. 集合
"paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.
function unique
set -l uniq
for a in $argv
if not contains $a $uniq
set uniq $uniq $a
echo $a
end
end
end
function union -S
unique $$argv
end
function intersect -S
for x in $$argv[1]
contains $x $$argv[2]; and echo $x
end
end
function difference -S
for x in $$argv[1]
contains $x $$argv[2]; or echo $x
end
end
set input "paraparaparadise" "paragraph"
set -l X (unique (n-gram 2 (string split '' $input[1])))
set -l Y (unique (n-gram 2 (string split '' $input[2])))
echo "X" $X
echo "Y" $Y
contains s-e $X; and echo "s-e は X に含まれます!"
contains s-e $Y; and echo "s-e は Y に含まれます!"
echo "X|Y" (union X Y)
echo "X-Y" (difference X Y)
echo "X&Y" (intersect X Y)
fish関数縛りをしているのでなければ,配列から重複を消すのにはsort
とuniq
を使うべきな気がする.
和集合・差集合・積集合の作成には複数の配列を受け取る必要があるので-S(--no-scope-shadowing)
で関数を定義.$
が連続する独特な記法になります.和集合union
の定義がキモいですね.
07. テンプレートによる文生成
引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.
function template -a x y z
echo "$x時の$yは$z"
end
template 12 気温 22.4
シェルなのでこれは非常に簡単.
08. 暗号文
与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.
- 英小文字ならば(219 - 文字コード)の文字に置換
- その他の文字はそのまま出力
この関数を用い,英語のメッセージを暗号化・復号化せよ.
function cipher
for c in (string split '' "$argv")
string match -qr '[a-z]' $c
and echo -e \\x(printf '%x' (math 219 - (printf '%d' \'$c)))
or echo "$c"
end | string join ''
end
cipher (cipher "Hello world!")
fishだけで文字コード取得とかできるのだろうか...としばらく悩みましたがprintf
で可能な模様です.
09. Typoglycemia
スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.
set input "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."
function typoglycemia
set -l array (string split ' ' "$argv")
if test (count $array) -lt 3 # 入力が3語以下なら何もしない
echo $array
else
echo $array[1]
set -l buf (for word in $array[2..-2] # 5文字以上の単語を一時変数に格納
test (string length $word) -gt 4; and echo $word
end)
for word in $array[2..-2]
if test (string length $word) -gt 4 # 5文字以上の単語を他の語に入れ替え
set -l i (random 1 (count $buf) ^/dev/null; or echo 1)
echo $buf[$i]
set -e buf[$i]
else
echo $word
end
end
echo $array[-1]
end | string join ' '
end
# pasteで確認
paste (string split ' ' $input | psub) (string split ' ' (typoglycemia $input) | psub)
fishのコマンド置換()
は途中で普通に改行してforループ回せたりするので,それを利用して入れ替えに使う単語の配列(buf
)を作成.
他のスクリプト言語だともっとあっさり書けるんですかねこれ...?
おわりに
fishで言語処理100本ノック (第2章) に続きたいです.
-
fishで連想配列を使うためのプラグインがoh-my-fishにあります(oh-my-fish/plugin-assoc)....が,これは内部でrubyを呼び出しています.それをするくらいなら最初からrubyで書けばよいのではという気がしないでもないです. ↩