Skip to content

Commit

Permalink
Performance improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Nov 9, 2023
1 parent 1069917 commit 684a03d
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 69 deletions.
14 changes: 13 additions & 1 deletion core/common/src/internal/format/Predicate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,27 @@ internal interface Predicate<in T> {
fun test(value: T): Boolean
}

internal object Truth: Predicate<Any?> {
override fun test(value: Any?): Boolean = true
}

internal class ComparisonPredicate<in T, E>(
private val expectedValue: E,
private val getter: (T) -> E?
): Predicate<T> {
override fun test(value: T): Boolean = getter(value) == expectedValue
}

internal class ConjunctionPredicate<in T>(
private class ConjunctionPredicate<in T>(
private val predicates: List<Predicate<T>>
): Predicate<T> {
override fun test(value: T): Boolean = predicates.all { it.test(value) }
}

internal fun<T> conjunctionPredicate(
predicates: List<Predicate<T>>
): Predicate<T> = when {
predicates.isEmpty() -> Truth
predicates.size == 1 -> predicates.single()
else -> ConjunctionPredicate(predicates)
}
4 changes: 2 additions & 2 deletions core/common/src/internal/format/StringFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ internal fun <T> FormatStructure<T>.formatter(): FormatterStructure<T> {
is AlternativesParsingFormatStructure -> mainFormat.rec()
is OptionalFormatStructure -> {
val (formatter, fields) = format.rec()
val predicate = ConjunctionPredicate(fields.map {
val predicate = conjunctionPredicate(fields.map {
it.toComparisonPredicate() ?: throw IllegalArgumentException(
"The field '${it.name}' does not define a default value, and only fields that define a default value can" +
"be used in an 'optional' format."
Expand All @@ -118,7 +118,7 @@ internal fun <T> FormatStructure<T>.formatter(): FormatterStructure<T> {
ConditionalFormatter(
listOf(
predicate::test to ConstantStringFormatterStructure(onZero),
ConjunctionPredicate<T>(emptyList())::test to formatter
Truth::test to formatter
)
) to fields
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ internal class UnsignedIntFormatterStructure<in T>(
}

override fun format(obj: T, builder: Appendable, minusNotRequired: Boolean) {
val numberStr = number(obj).toString()
val zeroPaddingStr = '0'.toString().repeat(maxOf(0, zeroPadding - numberStr.length))
builder.append(zeroPaddingStr, numberStr)
val num = number(obj)
val numberStr = num.toString()
repeat(zeroPadding - numberStr.length) { builder.append('0') }
builder.append(numberStr)
}
}

Expand Down
89 changes: 59 additions & 30 deletions core/common/src/internal/format/parser/NumberConsumer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,28 @@ internal sealed class NumberConsumer<in Receiver>(
* if [length] is `null`, with a string consisting of any number of digits. [consume] itself does not
* necessarily check the length of the input string, instead expecting to be passed a valid one.
*
* @throws NumberFormatException if the given [input] is too large a number.
* Returns `null` on success and a `NumberConsumptionError` on failure.
*/
abstract fun Receiver.consume(input: String)
abstract fun Receiver.consume(input: String): NumberConsumptionError?
}

internal sealed interface NumberConsumptionError {
fun errorMessage(): String
object ExpectedInt: NumberConsumptionError {
override fun errorMessage() = "expected an Int value"
}
object ExpectedLong: NumberConsumptionError {
override fun errorMessage() = "expected a Long value"
}
class TooManyDigits(val maxDigits: Int): NumberConsumptionError {
override fun errorMessage() = "expected at most $maxDigits digits"
}
class TooFewDigits(val minDigits: Int): NumberConsumptionError {
override fun errorMessage() = "expected at least $minDigits digits"
}
class WrongConstant(val expected: String): NumberConsumptionError {
override fun errorMessage() = "expected '$expected'"
}
}

/**
Expand All @@ -43,20 +62,23 @@ internal class UnsignedIntConsumer<in Receiver>(
require(length == null || length in 1..9) { "Invalid length for field $whatThisExpects: $length" }
}

override fun Receiver.consume(input: String) {
override fun Receiver.consume(input: String): NumberConsumptionError? {
if (maxLength != null) {
if (input.length > maxLength) {
throw NumberFormatException("Expected at most $maxLength digits for $whatThisExpects but got $input")
return NumberConsumptionError.TooManyDigits(maxLength)
}
}
if (minLength != null) {
if (input.length < minLength) {
throw NumberFormatException("Expected at least $minLength digits for $whatThisExpects but got $input")
return NumberConsumptionError.TooFewDigits(minLength)
}
}
when (val result = input.toIntOrNull()) {
null -> throw NumberFormatException("Expected an Int value for $whatThisExpects but got $input")
else -> setter(this, if (multiplyByMinus1) -result else result)
return when (val result = input.toIntOrNull()) {
null -> NumberConsumptionError.ExpectedInt
else -> {
setter(this, if (multiplyByMinus1) -result else result)
null
}
}
}
}
Expand All @@ -72,16 +94,15 @@ internal class ReducedIntConsumer<in Receiver>(
private val baseMod = base % modulo
private val baseFloor = base - baseMod

override fun Receiver.consume(input: String) {
when (val result = input.toIntOrNull()) {
null -> throw NumberFormatException("Expected an Int value for $whatThisExpects but got $input")
else -> {
setter(this, if (result >= baseMod) {
baseFloor + result
} else {
baseFloor + modulo + result
})
}
override fun Receiver.consume(input: String): NumberConsumptionError? = when (val result = input.toIntOrNull()) {
null -> NumberConsumptionError.ExpectedInt
else -> {
setter(this, if (result >= baseMod) {
baseFloor + result
} else {
baseFloor + modulo + result
})
null
}
}
}
Expand All @@ -92,8 +113,10 @@ internal class ReducedIntConsumer<in Receiver>(
internal class ConstantNumberConsumer<in Receiver>(
private val expected: String
) : NumberConsumer<Receiver>(expected.length, "the predefined string $expected") {
override fun Receiver.consume(input: String) {
require(input == expected) { "Expected '$expected' but got $input" }
override fun Receiver.consume(input: String): NumberConsumptionError? = if (input == expected) {
NumberConsumptionError.WrongConstant(expected)
} else {
null
}
}

Expand All @@ -111,8 +134,11 @@ internal class UnsignedLongConsumer<in Receiver>(
}

override fun Receiver.consume(input: String) = when (val result = input.toLongOrNull()) {
null -> throw NumberFormatException("Expected a Long value for $whatThisExpects but got $input")
else -> setter(this, result)
null -> NumberConsumptionError.ExpectedLong
else -> {
setter(this, result)
null
}
}
}

Expand All @@ -127,14 +153,17 @@ internal class FractionPartConsumer<in Receiver>(
// TODO: bounds on maxLength
}

override fun Receiver.consume(input: String) {
if (minLength != null && input.length < minLength)
throw NumberFormatException("Expected at least $minLength digits for $whatThisExpects but got $input")
if (maxLength != null && input.length > maxLength)
throw NumberFormatException("Expected at most $maxLength digits for $whatThisExpects but got $input")
when (val numerator = input.toIntOrNull()) {
null -> throw NumberFormatException("Expected at most a 9-digit value for $whatThisExpects but got $input")
else -> setter(this, DecimalFraction(numerator, input.length))
override fun Receiver.consume(input: String): NumberConsumptionError? = when {
minLength != null && input.length < minLength ->
NumberConsumptionError.TooFewDigits(minLength)
maxLength != null && input.length > maxLength ->
NumberConsumptionError.TooManyDigits(maxLength)
else -> when (val numerator = input.toIntOrNull()) {
null -> NumberConsumptionError.TooManyDigits(9)
else -> {
setter(this, DecimalFraction(numerator, input.length))
null
}
}
}
}
45 changes: 21 additions & 24 deletions core/common/src/internal/format/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,34 +138,35 @@ internal class Parser<Output>(
onError: (ParseError) -> Unit,
onSuccess: (Int, Output) -> Unit
) {
var states = mutableListOf(ParserState(defaultState(), StructureIndex(0, commands), startIndex))
var states = mutableListOf(ParserState(defaultState(), 0, commands, startIndex))
while (states.isNotEmpty()) {
states = states.flatMap { state ->
val index = state.commandPosition
if (index.operationIndex < index.parserStructure.operations.size) {
val newIndex = StructureIndex(index.operationIndex + 1, index.parserStructure)
val command = state.commandPosition.parserStructure.operations[state.commandPosition.operationIndex]
val newStates = mutableListOf<ParserState<Output>>()
for (state in states) {
if (state.operationIndex < state.parserStructure.operations.size) {
val command = state.parserStructure.operations[state.operationIndex]
val result = with(command) { state.output.consume(input, state.inputPosition) }
if (result.isOk()) {
listOf(ParserState(state.output, newIndex, result.tryGetIndex()!!))
newStates.add(
ParserState(state.output, state.operationIndex + 1, state.parserStructure, result.tryGetIndex()!!)
)
} else {
onError(result.tryGetError()!!)
emptyList()
}
} else {
index.parserStructure.followedBy.map { nextStructure ->
ParserState(copyState(state.output), StructureIndex(0, nextStructure), state.inputPosition)
}.also {
if (it.isEmpty()) {
if (allowDanglingInput || state.inputPosition == input.length) {
onSuccess(state.inputPosition, state.output)
} else {
onError(ParseError(state.inputPosition) { "There is more input to consume" })
}
if (state.parserStructure.followedBy.isEmpty()) {
if (allowDanglingInput || state.inputPosition == input.length) {
onSuccess(state.inputPosition, state.output)
} else {
onError(ParseError(state.inputPosition) { "There is more input to consume" })
}
} else {
for (nextStructure in state.parserStructure.followedBy) {
newStates.add(ParserState(copyState(state.output), 0, nextStructure, state.inputPosition))
}
}
}
}.toMutableList()
}
states = newStates
}
}

Expand All @@ -185,12 +186,8 @@ internal class Parser<Output>(

private inner class ParserState<Output>(
val output: Output,
val commandPosition: StructureIndex<Output>,
val inputPosition: Int,
)

private class StructureIndex<in Output>(
val operationIndex: Int,
val parserStructure: ParserStructure<Output>
val parserStructure: ParserStructure<Output>,
val inputPosition: Int,
)
}
17 changes: 8 additions & 9 deletions core/common/src/internal/format/parser/ParserOperation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,17 @@ internal class NumberSpanParserOperation<Output>(
return ParseResult.Error(startIndex) {
"Only found $digitsInRow digits in a row, but need to parse $whatThisExpects"
}
val lengths = consumers.map { it.length ?: (digitsInRow - minLength + 1) }
var index = startIndex
for (i in lengths.indices) {
val numberString = input.substring(index, index + lengths[i])
try {
with(consumers[i]) { consume(numberString) }
} catch (e: Throwable) {
return ParseResult.Error(index, e) {
"Can not interpret the string '$numberString' as ${consumers[i].whatThisExpects}"
for (i in consumers.indices) {
val length = consumers[i].length ?: (digitsInRow - minLength + 1)
val numberString = input.substring(index, index + length)
val error = with(consumers[i]) { consume(numberString) }
if (error != null) {
return ParseResult.Error(index) {
"Can not interpret the string '$numberString' as ${consumers[i].whatThisExpects}: ${error.errorMessage()}"
}
}
index += lengths[i]
index += length
}
return ParseResult.Ok(index)
}
Expand Down

0 comments on commit 684a03d

Please sign in to comment.