Refactor the Instruction parser into an Expression one.
Now support more arithmeric operator (mainly *, /) and recurcive bracket
This commit is contained in:
parent
409acf748f
commit
ce51a3be0a
19 changed files with 824 additions and 448 deletions
|
|
@ -1,130 +0,0 @@
|
|||
package com.pixelized.desktop.lwa.parser.arithmetic
|
||||
|
||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Dice.Modifier
|
||||
import com.pixelized.desktop.lwa.parser.arithmetic.Instruction.Word
|
||||
|
||||
// https://medium.com/@gustavoinzunza/how-to-solve-balanced-parenthesis-problem-with-regex-in-ruby-113eff30a79a
|
||||
// https://stackoverflow.com/questions/546433/regular-expression-to-match-balanced-parentheses
|
||||
class ArithmeticParser {
|
||||
|
||||
private val operatorParser = Regex(
|
||||
"""\s*(?<operator>[-+])?\s*(?<payload>[^-+]*)"""
|
||||
)
|
||||
private val diceParser = Regex(
|
||||
"""^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
|
||||
)
|
||||
private val wordParser = Regex(
|
||||
"""^(?<word>${words.joinToString(separator = "|") { it }})"""
|
||||
)
|
||||
private val flatParser = Regex(
|
||||
"""(?<flat>\d+)"""
|
||||
)
|
||||
|
||||
@Throws(Instruction.UnknownInstruction::class)
|
||||
fun parse(value: String): List<Instruction> {
|
||||
return operatorParser.findAll(value).mapNotNull {
|
||||
val (operator, instruction) = it.destructured
|
||||
if (instruction.isNotBlank()) {
|
||||
parseInstruction(
|
||||
sign = parseOperator(operator),
|
||||
instruction = instruction,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
|
||||
@Throws(Instruction.UnknownInstruction::class)
|
||||
fun parseInstruction(
|
||||
sign: Int,
|
||||
instruction: String
|
||||
): Instruction {
|
||||
diceParser.find(instruction)?.let {
|
||||
val (modifier, quantity, faces) = it.destructured
|
||||
Instruction.Dice(
|
||||
sign = sign,
|
||||
modifier = parseModifier(value = modifier),
|
||||
quantity = quantity.toInt(),
|
||||
faces = faces.toInt(),
|
||||
)
|
||||
}?.let { return it }
|
||||
|
||||
wordParser.find(instruction)?.let {
|
||||
parseWord(
|
||||
sign = sign,
|
||||
value = it.value
|
||||
)
|
||||
}?.let { return it }
|
||||
|
||||
flatParser.find(instruction)?.let {
|
||||
Instruction.Flat(
|
||||
sign = sign,
|
||||
value = it.value.toInt(),
|
||||
)
|
||||
}?.let { return it }
|
||||
|
||||
throw Instruction.UnknownInstruction(payload = instruction)
|
||||
}
|
||||
|
||||
private fun parseOperator(operator: String): Int {
|
||||
return when (operator) {
|
||||
"-" -> -1
|
||||
"+" -> 1
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseModifier(value: String): Modifier? {
|
||||
return when (value) {
|
||||
"a" -> Modifier.ADVANTAGE
|
||||
"d" -> Modifier.DISADVANTAGE
|
||||
"e" -> Modifier.EMPHASIS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseWord(
|
||||
sign: Int,
|
||||
value: String,
|
||||
): Word? {
|
||||
return try {
|
||||
Word(
|
||||
sign = sign,
|
||||
type = Word.Type.valueOf(value),
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun convertInstructionToString(
|
||||
instructions: List<Instruction>,
|
||||
): String {
|
||||
return instructions.map {
|
||||
val sign = if (it.sign > 0) "+" else "-"
|
||||
val value = when (it) {
|
||||
is Instruction.Dice -> "${it.modifier ?: ""}${it.quantity}d${it.faces}"
|
||||
is Instruction.Flat -> "${it.value}"
|
||||
is Word -> when (it.type) {
|
||||
Word.Type.BDC -> Word.Type.BDC.name
|
||||
Word.Type.BDD -> Word.Type.BDD.name
|
||||
Word.Type.STR -> Word.Type.STR.name
|
||||
Word.Type.DEX -> Word.Type.DEX.name
|
||||
Word.Type.CON -> Word.Type.CON.name
|
||||
Word.Type.HEI -> Word.Type.HEI.name
|
||||
Word.Type.INT -> Word.Type.INT.name
|
||||
Word.Type.POW -> Word.Type.POW.name
|
||||
Word.Type.CHA -> Word.Type.CHA.name
|
||||
}
|
||||
}
|
||||
"$sign$value"
|
||||
}.joinToString(separator = " ") {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val words: List<String> = Word.Type.entries.map { it.name }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package com.pixelized.desktop.lwa.parser.arithmetic
|
||||
|
||||
sealed class Instruction {
|
||||
abstract val sign: Int
|
||||
|
||||
data class Dice(
|
||||
override val sign: Int,
|
||||
val modifier: Modifier?,
|
||||
val quantity: Int,
|
||||
val faces: Int,
|
||||
) : Instruction() {
|
||||
|
||||
enum class Modifier {
|
||||
ADVANTAGE,
|
||||
DISADVANTAGE,
|
||||
EMPHASIS,
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "${sign.sign}${
|
||||
when (modifier) {
|
||||
Modifier.ADVANTAGE -> "a"
|
||||
Modifier.DISADVANTAGE -> "d"
|
||||
Modifier.EMPHASIS -> "e"
|
||||
null -> ""
|
||||
}
|
||||
}${quantity}d${faces}"
|
||||
}
|
||||
}
|
||||
|
||||
data class Flat(
|
||||
override val sign: Int,
|
||||
val value: Int,
|
||||
) : Instruction() {
|
||||
override fun toString(): String {
|
||||
return "${sign.sign}${value}"
|
||||
}
|
||||
}
|
||||
|
||||
data class Word(
|
||||
override val sign: Int,
|
||||
val type: Type,
|
||||
) : Instruction() {
|
||||
|
||||
enum class Type {
|
||||
BDC, // Damages bonus for melee
|
||||
BDD, // Damages bonus for range
|
||||
STR, // Strength
|
||||
DEX, // Dexterity
|
||||
CON, // Constitution
|
||||
HEI, // Height
|
||||
INT, // Intelligence
|
||||
POW, // Power
|
||||
CHA, // Charisma
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "${sign.sign}${type}"
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownInstruction(payload: String) : RuntimeException(
|
||||
"Unknown instruction exception. Unable to parse the following payload:\"$payload\" into an instruction"
|
||||
)
|
||||
|
||||
val Int.sign: String get() = if (this > 0) "+" else "-"
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.pixelized.desktop.lwa.parser.dice
|
||||
|
||||
data class Dice(
|
||||
val modifier: Modifier?,
|
||||
val quantity: Int,
|
||||
val faces: Int,
|
||||
) {
|
||||
enum class Modifier {
|
||||
ADVANTAGE,
|
||||
DISADVANTAGE,
|
||||
EMPHASIS,
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val modifier = when (modifier) {
|
||||
Modifier.ADVANTAGE -> "a"
|
||||
Modifier.DISADVANTAGE -> "d"
|
||||
Modifier.EMPHASIS -> "e"
|
||||
null -> ""
|
||||
}
|
||||
return "${modifier}${quantity}d${faces}"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.pixelized.desktop.lwa.parser.dice
|
||||
|
||||
class DiceParser {
|
||||
private val diceParser = Regex(
|
||||
"""^(?<modifier>[ade])?(?<quantity>\d+)[dD](?<faces>\d+)"""
|
||||
)
|
||||
|
||||
fun parse(expression: String): Dice? {
|
||||
return diceParser.find(expression)?.let {
|
||||
val (modifier, quantity, faces) = it.destructured
|
||||
Dice(
|
||||
modifier = parseModifier(value = modifier),
|
||||
quantity = quantity.toInt(),
|
||||
faces = faces.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseModifier(value: String): Dice.Modifier? {
|
||||
return when (value) {
|
||||
"a" -> Dice.Modifier.ADVANTAGE
|
||||
"d" -> Dice.Modifier.DISADVANTAGE
|
||||
"e" -> Dice.Modifier.EMPHASIS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.pixelized.desktop.lwa.parser.expression
|
||||
|
||||
import com.pixelized.desktop.lwa.parser.dice.Dice
|
||||
import com.pixelized.desktop.lwa.parser.word.Word
|
||||
|
||||
sealed interface Expression {
|
||||
|
||||
data class Add(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "$first+$second"
|
||||
}
|
||||
}
|
||||
|
||||
data class Minus(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "$first-$second"
|
||||
}
|
||||
}
|
||||
|
||||
data class Div(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "$first/$second"
|
||||
}
|
||||
}
|
||||
|
||||
data class Prod(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "$first*$second"
|
||||
}
|
||||
}
|
||||
|
||||
data class Minimum(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "minimum($first,$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Maximum(
|
||||
val first: Expression?,
|
||||
val second: Expression?,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "maximum($first,$second)"
|
||||
}
|
||||
}
|
||||
|
||||
data class Inversion(
|
||||
val expression: Expression,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "-($expression)"
|
||||
}
|
||||
}
|
||||
|
||||
data class DiceExpression(
|
||||
val dice: Dice,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return dice.toString()
|
||||
}
|
||||
}
|
||||
|
||||
data class WordExpression(
|
||||
val word: Word,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return word.toString()
|
||||
}
|
||||
}
|
||||
|
||||
data class Flat(
|
||||
val value: Int,
|
||||
) : Expression {
|
||||
override fun toString(): String {
|
||||
return "$value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
package com.pixelized.desktop.lwa.parser.expression
|
||||
|
||||
import com.pixelized.desktop.lwa.parser.dice.DiceParser
|
||||
import com.pixelized.desktop.lwa.parser.word.WordParser
|
||||
|
||||
/**
|
||||
* Highly inspired by the following javascript implementation:
|
||||
* https://tiarkrompf.github.io/notes/?/just-write-the-parser/
|
||||
*/
|
||||
class ExpressionParser(
|
||||
private val diceParser: DiceParser,
|
||||
private val wordParser: WordParser,
|
||||
) {
|
||||
companion object {
|
||||
private val tokenBreak = arrayOf(
|
||||
'+', '-', '/', '*', '(', ')', ','
|
||||
)
|
||||
private val operators = mapOf(
|
||||
'+' to Operator(
|
||||
evaluations = { first, second -> Expression.Add(first, second) },
|
||||
priority = 100,
|
||||
association = 1,
|
||||
),
|
||||
'-' to Operator(
|
||||
evaluations = { first, second -> Expression.Minus(first, second) },
|
||||
priority = 100,
|
||||
association = 1,
|
||||
),
|
||||
'*' to Operator(
|
||||
evaluations = { first, second -> Expression.Prod(first, second) },
|
||||
priority = 200,
|
||||
association = 1,
|
||||
),
|
||||
'/' to Operator(
|
||||
evaluations = { first, second -> Expression.Div(first, second) },
|
||||
priority = 200,
|
||||
association = 1,
|
||||
),
|
||||
)
|
||||
|
||||
private inline fun guard(condition: Boolean, error: () -> Error) {
|
||||
return if (condition.not()) throw error() else Unit
|
||||
}
|
||||
}
|
||||
|
||||
private val stack = Stack()
|
||||
|
||||
/**
|
||||
* Helper method to determined when a token should end.
|
||||
* Every characters that are not un the [Companion.tokenBreak] list can be part of a token.
|
||||
* @see Companion.tokenBreak
|
||||
*/
|
||||
private fun isToken(): Boolean = stack.peek().let {
|
||||
it != null && !tokenBreak.contains(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to pull from the stack until we have a complete token.
|
||||
* Use the isToken() method to determined when a token should end.
|
||||
* @see isToken
|
||||
*/
|
||||
private fun token(): String {
|
||||
guard(isToken()) {
|
||||
Error.ExpectedTokenCharacter(
|
||||
actual = stack.peek(),
|
||||
expression = stack.input,
|
||||
)
|
||||
}
|
||||
val token = StringBuilder()
|
||||
do {
|
||||
stack.pull().let(token::append)
|
||||
} while (isToken())
|
||||
return token.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to construct an expression from a token.
|
||||
* This method handle bracket recursion, functions [min(), max(), etc.]
|
||||
* and token expression creation (Word, Dice, Flat, etc.).
|
||||
* @see token
|
||||
*/
|
||||
private fun factor(): Expression? {
|
||||
when (stack.peek()) {
|
||||
'(' -> {
|
||||
stack.moveCursor()
|
||||
val result = evaluate()
|
||||
guard(stack.peek() == ')') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ')',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
stack.moveCursor()
|
||||
return result
|
||||
}
|
||||
|
||||
'-' -> { // this is considered as a sign function for the following expression.
|
||||
stack.moveCursor()
|
||||
val result = evaluate()
|
||||
return result?.let(Expression::Inversion)
|
||||
}
|
||||
|
||||
'+' -> { // this is considered as a sign function for the following expression.
|
||||
stack.moveCursor()
|
||||
val result = evaluate()
|
||||
return result
|
||||
}
|
||||
|
||||
else -> when (val token = token()) {
|
||||
"min" -> {
|
||||
// consume the '(' character
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the first parameter.
|
||||
val first = evaluate()
|
||||
// check that the expression is well formed, need a ,.
|
||||
guard(stack.peek() == ',') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ',',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ',' character of the second parameter.
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the second parameter.
|
||||
val second = evaluate()
|
||||
// check that the expression is well formed, need a ).
|
||||
guard(stack.peek() == ')') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ')',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ')' character
|
||||
stack.moveCursor()
|
||||
// build the final function expression
|
||||
return Expression.Minimum(first, second)
|
||||
}
|
||||
|
||||
"max" -> {
|
||||
// consume the '(' character
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the first parameter.
|
||||
val first = evaluate()
|
||||
// check that the expression is well formed, need a ,.
|
||||
guard(stack.peek() == ',') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ',',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ',' character of the second parameter.
|
||||
stack.moveCursor()
|
||||
// evaluate the content of the second parameter.
|
||||
val second = evaluate()
|
||||
// check that the expression is well formed, need a ).
|
||||
guard(stack.peek() == ')') {
|
||||
Error.ExpectedOperator(
|
||||
expected = ')',
|
||||
actual = stack.peek(),
|
||||
expression = stack.input
|
||||
)
|
||||
}
|
||||
// consume the ')' character
|
||||
stack.moveCursor()
|
||||
// build the final function expression
|
||||
return Expression.Maximum(first, second)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = token.toIntOrNull()
|
||||
if (value != null) {
|
||||
return Expression.Flat(value)
|
||||
}
|
||||
|
||||
val word = wordParser.parse(token)
|
||||
if (word != null) {
|
||||
return Expression.WordExpression(word)
|
||||
}
|
||||
|
||||
val dice = diceParser.parse(token)
|
||||
if (dice != null) {
|
||||
return Expression.DiceExpression(dice)
|
||||
}
|
||||
|
||||
throw Error.UnRecognizedToken(actual = token, expression = stack.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to handle operator priority and associativity.
|
||||
* build with the example https://tiarkrompf.github.io/notes/?/just-write-the-parser/ in mind.
|
||||
* @param minPriority the minimum priority to check.
|
||||
* @return a nullable [Expression]
|
||||
*/
|
||||
private fun evaluate(minPriority: Int): Expression? {
|
||||
var res = factor()
|
||||
while (
|
||||
operators.contains(stack.peek()) &&
|
||||
(operators[stack.peek()]?.priority ?: 0) >= minPriority
|
||||
) {
|
||||
val nextMin = operators[stack.peek()]?.let { it.priority + it.association } ?: 0
|
||||
res = operators[stack.pull()]?.evaluations?.invoke(
|
||||
res,
|
||||
evaluate(nextMin)
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to evaluate a expression that have been initialized into the stack.
|
||||
* This is typically call just after the stack have been initialized but also when a recursion is needed.
|
||||
* @return a nullable [Expression]
|
||||
*/
|
||||
private fun evaluate(): Expression? {
|
||||
return evaluate(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Public parsing method for an expression.
|
||||
* @param input the input expression [String]
|
||||
* @return a nullable [Expression]
|
||||
*/
|
||||
fun parse(input: String?): Expression? {
|
||||
return input.takeIf { it?.isNotBlank() == true }?.let {
|
||||
stack.init(expression = it)
|
||||
evaluate()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Error(
|
||||
message: String,
|
||||
expression: String,
|
||||
) : Exception("Expession:$expression - $message") {
|
||||
|
||||
class ExpectedTokenCharacter(
|
||||
val actual: Char?,
|
||||
val expression: String,
|
||||
) : Error(
|
||||
"Expected a token character (either a letter or a digit), but was: $actual", expression
|
||||
)
|
||||
|
||||
class ExpectedOperator(
|
||||
val expected: Char,
|
||||
val actual: Char?,
|
||||
val expression: String,
|
||||
) : Error("Expected operator: $expected, but was $actual", expression)
|
||||
|
||||
class UnRecognizedToken(
|
||||
val actual: String,
|
||||
val expression: String,
|
||||
) : Error(
|
||||
"Expected a specific token 'word', 'digit' or 'function', but was: $actual",
|
||||
expression
|
||||
)
|
||||
}
|
||||
|
||||
private data class Operator(
|
||||
val evaluations: (Expression?, Expression?) -> Expression?,
|
||||
val priority: Int, // operator priority, higher is priority
|
||||
val association: Int, // 0 : left associativity, 1: right
|
||||
)
|
||||
|
||||
private data class Stack(
|
||||
var input: String = "",
|
||||
private var cursor: Int = 0,
|
||||
private var peek: Char? = null,
|
||||
) {
|
||||
fun init(expression: String) {
|
||||
input = expression
|
||||
cursor = 0
|
||||
peek = null
|
||||
moveCursor()
|
||||
}
|
||||
|
||||
fun peek(): Char? = peek
|
||||
|
||||
fun pull(): Char? = peek().also { moveCursor() }
|
||||
|
||||
fun moveCursor() {
|
||||
peek = input.getOrNull(cursor)
|
||||
cursor = (cursor + 1).takeIf { it < input.length } ?: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.pixelized.desktop.lwa.parser.word
|
||||
|
||||
data class Word(
|
||||
val type: Type,
|
||||
) {
|
||||
enum class Type {
|
||||
BDC, // Damages bonus for melee
|
||||
BDD, // Damages bonus for range
|
||||
STR, // Strength
|
||||
DEX, // Dexterity
|
||||
CON, // Constitution
|
||||
HEI, // Height
|
||||
INT, // Intelligence
|
||||
POW, // Power
|
||||
CHA, // Charisma
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$type"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.pixelized.desktop.lwa.parser.word
|
||||
|
||||
class WordParser {
|
||||
|
||||
fun parse(
|
||||
value: String,
|
||||
): Word? {
|
||||
return try {
|
||||
Word(type = Word.Type.valueOf(value))
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue