-
Notifications
You must be signed in to change notification settings - Fork 623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support to decode numeric literals containing an exponent #2227
Conversation
Hi, thanks for your PR, and sorry it has taken so long to get to it. I see you have some other commits beside yours, did you start your branch off Regarding contents: this is really nice addition IMO, as json.org spec indeed permits exponent numbers. However, to fully support them, you also need to consider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
I actually started it on dev but synced my branch from github and pulled the master changes accidentally
I will take a look on decoding from JsonElement to Type |
you mean creating another pull request to the dev branch with the master changes? or just rebase master into my branch with newest changes? |
Your branch (with your-only commits) should start from the latest dev. You can force-push it to use the same pull request |
You can drop unnecessary commits during interactive rebase |
ty, I didn't know |
formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonExponentTest.kt
Show resolved
Hide resolved
private val INT_NUMBERS_CHARS = arrayOf('1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'e', 'E', '+', '-') | ||
internal fun String.toLongJson(): Long { | ||
return toLongOrNull() ?: toDouble() | ||
.takeIf { all { it in INT_NUMBERS_CHARS } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should cases like this one be supported when decoding into a long? and if so, should it be truncated, rounded...?
Same here
formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt
Show resolved
Hide resolved
formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt
Show resolved
Hide resolved
} | ||
internal fun String.toLongJsonOrNull(): Long? { | ||
return toLongOrNull() ?: toDoubleOrNull() | ||
?.takeIf { all { it in INT_NUMBERS_CHARS } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think such code poses hidden problem: all
iterates all symbols and it in
iterates all chars in the INT_NUMBERS_CHARS
array, so it results in a quadratic complexity. Moreover, it is just hard to read. I see several possibilities here:
- To simplify condition, I think it is possible just to check
'.' !in it
. All other non-digit and non-E signs should be prohibited bytoDoubleOrNull()
, I guess? - A harder, but more correct change would be extract parsing logic from
consumeNumericLiteral
or to createStringJsonLexer
in place so we wouldn't have two different code paths doing the same thing.
I think it is possible to also extract these takeIf
s to a separate function since it doesn't matter if you use toDouble
or toDoubleOrNull
given that exception is anyway thrown at the end if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think StringJsonLexer
is the best solution
true -> 10.0.pow(exponentAccumulator.toDouble()) | ||
} | ||
|
||
if(hasExponent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] formatting: space before opening brace (check other places too)
private inline fun <T> mapExceptionsToNull(f : () -> T) : T? { | ||
return try { f()} | ||
catch (e : JsonDecodingException) { null } | ||
catch (e : IndexOutOfBoundsException) { null } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt IIOBE can happen here, because there is special EOF handling in the consumeNumericLiteral
. (and generally, lexer is not allowed to throw it, because it must throw only JsonDecodingException). Do you have an example when it happens?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this test was failing on TREE mode, hasChar on consumeNumericLiteral doesn't update when the char is - | + | e | E
Lines 134 to 144 in cfce5d8
@Test | |
fun testInvalidNumber() = parametrizedTest { | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":-}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":+}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":--}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":1-1}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":0-1}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":0-}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":a}""", it) } | |
assertFailsWithSerial("JsonDecodingException") { default.decodeFromString<Holder>("""{"id":-a}""", it) } | |
} |
@@ -315,6 +321,18 @@ public val JsonPrimitive.contentOrNull: String? get() = if (this is JsonNull) nu | |||
private fun JsonElement.error(element: String): Nothing = | |||
throw IllegalArgumentException("Element ${this::class} is not a $element") | |||
|
|||
private inline fun <T> mapExceptionsToNull(f : () -> T) : T? { | |||
return try { f()} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatting: space before brace.
I recommend using IntelliJ IDEA auto-formatter, as it honors Kotlin's style guide
formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt
Outdated
Show resolved
Hide resolved
formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt
Outdated
Show resolved
Hide resolved
|
||
if (hasExponent) { | ||
val doubleAccumulator = accumulator.toDouble() * calculateExponent(exponentAccumulator, isExponentPositive) | ||
if(doubleAccumulator > Long.MAX_VALUE || doubleAccumulator < Long.MIN_VALUE) fail("Numeric value overflow") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fornatting: space after if
|
||
@Test | ||
fun testExponentDecodingTruncatedDecimal() = parametrizedTest { | ||
val decoded = Json.decodeFromString<SomeData>("""{ "count": -1E-1 }""", it) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just realized that it is incorrect to truncate such values. That's why the problem with 4.9E-324
happens in the first place - it is too small. We should not allow such values to be parsed to Long
, or any value with non-zero fractional part after applying exponent. I think the easiest way to check it would be with doubleAccumulator.floor() == doubleAccumulator
, as in here:
kotlinx.serialization/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt
Line 154 in e2092a4
val canBeConverted = number.isFinite() && floor(number) == number |
…mentSerializers.kt Co-authored-by: Leonid Startsev <[email protected]>
…mentSerializers.kt Co-authored-by: Leonid Startsev <[email protected]>
public val JsonPrimitive.intOrNull: Int? get() = content.toIntOrNull() | ||
public val JsonPrimitive.intOrNull: Int? | ||
get() = | ||
mapExceptionsToNull { StringJsonLexer(content).consumeNumericLiteral().toInt().toLong().toInt() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why there's .toInt().toLong().toInt()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why I did that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job, thank you!
Closes #2078