From 3f6e2be6876b01ba66d245d75fd4d2bcf2770d08 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Tue, 6 Jul 2021 04:21:42 +0300 Subject: [PATCH] Duration: do not use scientific format for large values The largest duration value formatted in ns with maximal decimals would fit in 40 chars. --- .../runtime/src/main/cpp/ToString.cpp | 7 +------ .../kotlin/kotlin/time/formatToDecimals.kt | 11 +---------- .../js/src/kotlin/time/formatToDecimals.kt | 14 +++++++++----- .../jvm/src/kotlin/time/formatToDecimals.kt | 16 ++-------------- libraries/stdlib/src/kotlin/time/Duration.kt | 13 ++++++------- libraries/stdlib/test/time/DurationTest.kt | 18 ++++++++++++++++-- .../stdlib/wasm/src/kotlin/time/Duration.kt | 5 ++--- 7 files changed, 37 insertions(+), 47 deletions(-) diff --git a/kotlin-native/runtime/src/main/cpp/ToString.cpp b/kotlin-native/runtime/src/main/cpp/ToString.cpp index 8123ccb6848..31dfa5e7fe8 100644 --- a/kotlin-native/runtime/src/main/cpp/ToString.cpp +++ b/kotlin-native/runtime/src/main/cpp/ToString.cpp @@ -108,15 +108,10 @@ OBJ_GETTER(Kotlin_Long_toStringRadix, KLong value, KInt radix) { } OBJ_GETTER(Kotlin_DurationValue_formatToExactDecimals, KDouble value, KInt decimals) { - char cstring[32]; + char cstring[40]; // log(2^62*1_000_000) + 2 (sign, decimal point) + 12 (max decimals) konan::snprintf(cstring, sizeof(cstring), "%.*f", decimals, value); RETURN_RESULT_OF(CreateStringFromCString, cstring) } -OBJ_GETTER(Kotlin_DurationValue_formatScientificImpl, KDouble value) { - char cstring[16]; - konan::snprintf(cstring, sizeof(cstring), "%.2e", value); - RETURN_RESULT_OF(CreateStringFromCString, cstring) -} } // extern "C" diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/time/formatToDecimals.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/time/formatToDecimals.kt index 23b2e24fe96..0ada5a7480f 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/time/formatToDecimals.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/time/formatToDecimals.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -13,12 +13,3 @@ internal actual external fun formatToExactDecimals(value: Double, decimals: Int) internal actual fun formatUpToDecimals(value: Double, decimals: Int): String { return formatToExactDecimals(value, decimals).trimEnd('0') } - -@GCUnsafeCall("Kotlin_DurationValue_formatScientificImpl") -internal external fun formatScientificImpl(value: Double): String - -internal actual fun formatScientific(value: Double): String { - val result = formatScientificImpl(value) - val expIndex = result.indexOf("e+0") - return if (expIndex < 0) result else result.removeRange(expIndex + 2, expIndex + 3) -} \ No newline at end of file diff --git a/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt b/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt index 93793768234..f4f958ae3ce 100644 --- a/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt +++ b/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt @@ -15,13 +15,17 @@ internal actual fun formatToExactDecimals(value: Double, decimals: Int): String val pow = 10.0.pow(decimals) JsMath.round(abs(value) * pow) / pow * sign(value) } - return rounded.asDynamic().toFixed(decimals).unsafeCast() + return if (abs(rounded) < 1e21) { + // toFixed switches to scientific format after 1e21 + rounded.asDynamic().toFixed(decimals).unsafeCast() + } else { + // toPrecision outputs the specified number of digits, but only for positive numbers + val positive = abs(rounded) + val positiveString = positive.asDynamic().toPrecision(ceil(log10(positive)) + decimals).unsafeCast() + if (rounded < 0) "-$positiveString" else positiveString + } } internal actual fun formatUpToDecimals(value: Double, decimals: Int): String { return value.asDynamic().toLocaleString("en-us", json("maximumFractionDigits" to decimals)).unsafeCast() } - -internal actual fun formatScientific(value: Double): String { - return value.asDynamic().toExponential(2).unsafeCast() -} \ No newline at end of file diff --git a/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt b/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt index 543853f0449..2ea65d1dff3 100644 --- a/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt +++ b/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -7,15 +7,11 @@ package kotlin.time import java.math.RoundingMode import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.util.* import kotlin.concurrent.getOrSet -private val rootNegativeExpFormatSymbols = DecimalFormatSymbols(Locale.ROOT).apply { exponentSeparator = "e" } -private val rootPositiveExpFormatSymbols = DecimalFormatSymbols(Locale.ROOT).apply { exponentSeparator = "e+" } private val precisionFormats = Array(4) { ThreadLocal() } -private fun createFormatForDecimals(decimals: Int) = DecimalFormat("0", rootNegativeExpFormatSymbols).apply { +private fun createFormatForDecimals(decimals: Int) = DecimalFormat("0").apply { if (decimals > 0) minimumFractionDigits = decimals roundingMode = RoundingMode.HALF_UP } @@ -32,11 +28,3 @@ internal actual fun formatUpToDecimals(value: Double, decimals: Int): String = createFormatForDecimals(0) .apply { maximumFractionDigits = decimals } .format(value) - -private val scientificFormat = ThreadLocal() -internal actual fun formatScientific(value: Double): String = - scientificFormat.getOrSet { DecimalFormat("0E0", rootNegativeExpFormatSymbols).apply { minimumFractionDigits = 2 } } - .apply { - decimalFormatSymbols = if (value >= 1 || value <= -1) rootPositiveExpFormatSymbols else rootNegativeExpFormatSymbols - } - .format(value) \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/time/Duration.kt b/libraries/stdlib/src/kotlin/time/Duration.kt index ef0060be648..5553f14292b 100644 --- a/libraries/stdlib/src/kotlin/time/Duration.kt +++ b/libraries/stdlib/src/kotlin/time/Duration.kt @@ -783,7 +783,10 @@ public value class Duration internal constructor(private val rawValue: Long) : C * and formatted with the specified [decimals] number of digits after decimal point. * * Special cases: - * - the infinite duration is formatted as `"Infinity"` without unit + * - an infinite duration is formatted as `"Infinity"` or `"-Infinity"` without a unit. + * + * @param decimals the number of digits after decimal point to show. The value must be non-negative. + * No more than 12 decimals will be shown, even if a larger number is requested. * * @return the value of duration in the specified [unit] followed by that unit abbreviated name: `d`, `h`, `m`, `s`, `ms`, `us`, or `ns`. * @@ -795,10 +798,7 @@ public value class Duration internal constructor(private val rawValue: Long) : C require(decimals >= 0) { "decimals must be not negative, but was $decimals" } val number = toDouble(unit) if (number.isInfinite()) return number.toString() - return when { - abs(number) < 1e14 -> formatToExactDecimals(number, decimals.coerceAtMost(12)) - else -> formatScientific(number) - } + unit.shortName() + return formatToExactDecimals(number, decimals.coerceAtMost(12)) + unit.shortName() } @@ -1211,5 +1211,4 @@ private fun millisToNanos(millis: Long): Long = millis * NANOS_IN_MILLIS internal expect fun formatToExactDecimals(value: Double, decimals: Int): String -internal expect fun formatUpToDecimals(value: Double, decimals: Int): String -internal expect fun formatScientific(value: Double): String \ No newline at end of file +internal expect fun formatUpToDecimals(value: Double, decimals: Int): String \ No newline at end of file diff --git a/libraries/stdlib/test/time/DurationTest.kt b/libraries/stdlib/test/time/DurationTest.kt index 146ed119159..f7011bcdc20 100644 --- a/libraries/stdlib/test/time/DurationTest.kt +++ b/libraries/stdlib/test/time/DurationTest.kt @@ -9,6 +9,7 @@ package test.time import test.numbers.assertAlmostEquals import kotlin.math.nextDown +import kotlin.math.pow import kotlin.native.concurrent.SharedImmutable import kotlin.test.* import kotlin.time.* @@ -520,7 +521,7 @@ class DurationTest { } @Test - fun formatInUnits() { + fun parseAndFormatInUnits() { var d = with(Duration) { days(1) + hours(15) + minutes(31) + seconds(45) + milliseconds(678) + microseconds(920) + nanoseconds(516.34) @@ -529,6 +530,13 @@ class DurationTest { fun test(unit: DurationUnit, vararg representations: String) { assertFails { d.toString(unit, -1) } assertEquals(representations.toList(), representations.indices.map { d.toString(unit, it) }) + for ((decimals, string) in representations.withIndex()) { + val d1 = Duration.parse(string) + assertEquals(d1, Duration.parseOrNull(string)) + if (!(d1 == d || (d1 - d).absoluteValue <= (0.5 * 10.0.pow(-decimals)).toDuration(unit))) { + fail("Parsed value $d1 (from $string) is too far from the real value $d") + } + } } test(DurationUnit.DAYS, "2d", "1.6d", "1.65d", "1.647d") @@ -553,7 +561,13 @@ class DurationTest { assertEquals("0.500000000000s", Duration.seconds(0.5).toString(DurationUnit.SECONDS, 100)) assertEquals("99999000000000.000000000000ns", Duration.seconds(99_999).toString(DurationUnit.NANOSECONDS, 15)) - assertEquals("1.00e+14ns", Duration.seconds(100_000).toString(DurationUnit.NANOSECONDS, 9)) + assertContains( + listOf( + "-4611686018427388000000000.000000000000ns", + "-4611686018427387904000000.000000000000ns" + ), + (-Duration.milliseconds(MAX_MILLIS - 1)).toString(DurationUnit.NANOSECONDS, 15) + ) d = Duration.INFINITE test(DurationUnit.DAYS, "Infinity", "Infinity") diff --git a/libraries/stdlib/wasm/src/kotlin/time/Duration.kt b/libraries/stdlib/wasm/src/kotlin/time/Duration.kt index f36abccdbca..45159fb8928 100644 --- a/libraries/stdlib/wasm/src/kotlin/time/Duration.kt +++ b/libraries/stdlib/wasm/src/kotlin/time/Duration.kt @@ -1,9 +1,8 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package kotlin.time internal actual fun formatToExactDecimals(value: Double, decimals: Int): String = TODO("Wasm stdlib: Duration") -internal actual fun formatUpToDecimals(value: Double, decimals: Int): String = TODO("Wasm stdlib: Duration") -internal actual fun formatScientific(value: Double): String = TODO("Wasm stdlib: Duration") \ No newline at end of file +internal actual fun formatUpToDecimals(value: Double, decimals: Int): String = TODO("Wasm stdlib: Duration") \ No newline at end of file