Skip to content

Commit 2d730c7

Browse files
committed
refactor: Convert ANTLR TimeExpression parser to a recursive decent parser #1615
1 parent 5d78360 commit 2d730c7

File tree

8 files changed

+234
-97
lines changed

8 files changed

+234
-97
lines changed

core/model/src/main/kotlin/au/com/dius/pact/core/model/generators/DateTimeExpression.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ object DateTimeExpression : KLogging() {
2020
TimeExpression.executeTimeExpression(base, split[1])
2121
when {
2222
datePart is Err<String> && timePart is Err<String> -> datePart.mapError { "$it, " +
23-
Regex("1:(\\d+)").replace(timePart.error) { mr ->
23+
Regex("index (\\d+)").replace(timePart.error) { mr ->
2424
val pos = parseInt(mr.groupValues[1])
25-
"1:${pos + split[0].length + 1}"
25+
"index ${pos + split[0].length + 1}"
2626
}
2727
}
2828
datePart is Err<String> -> datePart
2929
timePart is Err<String> -> timePart.mapError {
30-
Regex("1:(\\d+)").replace(timePart.error) { mr ->
30+
Regex("index (\\d+)").replace(timePart.error) { mr ->
3131
val pos = parseInt(mr.groupValues[1])
32-
"1:${pos + split[0].length + 1}"
32+
"index ${pos + split[0].length + 1}"
3333
}
3434
}
3535
else -> timePart

core/model/src/main/kotlin/au/com/dius/pact/core/model/generators/TimeExpression.kt

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
package au.com.dius.pact.core.model.generators
22

3-
import com.github.michaelbull.result.Err
4-
import com.github.michaelbull.result.Ok
5-
import com.github.michaelbull.result.Result
63
import au.com.dius.pact.core.support.generators.expressions.Adjustment
74
import au.com.dius.pact.core.support.generators.expressions.Operation
85
import au.com.dius.pact.core.support.generators.expressions.TimeBase
96
import au.com.dius.pact.core.support.generators.expressions.TimeExpressionLexer
107
import au.com.dius.pact.core.support.generators.expressions.TimeExpressionParser
118
import au.com.dius.pact.core.support.generators.expressions.TimeOffsetType
9+
import com.github.michaelbull.result.Err
10+
import com.github.michaelbull.result.Ok
11+
import com.github.michaelbull.result.Result
1212
import mu.KLogging
13-
import org.antlr.v4.runtime.CharStreams
14-
import org.antlr.v4.runtime.CommonTokenStream
1513
import java.time.LocalTime
1614
import java.time.OffsetDateTime
1715
import java.time.ZoneOffset
@@ -68,17 +66,11 @@ object TimeExpression : KLogging() {
6866
}
6967

7068
private fun parseTimeExpression(expression: String): Result<ParsedTimeExpression, String> {
71-
val charStream = CharStreams.fromString(expression)
72-
val lexer = TimeExpressionLexer(charStream)
73-
val tokens = CommonTokenStream(lexer)
74-
val parser = TimeExpressionParser(tokens)
75-
val errorListener = ErrorListener()
76-
parser.addErrorListener(errorListener)
77-
val result = parser.expression()
78-
return if (errorListener.errors.isNotEmpty()) {
79-
Err("Error parsing expression: ${errorListener.errors.joinToString(", ")}")
80-
} else {
81-
Ok(ParsedTimeExpression(result.timeBase, result.adj))
69+
val lexer = TimeExpressionLexer(expression)
70+
val parser = TimeExpressionParser(lexer)
71+
return when (val result = parser.expression()) {
72+
is Err -> Err("Error parsing expression: ${result.error}")
73+
is Ok -> Ok(ParsedTimeExpression(result.value.first, result.value.second.toMutableList()))
8274
}
8375
}
8476
}

core/model/src/test/groovy/au/com/dius/pact/core/model/generators/DateTimeExpressionSpec.groovy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,10 @@ class DateTimeExpressionSpec extends Specification {
104104
'+' | 'Error parsing expression: Was expecting an integer at index 1'
105105
'now +' | 'Error parsing expression: Was expecting an integer at index 5'
106106
'tomorr' | /^Error parsing expression.*/
107-
'now @ +' | 'Error parsing expression: line 1:7 mismatched input \'<EOF>\' expecting INT'
108-
'+ @ +' | 'Error parsing expression: Was expecting an integer at index 2, Error parsing expression: line 1:5 mismatched input \'<EOF>\' expecting INT'
109-
'now+ @ now +' | 'Error parsing expression: Was expecting an integer at index 5, Error parsing expression: line 1:12 mismatched input \'<EOF>\' expecting INT'
110-
'now @ now +' | 'Error parsing expression: line 1:11 mismatched input \'<EOF>\' expecting INT'
107+
'now @ +' | 'Error parsing expression: Was expecting an integer at index 7'
108+
'+ @ +' | 'Error parsing expression: Was expecting an integer at index 2, Error parsing expression: Was expecting an integer at index 5'
109+
'now+ @ now +' | 'Error parsing expression: Was expecting an integer at index 5, Error parsing expression: Was expecting an integer at index 12'
110+
'now @ now +' | 'Error parsing expression: Was expecting an integer at index 11'
111111
'now @ noo' | /^Error parsing expression.*/
112112
}
113113

core/model/src/test/groovy/au/com/dius/pact/core/model/generators/TimeExpressionSpec.groovy

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
package au.com.dius.pact.model.generators
1+
package au.com.dius.pact.core.model.generators
22

3-
import au.com.dius.pact.core.model.generators.TimeExpression
43
import spock.lang.Specification
54
import spock.lang.Unroll
65

@@ -51,8 +50,8 @@ class TimeExpressionSpec extends Specification {
5150
where:
5251

5352
expression | expected
54-
'+' | 'Error parsing expression: line 1:1 mismatched input \'<EOF>\' expecting INT'
55-
'now +' | 'Error parsing expression: line 1:5 mismatched input \'<EOF>\' expecting INT'
53+
'+' | 'Error parsing expression: Was expecting an integer at index 1'
54+
'now +' | 'Error parsing expression: Was expecting an integer at index 5'
5655
'noo' | /^Error parsing expression.*/
5756
}
5857
}

core/support/build.gradle

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
plugins {
2-
id 'antlr'
3-
}
4-
51
dependencies {
62
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
73
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
@@ -25,8 +21,6 @@ dependencies {
2521
exclude group: 'org.codehaus.groovy'
2622
}
2723
testImplementation "junit:junit:${project.junitVersion}"
28-
29-
antlr "org.antlr:antlr4:4.11.1"
3024
}
3125

3226
compileJava {

core/support/src/main/antlr/au/com/dius/pact/core/support/generators/expressions/TimeExpression.g4

Lines changed: 0 additions & 62 deletions
This file was deleted.
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package au.com.dius.pact.core.support.generators.expressions
2+
3+
import au.com.dius.pact.core.support.parsers.StringLexer
4+
import com.github.michaelbull.result.Err
5+
import com.github.michaelbull.result.Ok
6+
import com.github.michaelbull.result.Result
7+
8+
class TimeExpressionLexer(expression: String): StringLexer(expression) {
9+
companion object {
10+
val HOURS = Regex("^hours?")
11+
val SECONDS = Regex("^seconds?")
12+
val MINUTES = Regex("^minutes?")
13+
val MILLISECONDS = Regex("^milliseconds?")
14+
}
15+
}
16+
17+
@Suppress("MaxLineLength")
18+
class TimeExpressionParser(private val lexer: TimeExpressionLexer) {
19+
//expression returns [ TimeBase timeBase = TimeBase.Now.INSTANCE, List<Adjustment<TimeOffsetType>> adj = new ArrayList<>() ] : ( base { $timeBase = $base.t; }
20+
// | 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)); } )*
21+
// | base { $timeBase = $base.t; } ( op duration { if ($duration.d != null) $adj.add($duration.d.withOperation($op.o)); } )*
22+
// | 'next' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.PLUS)); }
23+
// | 'next' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.PLUS)); } ( op duration {
24+
// if ($duration.d != null) $adj.add($duration.d.withOperation($op.o));
25+
// } )*
26+
// | 'last' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.MINUS)); }
27+
// | 'last' offset { $adj.add(new Adjustment($offset.type, $offset.val, Operation.MINUS)); } ( op duration {
28+
// if ($duration.d != null) $adj.add($duration.d.withOperation($op.o));
29+
// } )*
30+
// ) EOF
31+
@Suppress("ComplexMethod", "ReturnCount")
32+
fun expression(): Result<Pair<TimeBase, List<Adjustment<TimeOffsetType>>>, String> {
33+
val timeBase = TimeBase.Now
34+
35+
val baseResult = base()
36+
if (baseResult is Ok && baseResult.value != null) {
37+
return when (val opResult = parseOp()) {
38+
is Ok -> if (opResult.value != null) {
39+
Ok(baseResult.value!! to opResult.value!!)
40+
} else {
41+
Ok(baseResult.value!! to emptyList())
42+
}
43+
is Err -> opResult
44+
}
45+
} else if (baseResult is Err) {
46+
return baseResult
47+
}
48+
49+
when (val opResult = parseOp()) {
50+
is Ok -> if (opResult.value != null) {
51+
return Ok(timeBase to opResult.value!!)
52+
}
53+
is Err -> return opResult
54+
}
55+
56+
val nextOrLastResult = parseNextOrLast()
57+
if (nextOrLastResult != null) {
58+
return when (val offsetResult = offset()) {
59+
is Ok -> {
60+
val adj = mutableListOf<Adjustment<TimeOffsetType>>()
61+
adj.add(Adjustment(offsetResult.value.first, offsetResult.value.second, nextOrLastResult))
62+
when (val opResult = parseOp()) {
63+
is Ok -> if (opResult.value != null) {
64+
adj.addAll(opResult.value!!)
65+
Ok(timeBase to adj)
66+
} else {
67+
Ok(timeBase to adj)
68+
}
69+
is Err -> opResult
70+
}
71+
}
72+
is Err -> offsetResult
73+
}
74+
}
75+
76+
return if (lexer.empty) {
77+
Ok(timeBase to emptyList())
78+
} else {
79+
Err("Unexpected characters '${lexer.remainder}' at index ${lexer.index}")
80+
}
81+
}
82+
83+
@Suppress("ReturnCount")
84+
private fun parseOp(): Result<List<Adjustment<TimeOffsetType>>?, String> {
85+
val adj = mutableListOf<Adjustment<TimeOffsetType>>()
86+
var opResult = op()
87+
if (opResult != null) {
88+
while (opResult != null) {
89+
when (val durationResult = duration()) {
90+
is Ok -> adj.add(durationResult.value.withOperation(opResult))
91+
is Err -> return durationResult
92+
}
93+
opResult = op()
94+
}
95+
return Ok(adj)
96+
}
97+
return Ok(null)
98+
}
99+
100+
//base returns [ TimeBase t ] : 'now' { $t = TimeBase.Now.INSTANCE; }
101+
// | 'midnight' { $t = TimeBase.Midnight.INSTANCE; }
102+
// | 'noon' { $t = TimeBase.Noon.INSTANCE; }
103+
// | INT oclock { $t = TimeBase.of($INT.int, $oclock.h); }
104+
// ;
105+
fun base(): Result<TimeBase?, String> {
106+
lexer.skipWhitespace()
107+
108+
val result = lexer.matchRegex(StringLexer.INT)
109+
return if (result != null) {
110+
val intValue = result.toInt()
111+
when (val hourResult = oclock()) {
112+
is Ok -> Ok(TimeBase.of(intValue, hourResult.value))
113+
is Err -> Err(hourResult.error)
114+
}
115+
} else {
116+
when {
117+
lexer.matchString("now") -> Ok(TimeBase.Now)
118+
lexer.matchString("midnight") -> Ok(TimeBase.Midnight)
119+
lexer.matchString("noon") -> Ok(TimeBase.Noon)
120+
else -> Ok(null)
121+
}
122+
}
123+
}
124+
125+
//oclock returns [ ClockHour h ] : 'o\'clock' 'am' { $h = ClockHour.AM; }
126+
// | 'o\'clock' 'pm' { $h = ClockHour.PM; }
127+
// | 'o\'clock' { $h = ClockHour.NEXT; }
128+
fun oclock(): Result<ClockHour, String> {
129+
lexer.skipWhitespace()
130+
return if (lexer.matchString("o'clock")) {
131+
lexer.skipWhitespace()
132+
when {
133+
lexer.matchString("am") -> Ok(ClockHour.AM)
134+
lexer.matchString("pm") -> Ok(ClockHour.PM)
135+
else -> Ok(ClockHour.NEXT)
136+
}
137+
} else {
138+
Err("Was expecting a clock hour at index ${lexer.index}")
139+
}
140+
}
141+
142+
//duration returns [ Adjustment<TimeOffsetType> d ] : INT durationType { $d = new Adjustment<TimeOffsetType>($durationType.type, $INT.int); } ;
143+
fun duration(): Result<Adjustment<TimeOffsetType>, String> {
144+
lexer.skipWhitespace()
145+
146+
val intResult = when (val result = lexer.parseInt()) {
147+
is Ok -> result.value
148+
is Err -> return result
149+
}
150+
151+
val durationTypeResult = durationType()
152+
return if (durationTypeResult != null) {
153+
Ok(Adjustment(durationTypeResult, intResult))
154+
} else {
155+
Err("Was expecting a duration type at index ${lexer.index}")
156+
}
157+
}
158+
159+
//durationType returns [ TimeOffsetType type ] : 'hour' { $type = TimeOffsetType.HOUR; }
160+
// | HOURS { $type = TimeOffsetType.HOUR; }
161+
// | 'minute' { $type = TimeOffsetType.MINUTE; }
162+
// | MINUTES { $type = TimeOffsetType.MINUTE; }
163+
// | 'second' { $type = TimeOffsetType.SECOND; }
164+
// | SECONDS { $type = TimeOffsetType.SECOND; }
165+
// | 'millisecond' { $type = TimeOffsetType.MILLISECOND; }
166+
// | MILLISECONDS { $type = TimeOffsetType.MILLISECOND; }
167+
// ;
168+
fun durationType(): TimeOffsetType? {
169+
lexer.skipWhitespace()
170+
return when {
171+
lexer.matchRegex(TimeExpressionLexer.HOURS) != null -> TimeOffsetType.HOUR
172+
lexer.matchRegex(TimeExpressionLexer.MINUTES) != null -> TimeOffsetType.MINUTE
173+
lexer.matchRegex(TimeExpressionLexer.SECONDS) != null -> TimeOffsetType.SECOND
174+
lexer.matchRegex(TimeExpressionLexer.MILLISECONDS) != null -> TimeOffsetType.MILLISECOND
175+
else -> null
176+
}
177+
}
178+
179+
//op returns [ Operation o ] : '+' { $o = Operation.PLUS; }
180+
// | '-' { $o = Operation.MINUS; }
181+
// ;
182+
fun op(): Operation? {
183+
lexer.skipWhitespace()
184+
return when {
185+
lexer.matchChar('+') -> Operation.PLUS
186+
lexer.matchChar('-') -> Operation.MINUS
187+
else -> null
188+
}
189+
}
190+
191+
//offset returns [ TimeOffsetType type, int val = 1 ] : 'hour' { $type = TimeOffsetType.HOUR; }
192+
// | 'minute' { $type = TimeOffsetType.MINUTE; }
193+
// | 'second' { $type = TimeOffsetType.SECOND; }
194+
// | 'millisecond' { $type = TimeOffsetType.MILLISECOND; }
195+
// ;
196+
fun offset(): Result<Pair<TimeOffsetType, Int>, String> {
197+
lexer.skipWhitespace()
198+
return when {
199+
lexer.matchString("hour") -> Ok(TimeOffsetType.HOUR to 1)
200+
lexer.matchString("minute") -> Ok(TimeOffsetType.MINUTE to 1)
201+
lexer.matchString("second") -> Ok(TimeOffsetType.SECOND to 1)
202+
lexer.matchString("millisecond") -> Ok(TimeOffsetType.MILLISECOND to 1)
203+
else -> Err("Was expecting an offset type at index ${lexer.index}")
204+
}
205+
}
206+
207+
private fun parseNextOrLast(): Operation? {
208+
lexer.skipWhitespace()
209+
return when {
210+
lexer.matchString("next") -> Operation.PLUS
211+
lexer.matchString("last") -> Operation.MINUS
212+
else -> null
213+
}
214+
}
215+
}

0 commit comments

Comments
 (0)