読者です 読者をやめる 読者になる 読者になる

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 <> ''
  )