diff --git a/js/js.libraries/src/core/numberConversions.kt b/js/js.libraries/src/core/numberConversions.kt new file mode 100644 index 00000000000..00dcd45d65f --- /dev/null +++ b/js/js.libraries/src/core/numberConversions.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kotlin.text + + + +/** + * 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. + */ +public fun String.toByte(): Byte = toByteOrNull() ?: numberFormatError(this) + +/** + * 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. + */ +public fun String.toByte(radix: Int): Byte = toByteOrNull(radix) ?: numberFormatError(this) + + +/** + * Parses the string as a [Short] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public fun String.toShort(): Short = toShortOrNull() ?: numberFormatError(this) + +/** + * Parses the string as a [Short] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public fun String.toShort(radix: Int): Short = toShortOrNull(radix) ?: numberFormatError(this) + +/** + * Parses the string as an [Int] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public fun String.toInt(): Int = toIntOrNull() ?: numberFormatError(this) + +/** + * Parses the string as an [Int] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public fun String.toInt(radix: Int): Int = toIntOrNull(radix) ?: numberFormatError(this) + +/** + * Parses the string as a [Long] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public fun String.toLong(): Long = toLongOrNull() ?: numberFormatError(this) + +/** + * Parses the string as a [Long] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public fun String.toLong(radix: Int): Long = toLongOrNull(radix) ?: numberFormatError(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. + */ +public fun String.toDouble(): Double = (+(this.asDynamic())).unsafeCast().also { + if (it.isNaN() && !this.isNaN()) + numberFormatError(this) +} + +/** + * Parses the string as a [Float] number and returns the result. + * @throws NumberFormatException if the string is not a valid representation of a number. + */ +public inline fun String.toFloat(): Float = toDouble().unsafeCast() + +/** + * Parses the string as a [Double] number and returns the result + * or `null` if the string is not a valid representation of a number. + */ +public fun String.toDoubleOrNull(): Double? = (+(this.asDynamic())).unsafeCast().takeIf { + !(it.isNaN() && !this.isNaN()) +} + +/** + * Parses the string as a [Float] number and returns the result + * or `null` if the string is not a valid representation of a number. + */ +public inline fun String.toFloatOrNull(): Float? = toDoubleOrNull().unsafeCast() + + +private fun String.isNaN(): Boolean = when(this.toLowerCase()) { + "nan", "+nan", "-nan" -> true + else -> false +} + +/** + * Checks whether the given [radix] is valid radix for string to number and number to string conversion. + */ +@PublishedApi +internal fun checkRadix(radix: Int): Int { + if(radix !in 2..36) { + throw IllegalArgumentException("radix $radix was not in valid range 2..36") + } + return radix +} + +internal fun digitOf(char: Char, radix: Int): Int = when { + char >= '0' && char <= '9' -> char - '0' + char >= 'A' && char <= 'Z' -> char - 'A' + 10 + char >= 'a' && char <= 'z' -> char - 'a' + 10 + else -> -1 +}.let { if (it >= radix) -1 else it } + +private fun numberFormatError(input: String): Nothing = throw NumberFormatException("Invalid number format: '$input'") \ No newline at end of file diff --git a/js/js.libraries/test/core/assertTypeEquals.kt b/js/js.libraries/test/core/testUtils.kt similarity index 82% rename from js/js.libraries/test/core/assertTypeEquals.kt rename to js/js.libraries/test/core/testUtils.kt index 1d9cbdc17e9..016af2ec2c9 100644 --- a/js/js.libraries/test/core/assertTypeEquals.kt +++ b/js/js.libraries/test/core/testUtils.kt @@ -19,4 +19,7 @@ package kotlin.test public fun assertTypeEquals(expected: Any?, actual: Any?) { //TODO: find analogue //assertEquals(expected?.javaClass, actual?.javaClass) -} \ No newline at end of file +} + +internal inline fun String.removeLeadingPlusOnJava6(): String = this +internal fun doubleTotalOrderEquals(a: Double?, b: Double?) = a == b || (a != a && b != b) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StdLibTestToJSTest.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StdLibTestToJSTest.java index 25d4a46f8b1..dbbffe52d21 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StdLibTestToJSTest.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/StdLibTestToJSTest.java @@ -31,7 +31,7 @@ public class StdLibTestToJSTest extends StdLibQUnitTestSupport { "collections/IteratorsTest.kt", "collections/CollectionBehaviors.kt", "collections/ComparisonDSL.kt", - "../../../js/js.libraries/test/core/assertTypeEquals.kt", + "../../../js/js.libraries/test/core/testUtils.kt", "text/StringTest.kt", "OrderingTest.kt", "collections/SequenceTest.kt", diff --git a/libraries/stdlib/common/src/kotlin/TextH.kt b/libraries/stdlib/common/src/kotlin/TextH.kt index c827b537ec6..c3f67cb0411 100644 --- a/libraries/stdlib/common/src/kotlin/TextH.kt +++ b/libraries/stdlib/common/src/kotlin/TextH.kt @@ -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 \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/text/StringNumberConversions.kt b/libraries/stdlib/src/kotlin/text/StringNumberConversions.kt index b28c0992866..e8b781ba498 100644 --- a/libraries/stdlib/src/kotlin/text/StringNumberConversions.kt +++ b/libraries/stdlib/src/kotlin/text/StringNumberConversions.kt @@ -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 screenFloatValue(str: String, parse: (String) -> T): T? { return try { if (ScreenFloatValueRegEx.value.matches(str)) diff --git a/libraries/stdlib/test/text/StringNumberConversionTest.kt b/libraries/stdlib/test/text/StringNumberConversionTest.kt index 4e5cb7a8362..53f1f35709a 100644 --- a/libraries/stdlib/test/text/StringNumberConversionTest.kt +++ b/libraries/stdlib/test/text/StringNumberConversionTest.kt @@ -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("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("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 compareConversion(convertOrFail: (String) -> T, convertOrNull: (String) -> T?, + equality: (T, T?) -> Boolean = { a, b -> a == b }, assertions: ConversionContext.() -> Unit) { - ConversionContext(convertOrFail, convertOrNull).assertions() + ConversionContext(convertOrFail, convertOrNull, equality).assertions() } @@ -210,10 +223,16 @@ private fun compareConversionWithRadix(convertOrFail: String.(Int) -> private class ConversionContext(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(val convertOrFail: (String) -> T, } } -private class ConversionWithRadixContext(val convertOrFail: String.(Int) -> T, - val convertOrNull: String.(Int) -> T?) { +private class ConversionWithRadixContext(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("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 \ No newline at end of file + if (isJava6) removePrefix("+") else this + +@kotlin.jvm.JvmVersion +private fun doubleTotalOrderEquals(a: Any?, b: Any?) = a == b