saba1024のブログ

どうでも良い思いついた事とかプログラミング関係のメモとか書いていきます。

Groovyで簡単なS式をパースするサンプル

無駄に複雑なコードになってしまっていますが、一応四則演算は実行できるS式のパーサが出来ました。
間違いなくもっとスマートな方法があるはずですが(普通にググれば10行程度でRubyで実装している方もいらっやいます)、今まで基本的にWebプログラミングしかして来なかったのでこういったパーサ系なんかも全部ライブラリ任せにしてしまってたので、まずは自分のための勉強ということで。。。
これを機会にこういったまた普段とは違ったコードも書いていきたいなと思います。

trait Parser {

    Closure findLastList = {l, next ->
        if (next <= 0 || l == [] || !(l instanceof List)) {
            l
        } else {
            call(l.last(), next-1)
        }
    }

    Closure normalizeSpaces = {String str ->
        str = str.replaceAll(/\(/, ' ( ')
        str = str.replaceAll(/\)/, ' ) ')
        // 先頭か末尾にスペースがある場合は1個に正規化されるけど必要ないのでtim()
        str.replaceAll(/\s{1,}/, ' ' ).trim()
    }
    
    Closure parseToList = {List<String> tokens ->
        List list = []
        Integer nest = 0
        tokens.each {
            if (it == '(') {
                if (nest > 1) {
                    findLastList(list, nest-1) << []
                } else {
                    list << []
                }
            nest++
            } else if (it == ')') {
                nest--
            } else {
                if (nest > 1) {
                    findLastList(list, nest-1) << it
                } else {
                    list << it
                }
            }
        }
        list.tail()
    }
    
    List<String> parse(String sourceCode) {
        String normalized = normalizeSpaces(sourceCode)
        List<String>tokens = normalized.tokenize(' ')
        parseToList(tokens)
    }
}

class MyLanguage implements Parser {

    Map operations = [
        '+': {a, b -> (a as Integer) + (b as Integer)},
        '-': {a, b -> (a as Integer) - (b as Integer)},
        '*': {a, b -> (a as Integer) * (b as Integer)},
        '/': {a, b -> (a as Integer) / (b as Integer)},
    ]
    
    Closure execute = {List code -> 
    
        String operation = code.head()
        List<String> operands = code.tail()

        def o = []
        for (obj in operands) {
            o << ( (obj instanceof List) ? call(obj) : obj )
        }
        o.inject(operations[operation])
    }

    def eval(String sourceCode) {
        List<String> parsedAsList = parse(sourceCode)
        execute(parsedAsList)
    }
}

// 以下のように、ネストしたS式でもちゃんと四則演算ができている。
def engine = new MyLanguage()
assert engine.eval('( + 1 2 3 ( - 1 2 ) )') == 5
assert engine.eval('(* 3 5)') == 15
assert engine.eval('(/ 20 2 5)') == 2
assert engine.eval('(* 2 (/ 10 5))') == 4
assert engine.eval('(+ (* 2 4) 5)') == 13
assert engine.eval('(- (+ (* 2 4) 5 (/ 6 2)) 10)') == 6