SQL で数式を評価 (不完全版)
そういえば(このパターン多いな)、redditだかstackoverflowで、いろんな言語で文字列として与えられた数式をできるだけ短いコードで評価するってのがあったけど、SQLでできる?w
Twitter / finalfusion: そういえば(このパターン多いな)、redditだかstack ...
あったあった。これだ Code Golf: Evaluating Mathematical Expressions - Stack Overflow http://tinyurl.com/kreb75
Twitter / finalfusion: あったあった。これだ ...
1 + 3 / -8 = -0.5 (No BODMAS) 2*3*4*5+99 = 219 4 * (9 - 4) / (2 * 6 - 2) + 8 = 10 1 + ((123 * 3 - 69) / 100) = 4 2.45/8.5*9.27+(5*0.0023) = 2.68...API Only - Stack Exchange
これはもう組むしかないでしょう。
ただ、このままではつらいので、とりあえずは以下の制限を付けときます。
なので「不完全版」です。
追記:
完全版できました!
WITH Input(id, str) AS ( -- idと数式を渡す SELECT 1, '1 + 3 / -8' UNION ALL SELECT 2, '2 * 3 * 4 * 5 + 99' UNION ALL SELECT 3, '4 * ( 9 - 4 ) / ( 2 * 6 - 2 ) + 8' UNION ALL SELECT 4, '1 + ( 123 * 3 - 69 / 100 )' UNION ALL SELECT 5, '2.45 / 8.5 * 9.27 + ( 5 * 0.0023 )' ) , Input_(id, str) AS ( -- 番兵の追加 SELECT id, str + ' ' FROM Input ) , ExprElements(id, i, ele, proc_len, str) AS ( -- 数式を要素に分割する SELECT id , 1 -- 要素に連番を振る , RTRIM( SUBSTRING(str, 1, CHARINDEX(' ', str, 1)) ) , LEN( SUBSTRING(str, 1, CHARINDEX(' ', str, 1)) ) + 1 -- スペース分も処理したとみなして、+ 1 , str FROM Input_ UNION ALL SELECT id , i + 1 , RTRIM( SUBSTRING( str, proc_len + 1, CHARINDEX(' ', str, proc_len + 1) - proc_len - 1 ) ) , proc_len + 1 -- スペース分も処理したとみなして、+ 1 + LEN( SUBSTRING( str, proc_len + 1, CHARINDEX(' ', str, proc_len + 1) - proc_len - 1 ) ) , str FROM ExprElements WHERE proc_len < LEN(str) ) , ExprCount(id, val) AS ( -- ExprElementsのidごとの個数(再帰SQL内でGROUP BYやCOUNTなどは使えないらしい) SELECT id, COUNT(*) FROM ExprElements GROUP BY id ) , Calc(id, result, op, crnt, pre_paren_res, pre_paren_op) AS ( -- 計算部分 SELECT id , CAST(ele AS real) -- 型を指定しておく , CAST(NULL AS char(1)) -- こちらも , i + 1 -- 処理する要素を表すカウンタ , CAST(NULL AS real) , CAST(NULL AS char(1)) FROM ExprElements WHERE -- 最初は要素に割り振った連番が1のもの、 -- つまり一番左の要素を対象にする i = 1 UNION ALL SELECT Calc.id , CASE -- eleが演算子なら途中結果をそのままにする WHEN ele IN('+', '-', '*', '/') THEN result -- 開き括弧ならリセット(別の場所に保管し、ここはリセット) WHEN ele = '(' THEN NULL -- 閉じ括弧なら保管した情報を巻き戻す WHEN ele = ')' THEN CASE pre_paren_op WHEN '+' THEN pre_paren_res + result WHEN '-' THEN pre_paren_res - result WHEN '*' THEN pre_paren_res * result WHEN '/' THEN pre_paren_res / result END -- opに演算子がたまっているなら -- 途中結果とeleを演算 WHEN op = '+' THEN result + ele WHEN op = '-' THEN result - ele WHEN op = '*' THEN result * ele WHEN op = '/' THEN result / ele -- 開き括弧の次はopがNULLなので、 -- eleをそのままためる ELSE ele END , CASE -- eleが演算子ならopにため、 -- そうでないならクリア WHEN ele IN('+', '-', '*', '/') THEN CAST(ele AS char(1)) ELSE NULL END , crnt + 1 , CASE ele -- 開き括弧ならresultを保存 WHEN '(' THEN result -- 閉じ括弧ならリセット、そうでない場合そのまま WHEN ')' THEN NULL ELSE pre_paren_res END , CASE ele -- 開き括弧ならopを保存 WHEN '(' THEN op -- 閉じ括弧ならリセット、そうでない場合そのまま WHEN ')' THEN NULL ELSE pre_paren_op END FROM Calc -- idごとに計算をするために、idでJOIN INNER JOIN ExprElements ON Calc.id = ExprElements.id WHERE -- 一回の再帰で同一idの一つの計算を行う i = crnt -- 再帰の終了条件はこっち -- 全ての要素を処理し終わったら終了 AND i <= (SELECT val FROM ExprCount WHERE id = Calc.id) ) , Result(id, result) AS ( -- 計算途中を除外して、計算結果のみにする SELECT id , result FROM Calc P WHERE crnt = (SELECT MAX(crnt) FROM Calc C WHERE P.id = C.id) ) -- 結果を表示 SELECT id , result FROM Result ORDER BY id
実行結果はこんな感じ。
id | result |
---|---|
1 | -0.5 |
2 | 219 |
3 | 10 |
4 | 4 |
5 | 2.683441 |
SQL はプログラミング言語です。
SQL はプログラミング言語です!
・・・あ、いや、これ本来の SQL 的な考え方してないですけどね。
「こんなことも出来るよ」って例であって、SQL をちょっとでも見直してくれるとうれしいです。
追記:
スペースのいらない ExprElements 作ったので、よかったらどうぞ。
, ExprElementsSrc(id, i, tmp, ele, pre_ch, input_str) AS ( -- 数式を要素に分割する SELECT id , 1 -- 要素に連番を振る , CAST(LEFT(str, 1) AS varchar(max)) , CAST('' AS varchar(max)) , CAST(' ' AS char(1)) , SUBSTRING(str, 2, LEN(str)) FROM Input UNION ALL SELECT id , CASE ele WHEN '' THEN i ELSE i + 1 END , CAST( CASE WHEN LEFT(input_str, 1) = ' ' THEN '' WHEN tmp = '-' THEN CASE WHEN pre_ch LIKE '[-+*/()]' THEN tmp + LEFT(input_str, 1) ELSE LEFT(input_str, 1) END WHEN LEFT(input_str, 1) IN ('+', '-', '*', '/', '(', ')') OR tmp IN ('+', '*', '/', '(', ')') THEN LEFT(input_str, 1) ELSE tmp + LEFT(input_str, 1) END AS varchar(max)) , CAST( CASE WHEN LEFT(input_str, 1) = ' ' THEN tmp WHEN LEFT(input_str, 1) = '-' THEN CASE WHEN tmp IN ('+', '-', '*', '/', '(', ')') THEN tmp ELSE '' END WHEN LEFT(input_str, 1) IN ('+', '*', '/', '(', ')') OR tmp IN ('+', '-', '*', '/', '(', ')') THEN CASE WHEN tmp = '-' AND pre_ch LIKE '[-+*/()]' THEN '' ELSE tmp END ELSE '' END AS varchar(max)) , CAST(LEFT(ele, 1) AS char(1)) , SUBSTRING(input_str, 2, LEN(input_str)) FROM ExprElementsSrc WHERE input_str <> '' OR tmp <> '' ) , ExprElements(id, i, ele) AS ( SELECT id , i , ele FROM ExprElementsSrc WHERE ele <> '' )