Provide string-to-number conversions in JS and common stdlib.

Relates to #KT-8286
#KT-4497 Fixed
This commit is contained in:
Ilya Gorbunov
2017-01-21 22:29:38 +03:00
parent 2cac6a9e7d
commit 4e5e14c9a0
6 changed files with 267 additions and 19 deletions
@@ -121,3 +121,83 @@ internal inline header fun String.nativeLastIndexOf(ch: Char, fromIndex: Int): I
header fun CharSequence.isBlank(): Boolean
header fun CharSequence.regionMatches(thisOffset: Int, other: CharSequence, otherOffset: Int, length: Int, ignoreCase: Boolean): Boolean
/**
* Parses the string as a signed [Byte] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toByte(): Byte
/**
* Parses the string as a signed [Byte] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toByte(radix: Int): Byte
/**
* Parses the string as a [Short] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toShort(): Short
/**
* Parses the string as a [Short] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toShort(radix: Int): Short
/**
* Parses the string as an [Int] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toInt(): Int
/**
* Parses the string as an [Int] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toInt(radix: Int): Int
/**
* Parses the string as a [Long] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toLong(): Long
/**
* Parses the string as a [Long] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toLong(radix: Int): Long
/**
* Parses the string as a [Double] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toDouble(): Double
/**
* Parses the string as a [Float] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
header fun String.toFloat(): Float
/**
* Parses the string as a [Double] number and returns the result
* or `null` if the string is not a valid representation of a number.
*/
header fun String.toDoubleOrNull(): Double?
/**
* Parses the string as a [Float] number and returns the result
* or `null` if the string is not a valid representation of a number.
*/
header fun String.toFloatOrNull(): Float?
@PublishedApi
internal header fun checkRadix(radix: Int): Int
internal header fun digitOf(char: Char, radix: Int): Int
@@ -1,6 +1,5 @@
@file:kotlin.jvm.JvmMultifileClass
@file:kotlin.jvm.JvmName("StringsKt")
@file:kotlin.jvm.JvmVersion
@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
package kotlin.text
@@ -9,6 +8,7 @@ package kotlin.text
* Returns a string representation of this [Byte] value in the specified [radix].
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun Byte.toString(radix: Int): String = this.toInt().toString(checkRadix(radix))
@@ -16,6 +16,7 @@ public inline fun Byte.toString(radix: Int): String = this.toInt().toString(chec
* Returns a string representation of this [Short] value in the specified [radix].
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun Short.toString(radix: Int): String = this.toInt().toString(checkRadix(radix))
@@ -23,6 +24,7 @@ public inline fun Short.toString(radix: Int): String = this.toInt().toString(che
* Returns a string representation of this [Int] value in the specified [radix].
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun Int.toString(radix: Int): String = java.lang.Integer.toString(this, checkRadix(radix))
@@ -30,12 +32,14 @@ public inline fun Int.toString(radix: Int): String = java.lang.Integer.toString(
* Returns a string representation of this [Long] value in the specified [radix].
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun Long.toString(radix: Int): String = java.lang.Long.toString(this, checkRadix(radix))
/**
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, and `false` otherwise.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toBoolean(): Boolean = java.lang.Boolean.parseBoolean(this)
@@ -43,6 +47,7 @@ public inline fun String.toBoolean(): Boolean = java.lang.Boolean.parseBoolean(t
* Parses the string as a signed [Byte] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toByte(): Byte = java.lang.Byte.parseByte(this)
@@ -51,6 +56,7 @@ public inline fun String.toByte(): Byte = java.lang.Byte.parseByte(this)
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toByte(radix: Int): Byte = java.lang.Byte.parseByte(this, checkRadix(radix))
@@ -59,6 +65,7 @@ public inline fun String.toByte(radix: Int): Byte = java.lang.Byte.parseByte(thi
* Parses the string as a [Short] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toShort(): Short = java.lang.Short.parseShort(this)
@@ -67,6 +74,7 @@ public inline fun String.toShort(): Short = java.lang.Short.parseShort(this)
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toShort(radix: Int): Short = java.lang.Short.parseShort(this, checkRadix(radix))
@@ -74,6 +82,7 @@ public inline fun String.toShort(radix: Int): Short = java.lang.Short.parseShort
* Parses the string as an [Int] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toInt(): Int = java.lang.Integer.parseInt(this)
@@ -82,6 +91,7 @@ public inline fun String.toInt(): Int = java.lang.Integer.parseInt(this)
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toInt(radix: Int): Int = java.lang.Integer.parseInt(this, checkRadix(radix))
@@ -89,6 +99,7 @@ public inline fun String.toInt(radix: Int): Int = java.lang.Integer.parseInt(thi
* Parses the string as a [Long] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toLong(): Long = java.lang.Long.parseLong(this)
@@ -97,6 +108,7 @@ public inline fun String.toLong(): Long = java.lang.Long.parseLong(this)
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toLong(radix: Int): Long = java.lang.Long.parseLong(this, checkRadix(radix))
@@ -104,6 +116,7 @@ public inline fun String.toLong(radix: Int): Long = java.lang.Long.parseLong(thi
* Parses the string as a [Float] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toFloat(): Float = java.lang.Float.parseFloat(this)
@@ -111,6 +124,7 @@ public inline fun String.toFloat(): Float = java.lang.Float.parseFloat(this)
* Parses the string as a [Double] number and returns the result.
* @throws NumberFormatException if the string is not a valid representation of a number.
*/
@kotlin.jvm.JvmVersion
@kotlin.internal.InlineOnly
public inline fun String.toDouble(): Double = java.lang.Double.parseDouble(this)
@@ -279,6 +293,7 @@ public fun String.toLongOrNull(radix: Int): Long? {
* or `null` if the string is not a valid representation of a number.
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
public fun String.toFloatOrNull(): Float? = screenFloatValue(this, java.lang.Float::parseFloat)
/**
@@ -286,11 +301,13 @@ public fun String.toFloatOrNull(): Float? = screenFloatValue(this, java.lang.Flo
* or `null` if the string is not a valid representation of a number.
*/
@SinceKotlin("1.1")
@kotlin.jvm.JvmVersion
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)`
*/
@kotlin.jvm.JvmVersion
private object ScreenFloatValueRegEx {
@JvmField val value = run {
val Digits = "(\\p{Digit}+)"
@@ -310,6 +327,7 @@ private object ScreenFloatValueRegEx {
}
}
@kotlin.jvm.JvmVersion
private inline fun <T> screenFloatValue(str: String, parse: (String) -> T): T? {
return try {
if (ScreenFloatValueRegEx.value.matches(str))
@@ -1,4 +1,3 @@
@file:kotlin.jvm.JvmVersion
package test.text
import kotlin.test.*
@@ -6,6 +5,7 @@ import org.junit.Test
class StringNumberConversionTest {
@kotlin.jvm.JvmVersion
@Test fun toBoolean() {
assertEquals(true, "true".toBoolean())
assertEquals(true, "True".toBoolean())
@@ -125,6 +125,7 @@ class StringNumberConversionTest {
}
}
@kotlin.jvm.JvmVersion
@Test fun toFloat() {
compareConversion(String::toFloat, String::toFloatOrNull) {
assertProduces("77.0", 77.0f)
@@ -135,7 +136,7 @@ class StringNumberConversionTest {
}
@Test fun toDouble() {
compareConversion(String::toDouble, String::toDoubleOrNull) {
compareConversion(String::toDouble, String::toDoubleOrNull, ::doubleTotalOrderEquals) {
assertProduces("-77", -77.0)
assertProduces("77.", 77.0)
assertProduces("77.0", 77.0)
@@ -147,15 +148,23 @@ class StringNumberConversionTest {
assertProduces("-NaN", -Double.NaN)
assertProduces("+Infinity", Double.POSITIVE_INFINITY)
assertProduces("0x77p1", (0x77 shl 1).toDouble())
assertProduces("0x.77P8", 0x77.toDouble())
assertFailsOrNull("7..7")
assertFailsOrNull("0x77e1")
assertFailsOrNull("007 not a number")
}
}
@kotlin.jvm.JvmVersion
@Test fun toHexDouble() {
compareConversion(String::toDouble, String::toDoubleOrNull, ::doubleTotalOrderEquals) {
assertProduces("0x77p1", (0x77 shl 1).toDouble())
assertProduces("0x.77P8", 0x77.toDouble())
assertFailsOrNull("0x77e1")
}
}
@kotlin.jvm.JvmVersion
@Test fun byteToStringWithRadix() {
assertEquals("7a", 0x7a.toByte().toString(16))
assertEquals("-80", Byte.MIN_VALUE.toString(radix = 16))
@@ -166,6 +175,7 @@ class StringNumberConversionTest {
assertFailsWith<IllegalArgumentException>("Expected to fail with radix 1") { 1.toByte().toString(radix = 1) }
}
@kotlin.jvm.JvmVersion
@Test fun shortToStringWithRadix() {
assertEquals("7FFF", 0x7FFF.toShort().toString(radix = 16).toUpperCase())
assertEquals("-8000", (-0x8000).toShort().toString(radix = 16))
@@ -175,6 +185,7 @@ class StringNumberConversionTest {
assertFailsWith<IllegalArgumentException>("Expected to fail with radix 1") { 1.toShort().toString(radix = 1) }
}
@kotlin.jvm.JvmVersion
@Test fun intToStringWithRadix() {
assertEquals("-ff", (-255).toString(radix = 16))
assertEquals("1100110", 102.toString(radix = 2))
@@ -184,6 +195,7 @@ class StringNumberConversionTest {
}
@kotlin.jvm.JvmVersion
@Test fun longToStringWithRadix() {
assertEquals("7f11223344556677", 0x7F11223344556677.toString(radix = 16))
assertEquals("hazelnut", 1356099454469L.toString(radix = 36))
@@ -197,8 +209,9 @@ class StringNumberConversionTest {
private fun <T : Any> compareConversion(convertOrFail: (String) -> T,
convertOrNull: (String) -> T?,
equality: (T, T?) -> Boolean = { a, b -> a == b },
assertions: ConversionContext<T>.() -> Unit) {
ConversionContext(convertOrFail, convertOrNull).assertions()
ConversionContext(convertOrFail, convertOrNull, equality).assertions()
}
@@ -210,10 +223,16 @@ private fun <T : Any> compareConversionWithRadix(convertOrFail: String.(Int) ->
private class ConversionContext<T: Any>(val convertOrFail: (String) -> T,
val convertOrNull: (String) -> T?) {
val convertOrNull: (String) -> T?,
val equality: (T, T?) -> Boolean) {
private fun assertEquals(expected: T, actual: T?, input: String, operation: String) {
assertTrue(equality(expected, actual), "Expected $operation('$input') to produce $expected but was $actual")
}
fun assertProduces(input: String, output: T) {
assertEquals(output, convertOrFail(input.removeLeadingPlusOnJava6()))
assertEquals(output, convertOrNull(input))
assertEquals(output, convertOrFail(input.removeLeadingPlusOnJava6()), input, "convertOrFail")
assertEquals(output, convertOrNull(input), input, "convertOrNull")
}
fun assertFailsOrNull(input: String) {
@@ -222,22 +241,27 @@ private class ConversionContext<T: Any>(val convertOrFail: (String) -> T,
}
}
private class ConversionWithRadixContext<T: Any>(val convertOrFail: String.(Int) -> T,
val convertOrNull: String.(Int) -> T?) {
private class ConversionWithRadixContext<T: Any>(val convertOrFail: (String, Int) -> T,
val convertOrNull: (String, Int) -> T?) {
fun assertProduces(radix: Int, input: String, output: T) {
assertEquals(output, input.removeLeadingPlusOnJava6().convertOrFail(radix))
assertEquals(output, input.convertOrNull(radix))
assertEquals(output, convertOrFail(input.removeLeadingPlusOnJava6(), radix))
assertEquals(output, convertOrNull(input, radix))
}
fun assertFailsOrNull(radix: Int, input: String) {
assertFailsWith<NumberFormatException>("Expected to fail on input \"$input\" with radix $radix",
{ input.convertOrFail(radix) })
{ convertOrFail(input, radix) })
assertNull(input.convertOrNull(radix), message = "On input \"$input\" with radix $radix")
assertNull(convertOrNull(input, radix), message = "On input \"$input\" with radix $radix")
}
}
@kotlin.jvm.JvmVersion
private val isJava6 = System.getProperty("java.version").startsWith("1.6.")
@kotlin.jvm.JvmVersion
private fun String.removeLeadingPlusOnJava6(): String =
if (isJava6) removePrefix("+") else this
if (isJava6) removePrefix("+") else this
@kotlin.jvm.JvmVersion
private fun doubleTotalOrderEquals(a: Any?, b: Any?) = a == b