JSON処理のコマンドラインツールであるjqは、JSONテキストを1行でちゃくっと解析、変換するときに使うものです。複雑な分岐や制御が絡んできたら、sedやawkなど他の文字列処理ユーティリティと組み合わせて何とかするのが通例です。単体で複雑なことをしたりはしません。
とは言え、jqにも変数定義、forやifなどの制御構造、関数定義などプログラミング言語らしき機能が備わっているので、「やればできんじゃねぇ」と野望を募らせてしまうこともあります。
というわけで、ここではフィボナッチ数と素数の生成に挑戦します。
コード
コード(フィルタファイル)はfibo.jqとprime.jqで、次のGithubから取得できます(短いのでコピペでも十分ですが)。
https://github.com/stoyosawa/jqDoc-public/
フィルタファイルの改行はUnixスタイルのLFだけでなければならないので、保存時に注意してください。Windows流にCRLFだと次のエラーが報告されます。
jq: error: syntax error, unexpected INVALID_CHARACTER (Unix shell quoting issues?)
at <top-level>, line 1:
フィルタファイルの使い方
これらファイルをjqから呼び出すときは、次のオプションを指定します。
-
-fオプション(--from-file)ーフィルタファイルを読み込みます。 -
-nオプション(--null-input)ー数列は内部で生成されるので、JSONデータを外部から読み込まないよう指示します。 -
-cオプション(--compact-output)ーデフォルトでは配列要素(数列は配列に収容される)が縦に表示されるので長くなります。これで読みやすくなります。 -
--argオプションー生成する数列の数を制限する変数値をセットします。
まとめると、次のようになります。
$ jq --arg loop 20 -cnf fibo.jq # フィボナッチ数を20個まで生成
$ jq --arg max 60 -cnf prime.jq # 60までの素数を探す
フィボナッチ数
フィボナッチ数列(Fibonacchi sequence)は、1つ前と2つ前の要素の和を次の要素の値とする数列です。0番目の要素は0、1番目の要素は1と決められているので、2番目は0+1=1、3番目は1+1=2です。定義は次のとおりです。
F(0) = 0 # 0番目のフィボナッチ数
F(1) = 1 # 1番目のフィボナッチ数
F(n) = F(n-1) + F(n-2) # n番目のフィボナッチ数(n≧2)
フィルタは次の通りです。
($loop | tonumber) as $loop |
[0, 1] |
while(
length <= $loop;
(
length as $len |
. + [.[$len-1] + .[$len-2]]
)
)
-
--argコマンドラインオプションで指定した変数loopは、フィルタ内では$loopで参照できます。コマンドライン引数は文字列なので、tonumberで数値に変換して$loopに入れ直します。なお、変数定義の... as $x文は、jqのパイプライン処理(|)には影響を与えません。データの流れという観点では無視して構いません。 - フィボナッチ数の最初の2要素を配列で定義します。以降、この配列(
.で参照)に要素を追加していきます。 - ループです。
whileには条件式と更新式という、2つの制御要素が含まれます。 - ループは条件式が
trueの間は回り続けます。ここでは配列(.)の長さをlengthから調べ、それが変数$loopより大きくなったら終了します。 - 更新式では、入力配列を変更します。わかりやすいように括弧
()でまとめています(なくても問題ありませんが)。 - 更新式ではまず、現在の配列長を変数
$lenに代入します。これは、配列中の要素を参照するときに用います。 - 続いて、現在の配列
.に末尾の要素(.[$len-1])とその1つ前の要素(.[$len-2])の和を加えます。和の値(たとえば0+1=1)をさらに[]で括ることで配列化しているのは、配列の加算(要素の追加)ではどちらも配列でなければならないからです。
実行例を示します。
$ jq --arg loop 20 -cnf fibo.jq
[0,1]
[0,1,1]
[0,1,1,2]
[0,1,1,2,3]
[0,1,1,2,3,5]
[0,1,1,2,3,5,8]
[0,1,1,2,3,5,8,13]
[0,1,1,2,3,5,8,13,21]
[0,1,1,2,3,5,8,13,21,34]
[0,1,1,2,3,5,8,13,21,34,55]
[0,1,1,2,3,5,8,13,21,34,55,89]
[0,1,1,2,3,5,8,13,21,34,55,89,144]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584]
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181]
最後の結果だけを出力したいのなら、whileをuntilに変えます。条件式が満たされたときに.を出力して終了します。
($loop | tonumber) as $loop |
[0, 1] |
until( # 変更点はここと
length >= $loop; # この条件式だけ
(
length as $len |
. + [.[$len-1] + .[$len-2]]
)
)
実行例を示します。
$ jq --arg loop 20 -cnf fibo.jq
[0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181]
素数
ある数が素数かは、その数までの素数すべてで割ってみて、どれでも割り切れないか否かをチェックすることで判定できます。つまり、コードは「ある数」を順次増やしていくループと、「これまでの素数」の配列要素で割っていくループの2重ループで構成されます。
ここでは、チェック対象となる数のループにはforループと等価なforeachを、素数の配列要素で試し割をするループには配列を一気に処理する述語関数のmapを使います。mapはPythonやJavaScriptのものと機能は同じです。
他にも、
xが素数かはx1/2以下の素数まで確認すれば効率が良い(たとえば11が素数かは2と3で割るだけでよく、5はチェックする必要はない)など高速化のテクニックがありますが、ここではそこまでは追及しません。だって、jqなんですもの。
フィルタは次の通りです。
($max | tonumber) as $max |
[2] |
foreach range(3; $max) as $x
(
.;
. as $p |
map($x % . == 0) |
if contains([true]) then
[]
else
[$x]
end |
$p + .;
.
)
2重ループが絡んできているだけあって、フィボナッチと比べるとややこしくなっています。また、パイプラインで次の処理に渡される.の中身がその都度変化するので、追うのにも苦労します。具体的には、次のように変遷します。
- 素数を収容した配列。2行目で定義され、5~7行目の
.で参照されているのがこれ。ループしてコード先頭に戻った時は、最後の13行目のものが用いられます。 -
mapが生成する、素数配列と同じ要素数のtrue/falseからなる配列(7行目)。この.は8行目のcontainsに暗黙的に引き渡されます。 - 素数配列に追加される配列。
$xが素数かに応じて[]または[$x]が9行目あるいは11行目で定義されます。この.は13行目で用いられます。 - 素数を収容した配列。最初のものを退避させた
$pから、13行目で新規に生成されます。14行目がこれです。
行順に説明します。
- コマンドライン引数取り込みの要領は
fibo.jqと同じです。 - 素数配列は
[2]からスタートします。以下、順次、この配列に素数を加えていきます。 - 素数か否かのチェック対象になる数値を生成する
foreachループです。foreach 入力値 as 変数で構成されています。入力値は反復可能な値ならなんでも構いません。ここではrange関数から3以上$max未満の整数の配列を生成しています。これら値は、ループを回る毎に変数$xに収容されます。 -
foreachループには3つの制御要素が含まれます。初期化値、更新式、抽出式です。 - ループの初期値は、入力された配列(2行目)を参照する
.です(だから、最初は[2])。 - ここで、現在の素数配列(
.)を一時的に変数$pに退避させます。以下の計算で.をオーバーライトしてしまうからです。 - 現在の数値
$xに対し、.の要素でモジュロ演算(%)を施します。演算結果は== 0で比較することでtrue(割り切れる)、false(割り切れない)に変換します。これらを現在の配列すべての素数で実行するには、述語関数のmapを用います。これにより、たとえば、$x=10に対し、ここまでの素数配列[2, 3, 5, 7]でモジュロ演算を行うと、[true, false, true, false]が得られます。以下、次のステップの.はこの真偽配列を参照します。 - 上記の結果に1つでも
trueが含まれていたら、$xは素数ではありません。これはcontains関数からチェックできます。引数で指定された要素が含まれていれば、関数はtrueを返します。なお、.(真偽配列)が配列なので引数も配列でなければなりません(true単体ではなく[true]配列)。 - 素数でないならば、カラ配列
[]を用意します。 -
containsがfalseなら、$xは素数です。 - 素数ならば、
$xを収容した配列[$x]を用意します。 -
jqのif文は、末尾にif文の末尾に終了を明示するendが必要です(bashのif-fiに似た感じです)。 - この段階で、
.は[]または[$x]を参照しています。始めに素数を収容していた配列は$pに退避されています。そこで、この$pに新たに素数と判定された値.を加えることで、素数配列を更新します。これで、foreachの更新式は終わりです。;で終了を示します。 -
foreachのだ3番目の要素の抽出式に記述された値が、ループを巡るたびに出力されます。ここで指定されている.は$p + .で更新された素数配列です。
長かったですね。では、実行例を示します。
$ jq --arg max 60 -cnf prime.jq
[2,3] # $x=3。3が加わる。
[2,3] # $x=4。4は2で割り切れるので加えられない。
[2,3,5] # $x=5。追加。
[2,3,5] # $x=6。素数ではない。
⁞
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53] # $x=57。3で割れる。
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53] # $x=58。2で割れる。
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59] # $x=59。素数。
もっと目に優しいコーディングがありそうですが、今日はここまでにしておきます。
おわりに
ええ、実用性はゼロです。jqを拗らせて「限界など知らない」と頑張っただけです(が、確かに、意味はねぇなぁ...)。でも、ここまで書ければ、レベル4くらいには達したと思います。
参考
-
./jq -
jqのオフィシャルサイトです(英文)。 -
Qiita jq tag - Qiita掲載の
jq関連の記事一覧です。 -
Hello World あたたたた 24日目 jq編 -
jqの言語仕様をていねいに説明してくれます。 -
jqマニュアル(開発バージョン) - Yuji Okazawa氏が(非公式に)訳しておられる
jqマニュアルの日本語版です。和訳マニュアルは他にもあるので、検索してください。 - The JavaScript Object Notation (JSON) Data Interchange Format(RFC 8259) - JSONの仕様です(英文)。しばしばRFC 7159が参照されますが、そちらはobsoleteになりました。
- JSON(JavaScript Object Notation)データ交換フォーマット - 上記の非公式の和訳です。
- jqハンドブックーNetOps/DevOps必携のJSONパーザ - このページに掲載したフィボナッチ数のコードは、この本の10.5節で紹介したものです。ご購入はこちらから: 【出版社 | honto | amazon.co.jp | ヨドバシカメラ】。
