2
2

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

Posted at

人事労務システムを設計する際に、ユーザーからの要望で、設定した数式で計算する機能が必要とされて、このために、Antrl(パーサージェネレータツール)を使用し、IF ELSEなどの条件分岐を含む、より複雑な計算が可能な電卓機能を実装しました。

Antrlって何

ANTLRANother Tool for Language Recognition)とは、LL()構文解析に基づくパーサジェネレータである(バージョン3.xはLL()、2.xまではLL(k)) Wikipedia

手順

  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  
   ;
  1. g4ファイルに基づいて、Recognizer を生成
    今回はIDEAのAntrl v4 プラグインを使い、Javaコードを生成Pasted image 20240109163932.png
    ]インストールしたら、Generate ANTRL Recognizerを押します
    uploading...0

  2. 必要なpackageを導入

dependencies {  
    implementation 'org.antlr:antlr4-runtime:4.13.1'  
}

4. VisitorでAntrlを実装

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)  
        }  
    }  
  
}
  1. 電卓を実現する
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の柔軟性と強力な機能により、特定の要件に合わせたカスタマイズが容易になり、プログラミング言語やデータフォーマットの解析において優れたパフォーマンスを発揮します

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2