Skip to content

Commit

Permalink
refactor: Convert ANTLR TimeExpression parser to a recursive decent p…
Browse files Browse the repository at this point in the history
…arser #1615
  • Loading branch information
rholshausen committed Oct 27, 2022
1 parent 5d78360 commit 2d730c7
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ object DateTimeExpression : KLogging() {
TimeExpression.executeTimeExpression(base, split[1])
when {
datePart is Err<String> && timePart is Err<String> -> datePart.mapError { "$it, " +
Regex("1:(\\d+)").replace(timePart.error) { mr ->
Regex("index (\\d+)").replace(timePart.error) { mr ->
val pos = parseInt(mr.groupValues[1])
"1:${pos + split[0].length + 1}"
"index ${pos + split[0].length + 1}"
}
}
datePart is Err<String> -> datePart
timePart is Err<String> -> timePart.mapError {
Regex("1:(\\d+)").replace(timePart.error) { mr ->
Regex("index (\\d+)").replace(timePart.error) { mr ->
val pos = parseInt(mr.groupValues[1])
"1:${pos + split[0].length + 1}"
"index ${pos + split[0].length + 1}"
}
}
else -> timePart
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package au.com.dius.pact.core.model.generators

import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import au.com.dius.pact.core.support.generators.expressions.Adjustment
import au.com.dius.pact.core.support.generators.expressions.Operation
import au.com.dius.pact.core.support.generators.expressions.TimeBase
import au.com.dius.pact.core.support.generators.expressions.TimeExpressionLexer
import au.com.dius.pact.core.support.generators.expressions.TimeExpressionParser
import au.com.dius.pact.core.support.generators.expressions.TimeOffsetType
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
import mu.KLogging
import org.antlr.v4.runtime.CharStreams
import org.antlr.v4.runtime.CommonTokenStream
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
Expand Down Expand Up @@ -68,17 +66,11 @@ object TimeExpression : KLogging() {
}

private fun parseTimeExpression(expression: String): Result<ParsedTimeExpression, String> {
val charStream = CharStreams.fromString(expression)
val lexer = TimeExpressionLexer(charStream)
val tokens = CommonTokenStream(lexer)
val parser = TimeExpressionParser(tokens)
val errorListener = ErrorListener()
parser.addErrorListener(errorListener)
val result = parser.expression()
return if (errorListener.errors.isNotEmpty()) {
Err("Error parsing expression: ${errorListener.errors.joinToString(", ")}")
} else {
Ok(ParsedTimeExpression(result.timeBase, result.adj))
val lexer = TimeExpressionLexer(expression)
val parser = TimeExpressionParser(lexer)
return when (val result = parser.expression()) {
is Err -> Err("Error parsing expression: ${result.error}")
is Ok -> Ok(ParsedTimeExpression(result.value.first, result.value.second.toMutableList()))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ class DateTimeExpressionSpec extends Specification {
'+' | 'Error parsing expression: Was expecting an integer at index 1'
'now +' | 'Error parsing expression: Was expecting an integer at index 5'
'tomorr' | /^Error parsing expression.*/
'now @ +' | 'Error parsing expression: line 1:7 mismatched input \'<EOF>\' expecting INT'
'+ @ +' | 'Error parsing expression: Was expecting an integer at index 2, Error parsing expression: line 1:5 mismatched input \'<EOF>\' expecting INT'
'now+ @ now +' | 'Error parsing expression: Was expecting an integer at index 5, Error parsing expression: line 1:12 mismatched input \'<EOF>\' expecting INT'
'now @ now +' | 'Error parsing expression: line 1:11 mismatched input \'<EOF>\' expecting INT'
'now @ +' | 'Error parsing expression: Was expecting an integer at index 7'
'+ @ +' | 'Error parsing expression: Was expecting an integer at index 2, Error parsing expression: Was expecting an integer at index 5'
'now+ @ now +' | 'Error parsing expression: Was expecting an integer at index 5, Error parsing expression: Was expecting an integer at index 12'
'now @ now +' | 'Error parsing expression: Was expecting an integer at index 11'
'now @ noo' | /^Error parsing expression.*/
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package au.com.dius.pact.model.generators
package au.com.dius.pact.core.model.generators

import au.com.dius.pact.core.model.generators.TimeExpression
import spock.lang.Specification
import spock.lang.Unroll

Expand Down Expand Up @@ -51,8 +50,8 @@ class TimeExpressionSpec extends Specification {
where:

expression | expected
'+' | 'Error parsing expression: line 1:1 mismatched input \'<EOF>\' expecting INT'
'now +' | 'Error parsing expression: line 1:5 mismatched input \'<EOF>\' expecting INT'
'+' | 'Error parsing expression: Was expecting an integer at index 1'
'now +' | 'Error parsing expression: Was expecting an integer at index 5'
'noo' | /^Error parsing expression.*/
}
}
6 changes: 0 additions & 6 deletions core/support/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
plugins {
id 'antlr'
}

dependencies {
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
Expand All @@ -25,8 +21,6 @@ dependencies {
exclude group: 'org.codehaus.groovy'
}
testImplementation "junit:junit:${project.junitVersion}"

antlr "org.antlr:antlr4:4.11.1"
}

compileJava {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package au.com.dius.pact.core.support.generators.expressions

import au.com.dius.pact.core.support.parsers.StringLexer
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result

class TimeExpressionLexer(expression: String): StringLexer(expression) {
companion object {
val HOURS = Regex("^hours?")
val SECONDS = Regex("^seconds?")
val MINUTES = Regex("^minutes?")
val MILLISECONDS = Regex("^milliseconds?")
}
}

@Suppress("MaxLineLength")
class TimeExpressionParser(private val lexer: TimeExpressionLexer) {
//expression returns [ TimeBase timeBase = TimeBase.Now.INSTANCE, List<Adjustment<TimeOffsetType>> adj = new ArrayList<>() ] : ( base { $timeBase = $base.t; }
// | op duration { if ($duration.d != null) $adj.add($duration.d.withOperation($op.o)); } ( op duration { if ($duration.d != null) $adj.add($duration.d.withOperation($op.o)); } )*
// | base { $timeBase = $base.t; } ( op duration { if ($duration.d != null) $adj.add($duration.d.withOperation($op.o)); } )*
// | 'next' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.PLUS)); }
// | 'next' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.PLUS)); } ( op duration {
// if ($duration.d != null) $adj.add($duration.d.withOperation($op.o));
// } )*
// | 'last' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.MINUS)); }
// | 'last' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.MINUS)); } ( op duration {
// if ($duration.d != null) $adj.add($duration.d.withOperation($op.o));
// } )*
// ) EOF
@Suppress("ComplexMethod", "ReturnCount")
fun expression(): Result<Pair<TimeBase, List<Adjustment<TimeOffsetType>>>, String> {
val timeBase = TimeBase.Now

val baseResult = base()
if (baseResult is Ok && baseResult.value != null) {
return when (val opResult = parseOp()) {
is Ok -> if (opResult.value != null) {
Ok(baseResult.value!! to opResult.value!!)
} else {
Ok(baseResult.value!! to emptyList())
}
is Err -> opResult
}
} else if (baseResult is Err) {
return baseResult
}

when (val opResult = parseOp()) {
is Ok -> if (opResult.value != null) {
return Ok(timeBase to opResult.value!!)
}
is Err -> return opResult
}

val nextOrLastResult = parseNextOrLast()
if (nextOrLastResult != null) {
return when (val offsetResult = offset()) {
is Ok -> {
val adj = mutableListOf<Adjustment<TimeOffsetType>>()
adj.add(Adjustment(offsetResult.value.first, offsetResult.value.second, nextOrLastResult))
when (val opResult = parseOp()) {
is Ok -> if (opResult.value != null) {
adj.addAll(opResult.value!!)
Ok(timeBase to adj)
} else {
Ok(timeBase to adj)
}
is Err -> opResult
}
}
is Err -> offsetResult
}
}

return if (lexer.empty) {
Ok(timeBase to emptyList())
} else {
Err("Unexpected characters '${lexer.remainder}' at index ${lexer.index}")
}
}

@Suppress("ReturnCount")
private fun parseOp(): Result<List<Adjustment<TimeOffsetType>>?, String> {
val adj = mutableListOf<Adjustment<TimeOffsetType>>()
var opResult = op()
if (opResult != null) {
while (opResult != null) {
when (val durationResult = duration()) {
is Ok -> adj.add(durationResult.value.withOperation(opResult))
is Err -> return durationResult
}
opResult = op()
}
return Ok(adj)
}
return Ok(null)
}

//base returns [ TimeBase t ] : 'now' { $t = TimeBase.Now.INSTANCE; }
// | 'midnight' { $t = TimeBase.Midnight.INSTANCE; }
// | 'noon' { $t = TimeBase.Noon.INSTANCE; }
// | INT oclock { $t = TimeBase.of($INT.int, $oclock.h); }
// ;
fun base(): Result<TimeBase?, String> {
lexer.skipWhitespace()

val result = lexer.matchRegex(StringLexer.INT)
return if (result != null) {
val intValue = result.toInt()
when (val hourResult = oclock()) {
is Ok -> Ok(TimeBase.of(intValue, hourResult.value))
is Err -> Err(hourResult.error)
}
} else {
when {
lexer.matchString("now") -> Ok(TimeBase.Now)
lexer.matchString("midnight") -> Ok(TimeBase.Midnight)
lexer.matchString("noon") -> Ok(TimeBase.Noon)
else -> Ok(null)
}
}
}

//oclock returns [ ClockHour h ] : 'o\'clock' 'am' { $h = ClockHour.AM; }
// | 'o\'clock' 'pm' { $h = ClockHour.PM; }
// | 'o\'clock' { $h = ClockHour.NEXT; }
fun oclock(): Result<ClockHour, String> {
lexer.skipWhitespace()
return if (lexer.matchString("o'clock")) {
lexer.skipWhitespace()
when {
lexer.matchString("am") -> Ok(ClockHour.AM)
lexer.matchString("pm") -> Ok(ClockHour.PM)
else -> Ok(ClockHour.NEXT)
}
} else {
Err("Was expecting a clock hour at index ${lexer.index}")
}
}

//duration returns [ Adjustment<TimeOffsetType> d ] : INT durationType { $d = new Adjustment<TimeOffsetType>($durationType.type, $INT.int); } ;
fun duration(): Result<Adjustment<TimeOffsetType>, String> {
lexer.skipWhitespace()

val intResult = when (val result = lexer.parseInt()) {
is Ok -> result.value
is Err -> return result
}

val durationTypeResult = durationType()
return if (durationTypeResult != null) {
Ok(Adjustment(durationTypeResult, intResult))
} else {
Err("Was expecting a duration type at index ${lexer.index}")
}
}

//durationType returns [ TimeOffsetType type ] : 'hour' { $type = TimeOffsetType.HOUR; }
// | HOURS { $type = TimeOffsetType.HOUR; }
// | 'minute' { $type = TimeOffsetType.MINUTE; }
// | MINUTES { $type = TimeOffsetType.MINUTE; }
// | 'second' { $type = TimeOffsetType.SECOND; }
// | SECONDS { $type = TimeOffsetType.SECOND; }
// | 'millisecond' { $type = TimeOffsetType.MILLISECOND; }
// | MILLISECONDS { $type = TimeOffsetType.MILLISECOND; }
// ;
fun durationType(): TimeOffsetType? {
lexer.skipWhitespace()
return when {
lexer.matchRegex(TimeExpressionLexer.HOURS) != null -> TimeOffsetType.HOUR
lexer.matchRegex(TimeExpressionLexer.MINUTES) != null -> TimeOffsetType.MINUTE
lexer.matchRegex(TimeExpressionLexer.SECONDS) != null -> TimeOffsetType.SECOND
lexer.matchRegex(TimeExpressionLexer.MILLISECONDS) != null -> TimeOffsetType.MILLISECOND
else -> null
}
}

//op returns [ Operation o ] : '+' { $o = Operation.PLUS; }
// | '-' { $o = Operation.MINUS; }
// ;
fun op(): Operation? {
lexer.skipWhitespace()
return when {
lexer.matchChar('+') -> Operation.PLUS
lexer.matchChar('-') -> Operation.MINUS
else -> null
}
}

//offset returns [ TimeOffsetType type, int val = 1 ] : 'hour' { $type = TimeOffsetType.HOUR; }
// | 'minute' { $type = TimeOffsetType.MINUTE; }
// | 'second' { $type = TimeOffsetType.SECOND; }
// | 'millisecond' { $type = TimeOffsetType.MILLISECOND; }
// ;
fun offset(): Result<Pair<TimeOffsetType, Int>, String> {
lexer.skipWhitespace()
return when {
lexer.matchString("hour") -> Ok(TimeOffsetType.HOUR to 1)
lexer.matchString("minute") -> Ok(TimeOffsetType.MINUTE to 1)
lexer.matchString("second") -> Ok(TimeOffsetType.SECOND to 1)
lexer.matchString("millisecond") -> Ok(TimeOffsetType.MILLISECOND to 1)
else -> Err("Was expecting an offset type at index ${lexer.index}")
}
}

private fun parseNextOrLast(): Operation? {
lexer.skipWhitespace()
return when {
lexer.matchString("next") -> Operation.PLUS
lexer.matchString("last") -> Operation.MINUS
else -> null
}
}
}
Loading

0 comments on commit 2d730c7

Please sign in to comment.