SQL で逆ポーランド記法の電卓
まだ未完成ですけど、もうほとんど完成してるのでさらしておきますね。
完成しました!
文字列でスタックを実現しています。
WITH Input(id, str) AS ( -- 1 + 3 / -8 SELECT 1, '1 3 + -8 /' -- 2*3*4*5+99 UNION ALL SELECT 2, '2 3 * 4 * 5 * 99 +' -- 4 * (9 - 4) / (2 * 6 - 2) + 8 UNION ALL SELECT 3, '9 4 - 4 * 2 6 * 2 - / 8 +' -- 1 + ((123 * 3 - 69) / 100) UNION ALL SELECT 4, '1 123 3 * 69 - 100 / +' -- 2.45/8.5*9.27+(5*0.0023) UNION ALL SELECT 5, '2.45 8.5 / 9.27 * 5 0.0023 * +' ) , Input_(id, str) AS ( SELECT id, str + ' ' FROM Input ) , Calc(id, stack, input_str) AS ( -- スタックの初期状態 SELECT id , CAST(' ' + RTRIM( LEFT(str, CHARINDEX(' ', str, 1)) -- 最初の要素をスタックに積む ) AS varchar(max)) , SUBSTRING(str, CHARINDEX(' ', str, 1) + 1, LEN(str)) -- 入力から消費した要素を取り除く FROM Input_ UNION ALL -- 再帰SQLを使って入力の先頭から一要素毎計算していく SELECT id -- 入力の先頭要素を元にスタックを更新 , CASE WHEN RTRIM( LEFT(input_str, CHARINDEX(' ', input_str, 1)) ) IN ('+', '-', '*', '/') -- 演算子ならスタックから2つの要素を取り出して、演算して、また戻す THEN ' ' + CASE LEFT( stack, LEN(stack) -- topの要素の長さ - LEN(REVERSE(LEFT(REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1)))) -- topの次の要素の長さ - LEN(REVERSE(SUBSTRING( REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1, CHARINDEX( ' ', REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1 ) - CHARINDEX(' ', REVERSE(stack), 1) - 1 ))) ) WHEN '' THEN '' -- 2つの要素を取り出した後のスタックが空じゃないならスタックの内容と空白を一つスタックに戻す ELSE RTRIM(LTRIM(LEFT( stack, LEN(stack) - LEN(REVERSE(LEFT(REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1)))) - LEN(REVERSE(SUBSTRING( REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1, CHARINDEX( ' ', REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1 ) - CHARINDEX(' ', REVERSE(stack), 1) - 1 ))) ))) + ' ' END + CAST( -- topから2番目の要素とtopの要素を取り出して演算後、スタックに戻す CASE RTRIM( LEFT(input_str, CHARINDEX(' ', input_str, 1)) ) WHEN '+' THEN CAST(REVERSE(SUBSTRING( REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1, CHARINDEX( ' ', REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1 ) - CHARINDEX(' ', REVERSE(stack), 1) - 1 )) AS real) + CAST(REVERSE(LEFT(REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1))) AS real) WHEN '-' THEN CAST(REVERSE(SUBSTRING( REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1, CHARINDEX( ' ', REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1 ) - CHARINDEX(' ', REVERSE(stack), 1) - 1 )) AS real) - CAST(REVERSE(LEFT(REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1))) AS real) WHEN '*' THEN CAST(REVERSE(SUBSTRING( REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1, CHARINDEX( ' ', REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1 ) - CHARINDEX(' ', REVERSE(stack), 1) - 1 )) AS real) * CAST(REVERSE(LEFT(REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1))) AS real) WHEN '/' THEN CAST(REVERSE(SUBSTRING( REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1, CHARINDEX( ' ', REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1) + 1 ) - CHARINDEX(' ', REVERSE(stack), 1) - 1 )) AS real) / CAST(REVERSE(LEFT(REVERSE(stack), CHARINDEX(' ', REVERSE(stack), 1))) AS real) END AS varchar(max)) ELSE stack + ' ' + RTRIM( SUBSTRING(input_str, 1, CHARINDEX(' ', input_str, 1)) ) END , SUBSTRING(input_str, CHARINDEX(' ', input_str, 1) + 1, LEN(input_str)) FROM Calc WHERE input_str <> '' ) , Result(id, val) AS ( SELECT id , stack FROM Calc WHERE input_str = '' ) SELECT id , val FROM Result ORDER BY id
実行結果
id | val |
---|---|
1 | -0.5 |
2 | 219 |
3 | 10 |
4 | 4 |
5 | 2.68344 |
最後の SELECT を、
SELECT * FROM Calc ORDER BY id, LEN(input_str) DESC
に置き換えると計算の途中結果を確認することができる。
id | stack | input_str |
---|---|---|
1 | 1 | 3 + -8 / |
1 | 1 3 | + -8 / |
1 | 4 | -8 / |
1 | 4 -8 | / |
1 | -0.5 | |
2 | 2 | 3 * 4 * 5 * 99 + |
2 | 2 3 | * 4 * 5 * 99 + |
2 | 6 | 4 * 5 * 99 + |
2 | 6 4 | * 5 * 99 + |
2 | 24 | 5 * 99 + |
2 | 24 5 | * 99 + |
2 | 120 | 99 + |
2 | 120 99 | + |
2 | 219 | |
3 | 9 | 4 - 4 * 2 6 * 2 - / 8 + |
3 | 9 4 | - 4 * 2 6 * 2 - / 8 + |
3 | 5 | 4 * 2 6 * 2 - / 8 + |
3 | 5 4 | * 2 6 * 2 - / 8 + |
3 | 20 | 2 6 * 2 - / 8 + |
3 | 20 2 | 6 * 2 - / 8 + |
3 | 20 2 6 | * 2 - / 8 + |
3 | 20 12 | 2 - / 8 + |
3 | 20 12 2 | - / 8 + |
3 | 20 10 | / 8 + |
3 | 2 | 8 + |
3 | 2 8 | + |
3 | 10 | |
4 | 1 | 123 3 * 69 - 100 / + |
4 | 1 123 | 3 * 69 - 100 / + |
4 | 1 123 3 | * 69 - 100 / + |
4 | 1 369 | 69 - 100 / + |
4 | 1 369 69 | - 100 / + |
4 | 1 300 | 100 / + |
4 | 1 300 100 | / + |
4 | 1 3 | + |
4 | 4 | |
5 | 2.45 | 8.5 / 9.27 * 5 0.0023 * + |
5 | 2.45 8.5 | / 9.27 * 5 0.0023 * + |
5 | 0.288235 | 9.27 * 5 0.0023 * + |
5 | 0.288235 9.27 | * 5 0.0023 * + |
5 | 2.67194 | 5 0.0023 * + |
5 | 2.67194 5 | 0.0023 * + |
5 | 2.67194 5 0.0023 | * + |
5 | 2.67194 0.0115 | + |
5 | 2.68344 |
こんな入力はどうでしょ?
-- 1 + (2 + (3 + (4 + (5 + (6 + (7 + (8 + (9 + 10)))))))) SELECT 1, '1 2 3 4 5 6 7 8 9 10 + + + + + + + + +'
途中結果を確認すると、
id | stack | input_str |
---|---|---|
1 | 1 | 2 3 4 5 6 7 8 9 10 + + + + + + + + + |
1 | 1 2 | 3 4 5 6 7 8 9 10 + + + + + + + + + |
1 | 1 2 3 | 4 5 6 7 8 9 10 + + + + + + + + + |
1 | 1 2 3 4 | 5 6 7 8 9 10 + + + + + + + + + |
1 | 1 2 3 4 5 | 6 7 8 9 10 + + + + + + + + + |
1 | 1 2 3 4 5 6 | 7 8 9 10 + + + + + + + + + |
1 | 1 2 3 4 5 6 7 | 8 9 10 + + + + + + + + + |
1 | 1 2 3 4 5 6 7 8 | 9 10 + + + + + + + + + |
1 | 1 2 3 4 5 6 7 8 9 | 10 + + + + + + + + + |
1 | 1 2 3 4 5 6 7 8 9 10 | + + + + + + + + + |
1 | 1 2 3 4 5 6 7 8 19 | + + + + + + + + |
1 | 1 2 3 4 5 6 7 27 | + + + + + + + |
1 | 1 2 3 4 5 6 34 | + + + + + + |
1 | 1 2 3 4 5 40 | + + + + + |
1 | 1 2 3 4 45 | + + + + |
1 | 1 2 3 49 | + + + |
1 | 1 2 52 | + + |
1 | 1 54 | + |
1 | 55 |
楽しい!
追記:
最初の実装以降に身につけたテクニックを使って書き直した。
短く読みやすくなってる・・・はず!
WITH Input(id, str) AS ( -- 1 + 3 / -8 SELECT 1, '1 3 + -8 /' -- 2*3*4*5+99 UNION ALL SELECT 2, '2 3 * 4 * 5 * 99 +' -- 4 * (9 - 4) / (2 * 6 - 2) + 8 UNION ALL SELECT 3, '9 4 - 4 * 2 6 * 2 - / 8 +' -- 1 + ((123 * 3 - 69) / 100) UNION ALL SELECT 4, '1 123 3 * 69 - 100 / +' -- 2.45/8.5*9.27+(5*0.0023) UNION ALL SELECT 5, '2.45 8.5 / 9.27 * 5 0.0023 * +' ) , Input_(id, str) AS ( SELECT id, str + ' ' FROM Input ) , Calc(id, stack, top1, top2, input_str) AS ( -- スタックの初期状態 SELECT id , CAST( LEFT(str, CHARINDEX(' ', str)) -- 最初の要素をスタックに積む AS varchar(max)) , CAST(LEFT(str, CHARINDEX(' ', str) - 1) AS real) -- スタックに積んだものと同じもの , CAST(NULL AS real) -- 最初は使わないのでなんでもいい , SUBSTRING(str, CHARINDEX(' ', str) + 1, LEN(str)) -- 入力から消費した要素を取り除く FROM Input_ UNION ALL -- 再帰SQLを使って入力の先頭から一要素毎計算していく SELECT id -- 入力の先頭要素を元にスタックを更新 , CASE WHEN LEFT(input_str, 2) IN ('+', '-', '*', '/') THEN CAST( -- 演算結果に CASE LEFT(input_str, 1) WHEN '+' THEN top2 + top1 WHEN '-' THEN top2 - top1 WHEN '*' THEN top2 * top1 WHEN '/' THEN top2 / top1 END AS varchar(max)) -- スタックから先頭要素を2つ取り除いたものを連結したものが新しいスタック + CASE LEN(stack) - LEN(CAST(top1 AS varchar(max))) - LEN(CAST(top2 AS varchar(max))) - 1 WHEN 0 THEN ' ' ELSE SUBSTRING( stack, LEN(CAST(top1 AS varchar(max))) + LEN(CAST(top2 AS varchar(max))) + 2, len(stack) ) END ELSE LEFT(input_str, CHARINDEX(' ', input_str)) + stack END -- 次のtop1は入力が演算子なら演算結果、そうでなければ入力の先頭 , CAST(CASE LEFT(input_str, CHARINDEX(' ', input_str)) WHEN '+' THEN top2 + top1 WHEN '-' THEN top2 - top1 WHEN '*' THEN top2 * top1 WHEN '/' THEN top2 / top1 ELSE LEFT(input_str, CHARINDEX(' ', input_str)) END AS real) -- 次のtop2は入力が演算子ならスタックの3番目の要素、そうでなければtop1 , CAST(CASE WHEN LEFT(input_str, CHARINDEX(' ', input_str)) IN ('+', '-', '*', '/') AND CHARINDEX(' ', stack, CHARINDEX(' ', stack, CHARINDEX(' ', stack) + 1) + 1) <> 0 THEN SUBSTRING( stack, LEN(CAST(top1 AS varchar(max))) + LEN(CAST(top2 AS varchar(max))) + 2, CHARINDEX(' ', stack, CHARINDEX(' ', stack, CHARINDEX(' ', stack) + 1) + 1) - CHARINDEX(' ', stack, CHARINDEX(' ', stack) + 1) ) ELSE top1 END AS real) , SUBSTRING(input_str, CHARINDEX(' ', input_str) + 1, LEN(input_str)) FROM Calc WHERE input_str <> '' ) , Result(id, val) AS ( SELECT id , stack FROM Calc WHERE input_str = '' ) SELECT id , val FROM Result ORDER BY id
途中結果も載せときますね。
id | stack | top1 | top2 | input_str |
---|---|---|---|---|
1 | 1 | 1 | NULL | 3 + -8 / |
1 | 3 1 | 3 | 1 | + -8 / |
1 | 4 | 4 | 3 | -8 / |
1 | -8 4 | -8 | 4 | / |
1 | -0.5 | -0.5 | -8 | |
2 | 2 | 2 | NULL | 3 * 4 * 5 * 99 + |
2 | 3 2 | 3 | 2 | * 4 * 5 * 99 + |
2 | 6 | 6 | 3 | 4 * 5 * 99 + |
2 | 4 6 | 4 | 6 | * 5 * 99 + |
2 | 24 | 24 | 4 | 5 * 99 + |
2 | 5 24 | 5 | 24 | * 99 + |
2 | 120 | 120 | 5 | 99 + |
2 | 99 120 | 99 | 120 | + |
2 | 219 | 219 | 99 | |
3 | 9 | 9 | NULL | 4 - 4 * 2 6 * 2 - / 8 + |
3 | 4 9 | 4 | 9 | - 4 * 2 6 * 2 - / 8 + |
3 | 5 | 5 | 4 | 4 * 2 6 * 2 - / 8 + |
3 | 4 5 | 4 | 5 | * 2 6 * 2 - / 8 + |
3 | 20 | 20 | 4 | 2 6 * 2 - / 8 + |
3 | 2 20 | 2 | 20 | 6 * 2 - / 8 + |
3 | 6 2 20 | 6 | 2 | * 2 - / 8 + |
3 | 12 20 | 12 | 20 | 2 - / 8 + |
3 | 2 12 20 | 2 | 12 | - / 8 + |
3 | 10 20 | 10 | 20 | / 8 + |
3 | 2 | 2 | 10 | 8 + |
3 | 8 2 | 8 | 2 | + |
3 | 10 | 10 | 8 | |
4 | 1 | 1 | NULL | 123 3 * 69 - 100 / + |
4 | 123 1 | 123 | 1 | 3 * 69 - 100 / + |
4 | 3 123 1 | 3 | 123 | * 69 - 100 / + |
4 | 369 1 | 369 | 1 | 69 - 100 / + |
4 | 69 369 1 | 69 | 369 | - 100 / + |
4 | 300 1 | 300 | 1 | 100 / + |
4 | 100 300 1 | 100 | 300 | / + |
4 | 3 1 | 3 | 1 | + |
4 | 4 | 4 | 3 | |
5 | 2.45 | 2.45 | NULL | 8.5 / 9.27 * 5 0.0023 * + |
5 | 8.5 2.45 | 8.5 | 2.45 | / 9.27 * 5 0.0023 * + |
5 | 0.288235 | 0.2882353 | 8.5 | 9.27 * 5 0.0023 * + |
5 | 9.27 0.288235 | 9.27 | 0.2882353 | * 5 0.0023 * + |
5 | 2.67194 | 2.671942 | 9.27 | 5 0.0023 * + |
5 | 5 2.67194 | 5 | 2.671942 | 0.0023 * + |
5 | 0.0023 5 2.67194 | 0.0023 | 5 | * + |
5 | 0.0115 2.67194 | 0.0115 | 2.67194 | + |
5 | 2.68344 | 2.68344 | 0.0115 |
Brainf*ck in SQL
Brainf*ck はチューリング完全らしいですよ。
Brainf*ck 自体に興味のある方は、Brainf*ck や Brainfuck - Wikipedia へどうぞ。
WITH -- 入力 Input(id, bf_program, stdin) AS ( -- Hello, World!と標準出力に出力するプログラム SELECT 0, ' >+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++ ++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.[-]> ++++++++[<++++>-]<+.[-]++++++++++.', '' -- hogeと標準出力に出力するプログラム UNION ALL SELECT 1, ' ++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++ ++++++++. +++++++. --------. --.', '' -- hogeと標準出力に出力するプログラム(ループ版) UNION ALL SELECT 2, ' ++++++++++[>++++++++++<-]> ++++.+++++++.--------.--.', '' -- echo・・・だけど無限ループなのでコメントアウト --UNION ALL SELECT 3, '+[>,.<]', 'hoge' -- 1 + 2を計算し、標準出力に出力するプログラム UNION ALL SELECT 4, ' +>++><< [->>>+<<<] >>>[-<+<<+>>>]<< [->>+<<] >>[-<+<+>>]< ++++++++++++++++++++++++++++++++++++++++++++++++.', '' -- 4 * 2を計算し、標準出力に出力するプログラム UNION ALL SELECT 5, ' ++++>++><< [- >[->>+<<] >>[-<+<+>>] <<< ]>> ++++++++++++++++++++++++++++++++++++++++++++++++.', '' -- 条件分岐のサンプル UNION ALL SELECT 6, ' +++++ # {0} = ? >+++< # {1} = ? >[ # while( {1} ) do <[->>+>+<<<]> # {2:3} = !{0} >>[-<<<+>>>]<< # {0} = !{3} >[[-]<<->>]< # if({2}) then decr {0} - # decr {1} ]< # end ++++++++++++++++++++++++++++++++++++++++++++++++.', '' ) , BracketTableSrc(id, open_bracket_pos, close_bracket_pos, stack, src, crnt) AS ( -- 括弧の対応表の元データ -- スタックに開き括弧のインデックスをpushし、 -- 閉じカッコを見つけ次第そのインデックスとスタックに積まれているインデックスを取り出して、 -- open_bracket_posとclose_bracket_posに格納する。 -- 後戻りが発生しないので、文字列消費型(srcがどんどん減っていく)で構成。 SELECT id , 0 , 0 , CAST('' AS varchar(max)) , bf_program , 1 FROM Input UNION ALL SELECT id -- スタックのtopにあるインデックスを設定 , CASE LEFT(src, 1) WHEN ']' THEN CAST(LEFT(stack, CHARINDEX(' ', stack, 1)) AS int) ELSE 0 END -- 閉じカッコのインデックスを設定 , CASE LEFT(src, 1) WHEN ']' THEN crnt ELSE 0 END -- 開き括弧ならスタックにインデックスをpushし、 -- 閉じカッコならスタックからpopする , CASE LEFT(src, 1) WHEN '[' THEN CAST(crnt AS varchar(4)) + ' ' + stack WHEN ']' THEN SUBSTRING(stack, CHARINDEX(' ', stack, 1) + 1, LEN(stack)) ELSE stack END -- 消費した文字列を取り除いたものが次のsrcになる , SUBSTRING(src, 2, LEN(src)) , crnt + 1 FROM BracketTableSrc WHERE src <> '' ) , BracketTable(id, open_bracket_pos, close_bracket_pos) AS ( -- open_bracket_posとclose_bracket_posがどちらも格納されているデータのみ抜き出す -- このテーブルをひくことで対応括弧のインデックスが取得できる SELECT id , open_bracket_pos , close_bracket_pos FROM BracketTableSrc WHERE open_bracket_pos <> 0 AND close_bracket_pos <> 0 ) , Eval(id, array, ptr, input_pgm, crnt, stdout, stdin) AS ( -- Brainf*ckの評価エンジン -- ループが存在するので、文字列消費型ではなく、 -- オリジナル文字列とインデックスによる方法(input_pgmは変わらず、crntが変わる)を使用している。 SELECT id -- Brainf*ckが使用する配列は5000に制限 -- SQL Serverに限らず、一行のサイズが制限されていることは多いので一応。 -- 初期状態は全て(数値の)0 , CAST(REPLICATE(CHAR(0), 5000) AS char(5000)) , 1 -- ポインタの初期状態はSQLの文字列のインデックスが1からなので1 , bf_program , 1 , CAST('' AS nvarchar(max)) -- 標準出力は最初空 , stdin -- 標準出力はInputのものをそのまま使う FROM Input UNION ALL SELECT id , CAST( -- 現在見ている文字が+、-、,のいずれかなら配列を操作する CASE SUBSTRING(input_pgm, crnt, 1) WHEN '+' THEN STUFF(array, ptr, 1, CHAR(ASCII(SUBSTRING(array, ptr, 1)) + 1)) WHEN '-' THEN STUFF(array, ptr, 1, CHAR(ASCII(SUBSTRING(array, ptr, 1)) - 1)) -- 標準入力にも一応対応。標準入力を一番最初に与えておかなければいけないのはSQLの限界ですね WHEN ',' THEN STUFF(array, ptr, 1, LEFT(stdin, 1)) ELSE array END AS char(5000)) -- 現在見ている文字が>か<ならポインタを操作する , CASE SUBSTRING(input_pgm, crnt, 1) WHEN '>' THEN ptr + 1 WHEN '<' THEN ptr - 1 ELSE ptr END , input_pgm -- 現在見ている文字が[か]ならインデックスを操作する , CASE SUBSTRING(input_pgm, crnt, 1) WHEN '[' THEN CASE ASCII(SUBSTRING(array, ptr, 1)) WHEN 0 THEN (SELECT close_bracket_pos FROM BracketTable WHERE Eval.id = BracketTable.id AND open_bracket_pos = crnt) + 1 ELSE crnt + 1 END WHEN ']' THEN (SELECT open_bracket_pos FROM BracketTable WHERE Eval.id = BracketTable.id AND close_bracket_pos = crnt) ELSE crnt + 1 END -- 現在見ている文字が.なら標準出力に追記する , CASE SUBSTRING(input_pgm, crnt, 1) WHEN '.' THEN stdout + SUBSTRING(array, ptr, 1) ELSE stdout END -- 現在見ている文字が,なら標準入力から消費する , CASE SUBSTRING(input_pgm, crnt, 1) WHEN ',' THEN SUBSTRING(stdin, 2, LEN(stdin)) ELSE stdin END FROM Eval WHERE crnt <= LEN(input_pgm) ) , Result(id, stdout) AS ( SELECT id , stdout FROM Eval P WHERE crnt = (SELECT MAX(crnt) FROM Eval C WHERE P.id = C.id) ) SELECT * FROM Result ORDER BY id OPTION (MAXRECURSION 32767)
実装には以下の書籍を参考にしました。超感謝!
カッコのネストのロジックはそのままです。
Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~
- 作者: 原悠
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2008/12/20
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 147回
- この商品を含むブログ (72件) を見る
Brainf*ck のプログラムは、Brainf*ck をそのまま使わせていただきました。
他に特筆すべきは、スタックの構造を「文字列の先頭が top になるように」変更したところですかね。
気付いてしまえば大したものじゃないんですけど、文字列の末尾を top にしていた逆ポーランド記法の電卓では、REVERSE 関数使いまくりで訳分からんコードになっていました。
それがこちらでは超すっきり記述できています。
実行結果
id | stdout |
---|---|
0 | Hello World! |
1 | hoge |
2 | hoge |
4 | 3 |
5 | 8 |
6 | 2 |