From ce9c981e265b1a3e211b97aef84094d2e8ae65a7 Mon Sep 17 00:00:00 2001 From: voddan Date: Sun, 20 Mar 2016 20:28:27 +0300 Subject: [PATCH] Implemented `String.toIntOrNull` , `String.toLongOrNull`. Rewrote `String.toShortOrNull` and `String.toByteOrNull` by delegating to `String.toIntOrNull`. Added a regEx check into `String.toDoubleOrNull` to prevent throwing exceptions mostly. #KT-7930 --- .../src/kotlin/text/ParsePrimitivesJVM.kt | 162 +++++++++++++++--- 1 file changed, 138 insertions(+), 24 deletions(-) diff --git a/libraries/stdlib/src/kotlin/text/ParsePrimitivesJVM.kt b/libraries/stdlib/src/kotlin/text/ParsePrimitivesJVM.kt index 79de9d5c5f1..adde1ec869e 100644 --- a/libraries/stdlib/src/kotlin/text/ParsePrimitivesJVM.kt +++ b/libraries/stdlib/src/kotlin/text/ParsePrimitivesJVM.kt @@ -60,53 +60,167 @@ public inline fun String.toDouble(): Double = java.lang.Double.parseDouble(this) * Parses the string as a signed [Byte] number and returns the result * or `null` if the string is not a valid representation of a number. */ -@kotlin.internal.InlineOnly -public inline fun String.toByteOrNull(): Byte? = try { - java.lang.Byte.parseByte(this) -} catch(e: NumberFormatException) { null } +public fun String.toByteOrNull(): Byte? { + val int = this.toIntOrNull() ?: return null + if (int < Byte.MIN_VALUE || int > Byte.MAX_VALUE) return null + return int.toByte() +} /** * Parses the string as a [Short] number and returns the result * or `null` if the string is not a valid representation of a number. */ -@kotlin.internal.InlineOnly -public inline fun String.toShortOrNull(): Short? = try { - java.lang.Short.parseShort(this) -} catch(e: NumberFormatException) { null } +public fun String.toShortOrNull(): Short? { + val int = this.toIntOrNull() ?: return null + if (int < Short.MIN_VALUE || int > Short.MAX_VALUE) return null + return int.toShort() +} /** * Parses the string as an [Int] number and returns the result * or `null` if the string is not a valid representation of a number. */ -@kotlin.internal.InlineOnly -public inline fun String.toIntOrNull(): Int? = try { - java.lang.Integer.parseInt(this) -} catch(e: NumberFormatException) { null } +public fun String.toIntOrNull(): Int? { + val length = this.length + if (length == 0) return null + + val start: Int + val isNegative: Boolean + val limit: Int + + val firstChar = this[0] + if (firstChar < '0') { // Possible leading sign + if (length == 1) return null // non-digit (possible sign) only, no digits after + + start = 1 + + if (firstChar == '-') { + isNegative = true + limit = Int.MIN_VALUE + } else if (firstChar == '+') { + isNegative = false + limit = -Int.MAX_VALUE + } else + return null + } else { + start = 0 + isNegative = false + limit = -Int.MAX_VALUE + } + + + val limitBeforeMul = limit / 10 + var result = 0 + for (i in start..(length - 1)) { + val digit = Character.digit(this[i], 10) + + if (digit < 0) return null + if (result < limitBeforeMul) return null + + result *= 10 + + if (result < limit + digit) return null + + result -= digit + } + + return if (isNegative) result else -result +} /** * Parses the string as a [Long] number and returns the result * or `null` if the string is not a valid representation of a number. */ -@kotlin.internal.InlineOnly -public inline fun String.toLongOrNull(): Long? = try { - java.lang.Long.parseLong(this) -} catch(e: NumberFormatException) { null } +public fun String.toLongOrNull(): Long? { + val length = this.length + if (length == 0) return null + + val start: Int + val isNegative: Boolean + val limit: Long + + val firstChar = this[0] + if (firstChar < '0') { // Possible leading sign + if (length == 1) return null // non-digit (possible sign) only, no digits after + + start = 1 + + if (firstChar == '-') { + isNegative = true + limit = Long.MIN_VALUE + } else if (firstChar == '+') { + isNegative = false + limit = -Long.MAX_VALUE + } else + return null + } else { + start = 0 + isNegative = false + limit = -Long.MAX_VALUE + } + + + val limitBeforeMul = limit / 10L + var result = 0L + for (i in start..(length - 1)) { + val digit = Character.digit(this[i], 10) + + if (digit < 0) return null + if (result < limitBeforeMul) return null + + result *= 10 + + if (result < limit + digit) return null + + result -= digit + } + + return if (isNegative) result else -result +} /** * Parses the string as a [Float] number and returns the result * or `null` if the string is not a valid representation of a number. */ -@kotlin.internal.InlineOnly -public inline fun String.toFloatOrNull(): Float? = try { - java.lang.Float.parseFloat(this) -} catch(e: NumberFormatException) { null } +public fun String.toFloatOrNull(): Float? = screenFloatValue(this, java.lang.Float::parseFloat) /** * Parses the string as a [Double] number and returns the result * or `null` if the string is not a valid representation of a number. */ -@kotlin.internal.InlineOnly -public inline fun String.toDoubleOrNull(): Double? = try { - java.lang.Double.parseDouble(this) -} catch(e: NumberFormatException) { null } +public fun String.toDoubleOrNull(): Double? = screenFloatValue(this, java.lang.Double::parseDouble) + +/** + * Recommended floating point number validation RegEx from the javadoc of `java.lang.Double.valueOf(String)` + */ +private object ScreenFloatValueRegEx { + @JvmField val value = run { + val Digits = "(\\p{Digit}+)" + val HexDigits = "(\\p{XDigit}+)" + val Exp = "[eE][+-]?$Digits" + + val HexString = "(0[xX]$HexDigits(\\.)?)|" + // 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt + "(0[xX]$HexDigits?(\\.)$HexDigits)" // 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt + + val Number = "($Digits(\\.)?($Digits?)($Exp)?)|" + // Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt + "(\\.($Digits)($Exp)?)|" + // . Digits ExponentPart_opt FloatTypeSuffix_opt + "(($HexString)[pP][+-]?$Digits)" // HexSignificand BinaryExponent + + val fpRegex = "[\\x00-\\x20]*[+-]?(NaN|Infinity|(($Number)[fFdD]?))[\\x00-\\x20]*" + + Regex(fpRegex) + } +} + +private inline fun screenFloatValue(str: String, parse: (String) -> T): T? { + // they say the RegEx screens all invalid cases, but who knows.. + return try { + if (ScreenFloatValueRegEx.value.matches(str)) + parse(str) + else + null + } catch(e: NumberFormatException) { + null + } +}