Skip to content

Commit

Permalink
Support for "inf" and "nan" values (#180)
Browse files Browse the repository at this point in the history
### What's done:
- Support for decoding of Infinity and Not a number values:
- "-nan","+nan", "nan", "-NaN", "+NaN", "NaN"
- "+inf", "inf", "-inf"
- tests for covering these corner cases and empty values
  • Loading branch information
orchestr7 authored Jan 7, 2023
1 parent 2d3c02c commit 60f1697
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,24 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() {
try {
(value.content as String).convertSpecialCharacters(keyValue.lineNo).single()
} catch (ex: NoSuchElementException) {
throw IllegalTypeException("Empty value is not allowed for type [Char], " +
"please check the value: [${value.content}] or use [String] type for deserialization of " +
"[${keyValue.key}] instead", keyValue.lineNo)
throw IllegalTypeException(
"Empty value is not allowed for type [Char], " +
"please check the value: [${value.content}] or use [String] type for deserialization of " +
"[${keyValue.key}] instead", keyValue.lineNo
)
} catch (ex: IllegalArgumentException) {
throw IllegalTypeException("[Char] type should be used for decoding of single character, but " +
"received multiple characters instead: [${value.content}]. " +
"If you really want to decode multiple chars, use [String] instead.", keyValue.lineNo)
throw IllegalTypeException(
"[Char] type should be used for decoding of single character, but " +
"received multiple characters instead: [${value.content}]. " +
"If you really want to decode multiple chars, use [String] instead.", keyValue.lineNo
)
}
// to avoid confusion, we prohibit basic strings with double quotes for decoding to a Char type
is TomlBasicString -> throw IllegalTypeException("Double quotes were used in the input for deserialization " +
"of [Char]. Use [String] type or single quotes ('') instead for: [${value.content}]", keyValue.lineNo)
is TomlBasicString -> throw IllegalTypeException(
"Double quotes were used in the input for deserialization " +
"of [Char]. Use [String] type or single quotes ('') instead for: [${value.content}]",
keyValue.lineNo
)
// all other toml tree types are not supported
else -> throw IllegalTypeException(
"Cannot decode the key [${keyValue.key.last()}] with the value [${keyValue.value.content}]" +
Expand Down Expand Up @@ -116,11 +123,17 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() {
}
}

private inline fun <reified T> decodeFloatingPoint(content: Double, lineNo: Int): T = when (T::class) {
Float::class -> validateAndConvertFloatingPoint(content, lineNo, FLOAT) { num: Double -> num.toFloat() as T }
Double::class -> validateAndConvertFloatingPoint(content, lineNo, DOUBLE) { num: Double -> num as T }
else -> invalidType(T::class.toString(), "Signed Type")
}
private inline fun <reified T> decodeFloatingPoint(content: Double, lineNo: Int): T =
when (T::class) {
Float::class -> validateAndConvertFloatingPoint(
content,
lineNo,
FLOAT
) { num: Double -> num.toFloat() as T }

Double::class -> validateAndConvertFloatingPoint(content, lineNo, DOUBLE) { num: Double -> num as T }
else -> invalidType(T::class.toString(), "Signed Type")
}

/**
* ktoml parser treats all integer literals as Long and all floating-point literals as Double,
Expand All @@ -131,10 +144,10 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() {
lineNo: Int,
limits: FloatingPointLimitsEnum,
conversion: (Double) -> T,
): T = if (content in limits.min..limits.max) {
conversion(content)
} else {
throw IllegalTypeException(
): T = when {
content.isInfinite() || content.isNaN() -> conversion(content)
content in limits.min..limits.max -> conversion(content)
else -> throw IllegalTypeException(
"The floating point literal, that you have provided is <$content>, " +
"but the type for deserialization is <${T::class}>. You will get an overflow, " +
"so we advise you to check the data or use other type for deserialization (Long, for example)",
Expand All @@ -159,6 +172,7 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() {
"Deserialized floating-point number should have a dot: <$content.0>",
lineNo
)

else -> invalidType(T::class.toString(), "Signed Type")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ public fun String.splitKeyValue(lineNo: Int, config: TomlInputConfig = TomlInput
* @return parsed TomlNode value
*/
public fun String.parseValue(lineNo: Int, config: TomlInputConfig): TomlValue = when (this) {
// ===== special values
"+inf", "inf" -> TomlDouble(Double.POSITIVE_INFINITY)
"-inf" -> TomlDouble(Double.NEGATIVE_INFINITY)
"-nan", "+nan", "nan", "-NaN", "+NaN", "NaN" -> TomlDouble(Double.NaN)
// ===== null values
"null", "nil", "NULL", "NIL", "" -> if (config.allowNullValues) {
TomlNull(lineNo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ internal constructor(
) : TomlValue() {
public constructor(content: String, lineNo: Int) : this(content.toDouble())

public constructor(content: Double, lineNo: Int) : this(content)

override fun write(
emitter: TomlEmitter,
config: TomlOutputConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ class PrimitivesDecoderTest {
test(-1f, "-1.0")
test(-128f, "-128.0")
test(127f, "127.0")
test(Float.NEGATIVE_INFINITY, "-inf")
test(Float.POSITIVE_INFINITY, "+inf")
test(Float.NaN, "nan")
}

@Test
Expand Down Expand Up @@ -247,12 +250,14 @@ class PrimitivesDecoderTest {

assertEquals(expected, data.value)
}

test(0.0, "0.0")
test(1.0, "1.0")
test(-1.0, "-1.0")
test(-128.0, "-128.0")
test(127.0, "127.0")
test(Double.NEGATIVE_INFINITY, "-inf")
test(Double.POSITIVE_INFINITY, "+inf")
test(Double.NaN, "nan")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.TomlInputConfig
import com.akuleshov7.ktoml.exceptions.ParseException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.Serializable
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class StringDecoderTest {
@Serializable
data class Literals(
val winpath: String,
val winpath: String?,
val winpath2: String,
val quoted: String,
val regex: String,
Expand Down Expand Up @@ -126,4 +129,33 @@ class StringDecoderTest {
decoded
)
}

@Test
fun emptyStringTest() {
var test = """
winpath =
winpath2 = ''
quoted = ""
regex = ''
"""

val decoded = Toml().decodeFromString<Literals>(test)
assertEquals(
Literals(
null,
"",
"",
""
),
decoded
)

test = """
winpath =
""".trimIndent()

assertFailsWith<ParseException> {
Toml(TomlInputConfig(allowEmptyValues = false)).decodeFromString<Literals>(test)
}
}
}

0 comments on commit 60f1697

Please sign in to comment.