Antlrを基にした複雑な電卓の実装

人事労務システムを設計する際に、ユーザーからの要望で、設定した数式で計算する機能が必要とされて、このために、Antrl（パーサージェネレータツール）を使用し、IF ELSEなどの条件分岐を含む、より複雑な計算が可能な電卓機能を実装しました。
## Antrlって何
**ANTLR**（_ANother Tool for Language Recognition_）とは、[LL](https://ja.wikipedia.org/wiki/LL%E6%B3%95 "LL法")(*)構文解析に基づく[パーサジェネレータ](https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%BC%E3%82%B5%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AC%E3%83%BC%E3%82%BF "パーサジェネレータ")である（バージョン3.xはLL(*)、2.xまではLL(k)）[ Wikipedia ](https://ja.wikipedia.org/wiki/ANTLR)

## 手順
1. antrlの設定ファイル.g4ファイルを書きます
```
grammar Calculator;  //ファイルが`Calculator`という名前の文法を定義
@header {  
package com.kaopasu.calculator;  //Javaのパッケージ宣言
}  
prog:   stat+ ;  //`prog` は、一つ以上の `stat`（ステートメント）から成るプログラムを定義しています。
  
stat:   expr NEWLINE                # returnValue  
    |   VARIABLE '=' expr NEWLINE   # assign  
    |   NEWLINE                     # blank  
    ;  
// 式の定義
expr:    expr op='==' expr          # Equality // Put this first  
    |    expr op='>' expr          #Gt  
    |    expr op='<' expr          #Lt  
    |    expr op='!=' expr         #Ne  
    |    expr op='>=' expr         #Ge  
    |    expr op='<=' expr         #Le  
    |    expr op='&&' expr         # And  
    |    expr op='||' expr         # Or  
    |   'if' LPAREN expr COMMA expr COMMA expr RPAREN  # IfFunction  
    |   expr '?' expr ':' expr      # Ternary  
    |   expr op='^' expr            # Power  
    |   expr op=('*'|'/') expr      # MulDiv  
    |   expr op=('+'|'-') expr      # AddSub  
    |   op=('+'|'-') atom           # Signed  
    |   atom                        # Atoms  
    |   LOG'('expr',' expr')'       # Logarithm  
    |   LN'('expr')'                # NaturalLogarithm  
    |   SQRT'('expr')'              # SquareRoot  
    |   SIN'('expr')'          # Sine  
    |   ASIN'('expr')'         # ASine  
    |   COS'('expr')'          # Cosine  
    |   ACOS'('expr')'         # ACosine  
    |   TAN'('expr')'          # Tangent  
    |   ATAN'('expr')'              # ATangent  
    |   FLOOR'('expr')'             # Floor  
    |   CEIL'('expr')'              # Cell  
    |   ROUND'('expr')'             # Round  
    |   MAX'('expr',' expr')'       # Max  
 // | I     #ConstantI  
    ;  
  
atom  
    :   INT                         # Integer  
    |   DOUBLE                     # Double  
    |   PI                          # ConstantPI  
    |   EULER                       # ConstantE  
    |   SCIENTIFIC_NUMBER           # Scientific  
    |   VARIABLE                    # Variable  
    |   LPAREN expr RPAREN          # Braces  
    ;  
  
INT  : [0-9]+;  
  
DOUBLE : [0-9]+'.'[0-9]+;  
  
  
SCIENTIFIC_NUMBER  
   : INT+ ((E1 | E2) SIGN? NUMBER)  
   ;  
  
COS  
   : 'cos'  
   ;  
  
  
SIN  
   : 'sin'  
   ;  
  
  
TAN  
   : 'tan'  
   ;  
  
  
ACOS  
   : 'acos'  
   ;  
  
  
ASIN  
   : 'asin'  
   ;  
  
  
ATAN  
   : 'atan'  
   ;  
  
FLOOR  
   : 'floor'  
   ;  
  
CEIL  
   : 'ceil'  
   ;  
  
ROUND  
   : 'round'  
   ;  
  
MAX  
   : 'max'  
   ;  
  
LN  
   : 'ln'  
   ;  
  
  
LOG  
   : 'log'  
   ;  
  
  
SQRT  
   : 'sqrt'  
   ;  
  
IF  : 'if' ;  
  
  
LPAREN  
   : '('  
   ;  
  
  
RPAREN  
   : ')'  
   ;  
  
  
PLUS  
   : '+'  
   ;  
  
  
MINUS  
   : '-'  
   ;  
  
  
TIMES  
   : '*'  
   ;  
  
  
DIV  
   : '/'  
   ;  
  
  
GT  
   : '>'  
   ;  
  
  
LT  
   : '<'  
   ;  
  
  
EQ  
   : '=='  
   ;  
  
GE  
   : '>='  
   ;  
  
LE  
   : '<='  
   ;  
NE  
   : '!='  
   ;  
  
AND  
   : '&&'  
   ;  
  
OR  
   : '||'  
   ;  
  
  
  
COMMA  
   : ','  
   ;  
  
  
POINT  
   : '.'  
   ;  
  
  
POW  
   : '^'  
   ;  
  
  
PI  
   : 'pi'  
   ;  
  
  
EULER  
   : E2  
   ;  
  
  
I  
   : 'i'  
   ;  
  
  
VARIABLE  
   : VALID_ID_START VALID_ID_CHAR*  
   ;  
//変数の定義
fragment VALID_ID_START  
   : ('a' .. 'z') | ('A' .. 'Z') | '_' | '\u3040' .. '\u309F' | '\u30A0' .. '\u30FF' | '\u4E00' .. '\u9FFF'  
   ;  
  
fragment VALID_ID_CHAR  
   : VALID_ID_START | ('0' .. '9')  
   ;  
  
  
NUMBER  
   : ('0' .. '9') + ('.' ('0' .. '9') +)?  
   ;  
  
  
fragment E1  
   : 'E'  
   ;  
  
  
fragment E2  
   : 'e'  
   ;  
  
  
fragment SIGN  
   : ('+' | '-')  
   ;  
  
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)  
  
WS  
   : [ \r\n\t] + -> skip  
   ;
```

2.  g4ファイルに基づいて、Recognizer を生成
今回はIDEAのAntrl v4 プラグインを使い、Javaコードを生成![Pasted image 20240109163932.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2322984/09d27136-05df-b754-ed1f-baeb6ee0b63f.png)
]インストールしたら、Generate ANTRL Recognizerを押します
![uploading...0]()


3. 必要なpackageを導入
```Groovy
dependencies {  
    implementation 'org.antlr:antlr4-runtime:4.13.1'  
}
```
4.　VisitorでAntrlを実装
```kotlin
package com.kaopasu.calculator  
  
import com.kaopasu.calculator.CalculatorParser.*  
import java.math.BigDecimal  
import java.math.MathContext  
import java.math.RoundingMode  
import kotlin.math.*  
  
/***  
 * Notice：some functions have loss of precision,but as a salary calculator, it's enough  
 * * Excerpted from "The Definitive ANTLR 4 Reference", * published by The Pragmatic Bookshelf. * Copyrights apply to this code. It may not be used to create training material, * courses, books, articles, and the like. Contact us if you are in doubt. * We make no guarantees that this code is fit for any purpose. * Visit http://www.pragmaticprogrammer.com/titles/tpantlr2 for more book information. */class CalculatorEvalVisitorKotlin : CalculatorBaseVisitor<BigDecimal?>() {  
    /** "memory" for our calculator; variable/value pairs go here  */  
    private var memory: MutableMap<String, BigDecimal> = HashMap()  
    override fun visitAssign(ctx: AssignContext): BigDecimal {  
        val id = ctx.VARIABLE().text // id is left-hand side of '='  
        val value = visit(ctx.expr())!! // compute value of expression on right  
        memory[id] = value // store it in our memory  
        return value  
    }  
  
    override fun visitReturnValue(ctx: ReturnValueContext): BigDecimal {  
        return visit(ctx.expr())!!  
    }  
  
    /** Power  */  
    override fun visitPower(ctx: PowerContext): BigDecimal {  
        val left = visit(ctx.expr(0))!! // get value of left subexpression  
        val right = visit(ctx.expr(1))!! // get value of right subexpression  
        return BigDecimal(left.toDouble().pow(right.toDouble()))  
    }  
  
    /** expr op=('*'|'/') expr  */  
    override fun visitMulDiv(ctx: MulDivContext): BigDecimal {  
        val left = visit(ctx.expr(0))!! // get value of left subexpression  
        val right = visit(ctx.expr(1))!! // get value of right subexpression  
        return if (ctx.op.type == TIMES) left * right else left.divide(right, MathContext(20))  
    }  
  
    /** expr op=('+'|'-') expr  */  
    override fun visitAddSub(ctx: AddSubContext): BigDecimal {  
        val left = visit(ctx.expr(0))!! // get value of left subexpression  
        val right = visit(ctx.expr(1))!! // get value of right subexpression  
        return if (ctx.op.type == PLUS) left + right else left - right  
    }  
  
    /** Signed  */  
    override fun visitSigned(ctx: SignedContext): BigDecimal {  
        val value = visit(ctx.atom())!! // get value of subexpression  
        return if (ctx.op.type == PLUS) value else BigDecimal(-1) * value  
    }  
  
    /** Double  */  
    override fun visitDouble(ctx: DoubleContext): BigDecimal {  
        return BigDecimal(ctx.DOUBLE().text)  
    }  
  
    /** Integer  */  
    override fun visitInteger(ctx: IntegerContext): BigDecimal {  
        return BigDecimal(ctx.INT().text)  
    }  
  
    /** ConstantPI  */  
    override fun visitConstantPI(ctx: ConstantPIContext): BigDecimal {  
        return BigDecimal(Math.PI)  
    }  
  
    /** ConstantE  */  
    override fun visitConstantE(ctx: ConstantEContext): BigDecimal {  
        return BigDecimal(Math.E)  
    }  
  
    /** Variable  */  
    override fun visitVariable(ctx: VariableContext): BigDecimal {  
        val id = ctx.VARIABLE().text  
        return if (memory.containsKey(id)) memory[id]!! else BigDecimal(0)  
    }  
  
    /** Braces  */  
    override fun visitBraces(ctx: BracesContext): BigDecimal {  
        return visit(ctx.expr())!! // return child expr's value  
    }  
  
    /** LOG'('expr',' expr')'  */  
    override fun visitLogarithm(ctx: LogarithmContext): BigDecimal {  
        val a = visit(ctx.expr(0))!! // get value of 1st subexpression  
        val b = visit(ctx.expr(1))!! // get value of 2nd subexpression  
        return (Math.log(b.toDouble()) / Math.log(a.toDouble())).toBigDecimal()  
    }  
  
    /** LN'('expr')'  */  
    override fun visitNaturalLogarithm(ctx: NaturalLogarithmContext): BigDecimal {  
        val a = visit(ctx.expr())!! // get value of the subexpression  
        return ln(a.toDouble()).toBigDecimal()  
    }  
  
    /** SQRT'('expr')'  */  
    override fun visitSquareRoot(ctx: SquareRootContext): BigDecimal {  
        val value = visit(ctx.expr())!! // There is a loss of precision  
        return sqrt(value.toDouble()).toBigDecimal()  
    }  
  
    /** SIN'('expr')'  */  
    override fun visitSine(ctx: SineContext): BigDecimal {  
        val value = visit(ctx.expr())!! // There is a loss of precision  
        return sin(value.toDouble()).toBigDecimal()  
    }  
  
    /** ASIN'('expr')'  */  
    override fun visitASine(ctx: ASineContext): BigDecimal {  
        val value = visit(ctx.expr())!! // There is a loss of precision  
        return asin(value.toDouble()).toBigDecimal()  
    }  
  
    /** COS'('expr')'  */  
    override fun visitCosine(ctx: CosineContext): BigDecimal {  
        val value = visit(ctx.expr())!! //There is a loss of precision  
        return cos(value.toDouble()).toBigDecimal()  
    }  
  
    /** ACOS'('expr')'  */  
    override fun visitACosine(ctx: ACosineContext): BigDecimal {  
        val value = visit(ctx.expr())!! //There is a loss of precision  
        return acos(value.toDouble()).toBigDecimal()  
    }  
  
    /** TAN'('expr')'  */  
    override fun visitTangent(ctx: TangentContext): BigDecimal {  
        val value = visit(ctx.expr())!! // There is a loss of precision  
        return Math.tan(value.toDouble()).toBigDecimal()  
    }  
  
    /** ATAN'('expr')'  */  
    override fun visitATangent(ctx: ATangentContext): BigDecimal {  
        val value = visit(ctx.expr())!! // There is a loss of precision  
        return atan(value.toDouble()).toBigDecimal()  
    }  
  
  
    override fun visitRound(ctx: RoundContext): BigDecimal? {  
        val value = visit(ctx.expr())!! // get value of the subexpression  
        return value.setScale(0, RoundingMode.HALF_UP)  
    }  
  
    override fun visitFloor(ctx: FloorContext): BigDecimal {  
        val value = visit(ctx.expr())!! // get value of the subexpression  
        return value.setScale(0, RoundingMode.DOWN)  
    }  
  
    override fun visitCell(ctx: CellContext): BigDecimal {  
        val value = visit(ctx.expr())!! // get value of the subexpression  
        return value.setScale(0, RoundingMode.UP)  
    }  
  
    override fun visitMax(ctx: MaxContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!! // get value of left subexpression  
        val right = visit(ctx.expr(1))!! // get value of right subexpression  
        return if (left > right) left else right  
    }  
  
    override fun visitTernary(ctx: TernaryContext?): BigDecimal {  
        val condition = visit(ctx!!.expr(0))!! // get value of left subexpression  
        return if (condition > BigDecimal(0)) {  
            visit(ctx.expr(1))!!  
        } else {  
            visit(ctx.expr(2))!!  
        }  
    }  
    override fun visitEquality(ctx: EqualityContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!! // get value of left subexpression  
        val right = visit(ctx.expr(1))!! // get value of right subexpression  
        return if (left.compareTo(right)==0) BigDecimal(1) else BigDecimal(0)  
    }  
  
    override fun visitIfFunction(ctx: IfFunctionContext?): BigDecimal? {  
        val condition = visit(ctx!!.expr(0))!! // get value of left subexpression  
        return if (condition > BigDecimal(0)) {  
            visit(ctx.expr(1))  
        }else{  
            visit(ctx.expr(2))  
        }  
    }  
  
    override fun visitGe(ctx: GeContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left >= right){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
  
    override fun visitLe(ctx: LeContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left <= right){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
  
    override fun visitNe(ctx: NeContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left.compareTo(right)!=0){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
  
    override fun visitGt(ctx: GtContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left > right){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
  
    override fun visitLt(ctx: LtContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left < right){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
  
    override fun visitAnd(ctx: AndContext?): BigDecimal? {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left > BigDecimal(0) && right > BigDecimal(0)){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
    override fun visitOr(ctx: OrContext?): BigDecimal {  
        val left = visit(ctx!!.expr(0))!!  
        val right = visit(ctx.expr(1))!!  
        return if (left > BigDecimal(0) || right > BigDecimal(0)){  
            BigDecimal(1)  
        }else{  
            BigDecimal(0)  
        }  
    }  
  
}
```

5. 電卓を実現する
```kotlin
package com.kaopasu.calculator  
  
import org.antlr.v4.runtime.CharStreams  
import org.antlr.v4.runtime.CommonTokenStream  
import org.antlr.v4.runtime.tree.ParseTree  
import java.math.BigDecimal  
  
/**  
 * a calculator, it can calculate the formula with context and return decimal result * the context is a map, the key is variable name, the value is variable value * */class Calculator(private var context: MutableMap<String, String>) {  
    fun setContext(context: MutableMap<String, String>){  
        this.context = context  
    }  
    fun calculate(formula:String): BigDecimal? {  
        val contextString: String = context.map {  
            "${it.key}=${it.value}\n"  
        }.joinToString("")  
        val result: BigDecimal?  
        val charStreams = CharStreams.fromString(contextString+formula+"\n")  
        val lexer = CalculatorLexer(charStreams)  
        val tokens = CommonTokenStream(lexer)  
        val parser = CalculatorParser(tokens)  
        val tree: ParseTree = parser.prog() // parse  
        val eval = CalculatorEvalVisitorKotlin()  
        result = eval.visit(tree)  
        return result  
    }  
}  
  
fun main(){  
    //test  
    val calculator = Calculator(mutableMapOf("ショート勤務日数" to "1600","時間数" to "98"))  
    val result = calculator.calculate("if(時間数>ショート勤務日数||(0&&0),0,1)")  
    println(result)  
}
```

## まとめ
ANTLRは非常に使いやすく、機能が豊富なツールであり、私の要求を見事に実現してくれました。このツールを使うことで、ユーザーのいかなる給与計算ニーズも可能になりました。ANTLRの柔軟性と強力な機能により、特定の要件に合わせたカスタマイズが容易になり、プログラミング言語やデータフォーマットの解析において優れたパフォーマンスを発揮します