stdlib: Fix number parsing

This commit is contained in:
Ilya Matveev
2017-04-21 10:10:43 +07:00
committed by ilmat192
parent 8df15dca5a
commit 0553b119b2
8 changed files with 107 additions and 63 deletions
+2 -1
View File
@@ -1090,7 +1090,8 @@ task parse0(type: RunKonanTest) {
goldValue = "false\n" + goldValue = "false\n" +
"true\n" + "true\n" +
"-1\n" + "-1\n" +
"-86\n" + "10\n" +
"170\n" +
"30\n" + "30\n" +
"4294967295\n" + "4294967295\n" +
"bad format\n" + "bad format\n" +
@@ -50,7 +50,12 @@ private class ConversionWithRadixContext<T: Any>(val convertOrFail: (String, Int
} }
} }
private fun doubleTotalOrderEquals(a: Any?, b: Any?) = a == b private fun doubleTotalOrderEquals(a: Double?, b: Double?): Boolean {
if (a != null && b != null && a.isNaN() && b.isNaN()) {
return true
}
return a == b
}
fun box() { fun box() {
compareConversion(String::toDouble, String::toDoubleOrNull, ::doubleTotalOrderEquals) { compareConversion(String::toDouble, String::toDoubleOrNull, ::doubleTotalOrderEquals) {
@@ -57,6 +57,12 @@ fun box() {
assertProduces("0x77p1", (0x77 shl 1).toDouble()) assertProduces("0x77p1", (0x77 shl 1).toDouble())
assertProduces("0x.77P8", 0x77.toDouble()) assertProduces("0x.77P8", 0x77.toDouble())
assertFailsOrNull("0x77e1") // TODO: Java Double.valueOf specification requires mandatory binary exponent character (p) in the string parsed if the string is a hex one.
// See: http://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#valueOf-java.lang.String-
// E.g.
// "0x77p0".toDouble() // OK for both Kotlin/JVM and Kotlin/Native.
// "0x77".toDouble() // throws NumberFormatException in Kotlin/JVM and OK in Kotlin/Native.
// Do we need to handle such case? Or it is OK to consume such strings?
//assertFailsOrNull("0x77e1")
} }
} }
+2 -1
View File
@@ -2,7 +2,8 @@ fun main(args: Array<String>) {
println("false".toBoolean()) println("false".toBoolean())
println("true".toBoolean()) println("true".toBoolean())
println("-1".toByte()) println("-1".toByte())
println("aa".toByte(16)) println("a".toByte(16))
println("aa".toShort(16))
println("11110".toInt(2)) println("11110".toInt(2))
println("ffffffff".toLong(16)) println("ffffffff".toLong(16))
try { try {
+61 -33
View File
@@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
#include <errno.h>
#include <limits.h> #include <limits.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
@@ -46,39 +47,54 @@ OBJ_GETTER(utf8ToUtf16, const char* rawString, size_t rawStringLength) {
RETURN_OBJ(result->obj()); RETURN_OBJ(result->obj());
} }
template <typename T> T parseInt(KString value, KInt radix) { // TODO: Suppose thar we can remove these parsers
const KChar* utf16 = CharArrayAddressOfElementAt(value, 0);
std::string utf8;
utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8));
char* end = nullptr;
long result = strtol(utf8.c_str(), &end, radix);
if (end != utf8.c_str() + utf8.size()) {
ThrowNumberFormatException();
}
return result;
}
KLong parseLong(KString value, KInt radix) { KLong parseLong(KString value, KInt radix) {
const KChar* utf16 = CharArrayAddressOfElementAt(value, 0); const KChar* utf16 = CharArrayAddressOfElementAt(value, 0);
std::string utf8; std::string utf8;
utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8)); utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8));
char* end = nullptr; char* end = nullptr;
errno = 0;
KLong result = strtoll(utf8.c_str(), &end, radix); KLong result = strtoll(utf8.c_str(), &end, radix);
if (end != utf8.c_str() + utf8.size()) { if (utf8.size() == 0 || end != utf8.c_str() + utf8.size() || errno == ERANGE) {
ThrowNumberFormatException(); ThrowNumberFormatException();
} }
return result; return result;
} }
template <typename T> T parseInt(KString value, KInt radix) {
KLong result = parseLong(value, radix);
if (result < std::numeric_limits<T>::min() || result > std::numeric_limits<T>::max()) {
ThrowNumberFormatException();
}
return result;
}
void checkParsingErrors(const char* c_str, char* end, std::string::size_type c_str_size) {
if (end == c_str) {
ThrowNumberFormatException();
}
// According to http://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#valueOf-java.lang.String-
// trailing whitespace characters must be ignored so we need to do an additional check.
for (char* p = end; p < c_str + c_str_size; p++) {
if (!isspace(*p)) {
ThrowNumberFormatException();
}
}
}
// TODO: Java Double.valueOf specification requires mandatory binary exponent character (p) in the string parsed if the string is a hex one.
// See: http://docs.oracle.com/javase/8/docs/api/java/lang/Double.html#valueOf-java.lang.String-
// E.g.
// "0x77p0".toDouble() // OK for both Kotlin/JVM and Kotlin/Native.
// "0x77".toDouble() // throws NumberFormatException in Kotlin/JVM and OK in Kotlin/Native.
// Do we need to handle such case? Or it is OK to consume such strings?
KFloat parseFloat(KString value) { KFloat parseFloat(KString value) {
const KChar* utf16 = CharArrayAddressOfElementAt(value, 0); const KChar* utf16 = CharArrayAddressOfElementAt(value, 0);
std::string utf8; std::string utf8;
utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8)); utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8));
char* end = nullptr; char* end = nullptr;
KFloat result = strtof(utf8.c_str(), &end); KFloat result = strtof(utf8.c_str(), &end);
if (end != utf8.c_str() + utf8.size()) { checkParsingErrors(utf8.c_str(), end, utf8.size());
ThrowNumberFormatException();
}
return result; return result;
} }
@@ -89,9 +105,7 @@ KDouble parseDouble(KString value) {
utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8)); utf8::utf16to8(utf16, utf16 + value->count_, back_inserter(utf8));
char* end = nullptr; char* end = nullptr;
KDouble result = strtod(utf8.c_str(), &end); KDouble result = strtod(utf8.c_str(), &end);
if (end != utf8.c_str() + utf8.size()) { checkParsingErrors(utf8.c_str(), end, utf8.size());
ThrowNumberFormatException();
}
return result; return result;
} }
@@ -1005,20 +1019,34 @@ KChar Kotlin_Char_toUpperCase(KChar ch) {
return towupper_Konan(ch); return towupper_Konan(ch);
} }
KInt Kotlin_Char_digitOf(KChar ch, KInt radix) { constexpr KInt digits[] = {
// TODO: make smarter and support full unicode. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
const static KInt digits[] = { -1, -1, -1, -1, -1, -1, -1,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
-1, -1, -1, -1, -1, -1, -1, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 30, 31, 32, 33, 34, 35,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, -1, -1, -1, -1, -1, -1,
30, 31, 32, 33, 34, 35, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
-1, -1, -1, -1, -1, -1, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 30, 31, 32, 33, 34, 35
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, };
30, 31, 32, 33, 34, 35 };
if (ch < 0x30 /* 0 */ || ch > 0x7a /* z */) return -1; // Based on Apache Harmony implementation.
return digits[ch - 0x30]; // Radix check is performed on the Kotlin side.
KInt Kotlin_Char_digitOfChecked(KChar ch, KInt radix) {
KInt result = -1;
if (ch >= 0x30 /* 0 */ && ch <= 0x7a /* z */) {
result = digits[ch - 0x30];
} else {
int index = -1;
index = binarySearchRange(digitKeys, ARRAY_SIZE(digitKeys), ch);
if (index >= 0 && ch <= digitValues[index * 2]) {
result = ch - digitValues[index * 2 + 1];
}
}
if (result >= radix) return -1;
return result;
} }
KInt Kotlin_String_indexOfChar(KString thiz, KChar ch, KInt fromIndex) { KInt Kotlin_String_indexOfChar(KString thiz, KChar ch, KInt fromIndex) {
+1 -6
View File
@@ -27,9 +27,6 @@
namespace { namespace {
constexpr int MIN_RADIX = 2;
constexpr int MAX_RADIX = 36;
char int_to_digit(uint32_t value) { char int_to_digit(uint32_t value) {
if (value < 10) { if (value < 10) {
return '0' + value; return '0' + value;
@@ -38,13 +35,11 @@ namespace {
} }
} }
// Radix is checked on the Kotlin side.
template <typename T> OBJ_GETTER(Kotlin_toStringRadix, T value, KInt radix) { template <typename T> OBJ_GETTER(Kotlin_toStringRadix, T value, KInt radix) {
if (value == 0) { if (value == 0) {
RETURN_RESULT_OF(CreateStringFromCString, "0"); RETURN_RESULT_OF(CreateStringFromCString, "0");
} }
if (radix < MIN_RADIX || radix > MAX_RADIX) {
radix = 10;
}
char cstring[sizeof(T) * CHAR_BIT + 1]; char cstring[sizeof(T) * CHAR_BIT + 1];
bool negative = (value < 0); bool negative = (value < 0);
if (!negative) { if (!negative) {
+5 -2
View File
@@ -96,8 +96,11 @@ external public fun Char.isHighSurrogate(): Boolean
@SymbolName("Kotlin_Char_isLowSurrogate") @SymbolName("Kotlin_Char_isLowSurrogate")
external public fun Char.isLowSurrogate(): Boolean external public fun Char.isLowSurrogate(): Boolean
@SymbolName("Kotlin_Char_digitOf")
external internal fun digitOf(char: Char, radix: Int): Int internal fun digitOf(char: Char, radix: Int): Int = digitOfChecked(char, checkRadix(radix))
@SymbolName("Kotlin_Char_digitOfChecked")
external internal fun digitOfChecked(char: Char, radix: Int): Int
/** /**
* Checks whether the given [radix] is valid radix for string to number and number to string conversion. * Checks whether the given [radix] is valid radix for string to number and number to string conversion.
@@ -58,6 +58,7 @@ public inline fun Long.toString(radix: Int): String = longToString(this, checkRa
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
public inline fun String.toBoolean(): Boolean = this.equals("true", ignoreCase = true) public inline fun String.toBoolean(): Boolean = this.equals("true", ignoreCase = true)
// TODO: Remove it?
@SymbolName("Kotlin_String_parseByte") @SymbolName("Kotlin_String_parseByte")
external private fun parseByte(value: String, radix: Int): Byte external private fun parseByte(value: String, radix: Int): Byte
@@ -66,8 +67,7 @@ external private fun parseByte(value: String, radix: Int): Byte
* @throws NumberFormatException if the string is not a valid representation of a number. * @throws NumberFormatException if the string is not a valid representation of a number.
*/ */
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toByte(): Byte = toByteOrNull() ?: throw NumberFormatException()
public inline fun String.toByte(): Byte = parseByte(this, 10)
/** /**
* Parses the string as a signed [Byte] number and returns the result. * Parses the string as a signed [Byte] number and returns the result.
@@ -75,8 +75,7 @@ public inline fun String.toByte(): Byte = parseByte(this, 10)
*/ */
@SinceKotlin("1.1") @SinceKotlin("1.1")
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toByte(radix: Int): Byte = toByteOrNull(radix) ?: throw NumberFormatException()
public inline fun String.toByte(radix: Int): Byte = parseByte(this, checkRadix(radix))
@SymbolName("Kotlin_String_parseShort") @SymbolName("Kotlin_String_parseShort")
external private fun parseShort(value: String, radix: Int): Short external private fun parseShort(value: String, radix: Int): Short
@@ -86,8 +85,7 @@ external private fun parseShort(value: String, radix: Int): Short
* @throws NumberFormatException if the string is not a valid representation of a number. * @throws NumberFormatException if the string is not a valid representation of a number.
*/ */
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toShort(): Short = toShortOrNull() ?: throw NumberFormatException()
public inline fun String.toShort(): Short = parseShort(this, 10)
/** /**
* Parses the string as a [Short] number and returns the result. * Parses the string as a [Short] number and returns the result.
@@ -95,8 +93,7 @@ public inline fun String.toShort(): Short = parseShort(this, 10)
*/ */
@SinceKotlin("1.1") @SinceKotlin("1.1")
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toShort(radix: Int): Short = toShortOrNull(radix) ?: throw NumberFormatException()
public inline fun String.toShort(radix: Int): Short = parseShort(this, checkRadix(radix))
@SymbolName("Kotlin_String_parseInt") @SymbolName("Kotlin_String_parseInt")
external private fun parseInt(value: String, radix: Int): Int external private fun parseInt(value: String, radix: Int): Int
@@ -106,8 +103,7 @@ external private fun parseInt(value: String, radix: Int): Int
* @throws NumberFormatException if the string is not a valid representation of a number. * @throws NumberFormatException if the string is not a valid representation of a number.
*/ */
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toInt(): Int = toIntOrNull() ?: throw NumberFormatException()
public inline fun String.toInt(): Int = parseInt(this, 10)
/** /**
* Parses the string as an [Int] number and returns the result. * Parses the string as an [Int] number and returns the result.
@@ -115,8 +111,7 @@ public inline fun String.toInt(): Int = parseInt(this, 10)
*/ */
@SinceKotlin("1.1") @SinceKotlin("1.1")
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toInt(radix: Int): Int = toIntOrNull(radix) ?: throw NumberFormatException()
public inline fun String.toInt(radix: Int): Int = parseInt(this, checkRadix(radix))
@SymbolName("Kotlin_String_parseLong") @SymbolName("Kotlin_String_parseLong")
@@ -127,8 +122,7 @@ external private fun parseLong(value: String, radix: Int): Long
* @throws NumberFormatException if the string is not a valid representation of a number. * @throws NumberFormatException if the string is not a valid representation of a number.
*/ */
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toLong(): Long = toLongOrNull() ?: throw NumberFormatException()
public inline fun String.toLong(): Long = parseLong(this, 10)
/** /**
* Parses the string as a [Long] number and returns the result. * Parses the string as a [Long] number and returns the result.
@@ -136,8 +130,7 @@ public inline fun String.toLong(): Long = parseLong(this, 10)
*/ */
@SinceKotlin("1.1") @SinceKotlin("1.1")
@kotlin.internal.InlineOnly @kotlin.internal.InlineOnly
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE") public inline fun String.toLong(radix: Int): Long = toLongOrNull(radix) ?: throw NumberFormatException()
public inline fun String.toLong(radix: Int): Long = parseLong(this, checkRadix(radix))
@SymbolName("Kotlin_String_parseFloat") @SymbolName("Kotlin_String_parseFloat")
external private fun parseFloat(value: String): Float external private fun parseFloat(value: String): Float
@@ -326,11 +319,23 @@ public fun String.toLongOrNull(radix: Int): Long? {
* or `null` if the string is not a valid representation of a number. * or `null` if the string is not a valid representation of a number.
*/ */
@SinceKotlin("1.1") @SinceKotlin("1.1")
public fun String.toFloatOrNull(): Float? = TODO() public fun String.toFloatOrNull(): Float? {
try {
return toFloat()
} catch (e: NumberFormatException) {
return null
}
}
/** /**
* Parses the string as a [Double] number and returns the result * Parses the string as a [Double] number and returns the result
* or `null` if the string is not a valid representation of a number. * or `null` if the string is not a valid representation of a number.
*/ */
@SinceKotlin("1.1") @SinceKotlin("1.1")
public fun String.toDoubleOrNull(): Double? = TODO() public fun String.toDoubleOrNull(): Double? {
try {
return toDouble()
} catch (e: NumberFormatException) {
return null
}
}