3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

jqのむだづかいーフィボナッチ数・素数生成篇

3
Last updated at Posted at 2022-06-18

JSON処理のコマンドラインツールであるjqは、JSONテキストを1行でちゃくっと解析、変換するときに使うものです。複雑な分岐や制御が絡んできたら、sedawkなど他の文字列処理ユーティリティと組み合わせて何とかするのが通例です。単体で複雑なことをしたりはしません。

とは言え、jqにも変数定義、forifなどの制御構造、関数定義などプログラミング言語らしき機能が備わっているので、「やればできんじゃねぇ」と野望を募らせてしまうこともあります。

というわけで、ここではフィボナッチ数と素数の生成に挑戦します。

コード

コード(フィルタファイル)はfibo.jqprime.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]]
	)
)
  1. --argコマンドラインオプションで指定した変数loopは、フィルタ内では$loopで参照できます。コマンドライン引数は文字列なので、tonumberで数値に変換して$loopに入れ直します。なお、変数定義の... as $x文は、jqのパイプライン処理(|)には影響を与えません。データの流れという観点では無視して構いません。
  2. フィボナッチ数の最初の2要素を配列で定義します。以降、この配列(.で参照)に要素を追加していきます。
  3. ループです。whileには条件式と更新式という、2つの制御要素が含まれます。
  4. ループは条件式がtrueの間は回り続けます。ここでは配列(.)の長さをlengthから調べ、それが変数$loopより大きくなったら終了します。
  5. 更新式では、入力配列を変更します。わかりやすいように括弧()でまとめています(なくても問題ありませんが)。
  6. 更新式ではまず、現在の配列長を変数$lenに代入します。これは、配列中の要素を参照するときに用います。
  7. 続いて、現在の配列.に末尾の要素(.[$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]

最後の結果だけを出力したいのなら、whileuntilに変えます。条件式が満たされたときに.を出力して終了します。

($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行目がこれです。

行順に説明します。

  1. コマンドライン引数取り込みの要領はfibo.jqと同じです。
  2. 素数配列は[2]からスタートします。以下、順次、この配列に素数を加えていきます。
  3. 素数か否かのチェック対象になる数値を生成するforeachループです。foreach 入力値 as 変数で構成されています。入力値は反復可能な値ならなんでも構いません。ここではrange関数から3以上$max未満の整数の配列を生成しています。これら値は、ループを回る毎に変数$xに収容されます。
  4. foreachループには3つの制御要素が含まれます。初期化値、更新式、抽出式です。
  5. ループの初期値は、入力された配列(2行目)を参照する.です(だから、最初は[2])。
  6. ここで、現在の素数配列(.)を一時的に変数$pに退避させます。以下の計算で.をオーバーライトしてしまうからです。
  7. 現在の数値$xに対し、.の要素でモジュロ演算(%)を施します。演算結果は== 0で比較することでtrue(割り切れる)、false(割り切れない)に変換します。これらを現在の配列すべての素数で実行するには、述語関数のmapを用います。これにより、たとえば、$x=10に対し、ここまでの素数配列[2, 3, 5, 7]でモジュロ演算を行うと、[true, false, true, false]が得られます。以下、次のステップの.はこの真偽配列を参照します。
  8. 上記の結果に1つでもtrueが含まれていたら、$xは素数ではありません。これはcontains関数からチェックできます。引数で指定された要素が含まれていれば、関数はtrueを返します。なお、.(真偽配列)が配列なので引数も配列でなければなりません(true単体ではなく[true]配列)。
  9. 素数でないならば、カラ配列[]を用意します。
  10. containsfalseなら、$xは素数です。
  11. 素数ならば、$xを収容した配列[$x]を用意します。
  12. jqif文は、末尾にif文の末尾に終了を明示するendが必要です(bashのif-fiに似た感じです)。
  13. この段階で、.[]または[$x]を参照しています。始めに素数を収容していた配列は$pに退避されています。そこで、この$pに新たに素数と判定された値.を加えることで、素数配列を更新します。これで、foreachの更新式は終わりです。;で終了を示します。
  14. 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くらいには達したと思います。

参考

image.png

3
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?