四則演算と剰余のみのexprコマンドをKotlinで作ってみた

Kotlin に入門しています。

まずは何か適当なものを作りながら慣れようということで、四則演算と剰余のみのexprコマンドをRubyで作ってみた を移植してみました。手書きの再帰下降パーサです。

# (100 - 2 - 1) / (1 + 2) % 5 * 3
# => 97 / 3 % 5 * 3
# => 32 % 5 * 3
# => 2 * 3
# => 6

## 実行の例
$ kotlin MyExpr.kts -- \( 100 - 2 - 1 \) / \( 1 + 2 \) % 5 \* 3
6

## 確認のため同じ引数で expr コマンドを実行
$ expr \( 100 - 2 - 1 \) / \( 1 + 2 \) % 5 \* 3
6
// MyExpr.kts

enum class Op(val symbol: String) {
    ADD("+"), SUB("-"),
    MUL("*"), DIV("/"), MOD("%")
}

abstract class Node () {
    abstract fun eval(): Int
}

class NumberNode (val n: Int) : Node() {
    override fun eval(): Int = this.n
}

class BinopNode (
    val op: Op,
    val left: Node,
    val right: Node
) : Node() {
    override fun eval(): Int {
        return (
            when (this.op) {
                Op.ADD -> this.left.eval() + this.right.eval()
                Op.SUB -> this.left.eval() - this.right.eval()
                Op.MUL -> this.left.eval() * this.right.eval()
                Op.DIV -> this.left.eval() / this.right.eval()
                Op.MOD -> this.left.eval() % this.right.eval()
            }
        )
    }
}

class Parser (val tokens: List<String>) {
    final val NUMERIC_CHARS = setOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')

    var cur = 0

    // --------------------------------

    class ParseException(msg: String) : RuntimeException(msg)

    fun currentToken(): String = this.tokens.get(this.cur)

    fun isAdditive(): Boolean {
        if (this.tokens.size <= this.cur) {
            // end of tokens
            return false
        }
        return setOf("+", "-").contains(currentToken())
    }

    fun isMultiply(): Boolean {
        if (this.tokens.size <= this.cur) {
            // end of tokens
            return false
        }
        return setOf("*", "/", "%").contains(currentToken())
    }

    fun consume(token: String, exception: Boolean = false): Boolean {
        if (currentToken() == token) {
            this.cur += 1
            return true
        } else {
            if (exception) {
                throw  ParseException("expected <${token}> / got <${currentToken()}>")
            }
            return false
        }
    }

    // --------------------------------

    fun parse(): Node = parseExpression()

    fun parseExpression(): Node = parseAdditive()

    fun parseAdditive(): Node {
        var node = parseMultiply()

        while (isAdditive()) {
            val (op, multiply) = parseAdditiveTail()
            node = BinopNode(op, node, multiply)
        }

        return node
    }

    fun parseAdditiveTail(): Pair<Op, Node> {
        val op =
            when {
                consume("+") -> Op.ADD
                consume("-") -> Op.SUB
                else -> {
                    throw ParseException("expected '+' or '-' / got <${currentToken()}>")
                }
            }

        return Pair(op, parseMultiply())
    }

    fun parseMultiply(): Node {
        var node = parseFactor()

        while (isMultiply()) {
            val (op, factor) = parseMultiplyTail()
            node = BinopNode(op, node, factor)
        }

        return node
    }

    fun parseMultiplyTail(): Pair<Op, Node> {
        val op =
            when {
                consume("*") -> Op.MUL
                consume("/") -> Op.DIV
                consume("%") -> Op.MOD
                else -> {
                    throw ParseException("expected '*', '/' or '%' / got <${currentToken()}>")
                }
            }

        return Pair(op, parseFactor())
    }

    fun parseFactor(): Node {
        if (consume("(")) {
            val exp = parseExpression()
            consume(")", true)
            return exp
        } else {
            return parseNumber()
        }
    }

    fun parseNumber(): NumberNode {
        val token = currentToken()
        this.cur += 1
        if (isNumber(token)) {
            return NumberNode(
                Integer.valueOf(token)
            )
        } else {
            throw ParseException("invalid number (${token})")
        }
    }

    fun isNumber(token: String): Boolean {
        val firstIndex =
            if (token.get(0) == '-') {
                1
            } else {
                0
            }

        for (i in firstIndex .. (token.length - 1)) {
            val c = token.get(i)
            if (! NUMERIC_CHARS.contains(c)) {
                return false
            }
        }

        return true
    }
}

// --------------------------------

val tokens = args.toList()
val tree = Parser(tokens).parse()
val result = tree.eval()
println(result)

関連