人事労務システムを設計する際に、ユーザーからの要望で、設定した数式で計算する機能が必要とされて、このために、Antrl(パーサージェネレータツール)を使用し、IF ELSEなどの条件分岐を含む、より複雑な計算が可能な電卓機能を実装しました。
Antrlって何
ANTLR(ANother Tool for Language Recognition)とは、LL()構文解析に基づくパーサジェネレータである(バージョン3.xはLL()、2.xまではLL(k)) Wikipedia
手順
- 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
;
-
g4ファイルに基づいて、Recognizer を生成
今回はIDEAのAntrl v4 プラグインを使い、Javaコードを生成
]インストールしたら、Generate ANTRL Recognizerを押します
-
必要な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)
}
}
}
- 電卓を実現する
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の柔軟性と強力な機能により、特定の要件に合わせたカスタマイズが容易になり、プログラミング言語やデータフォーマットの解析において優れたパフォーマンスを発揮します