diff --git a/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt b/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt index 218d3a132b7..3d134fc5501 100644 --- a/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt +++ b/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt @@ -5,6 +5,24 @@ package kotlin.time -internal actual fun formatToDecimals(value: Double, decimals: Int, unitName: String): String { - TODO("not implemented") +import kotlin.js.json +import kotlin.math.* + +internal actual fun formatToExactDecimals(value: Double, decimals: Int): String { + val rounded = if (decimals == 0) { + value + } else { + val pow = 10.0.pow(decimals) + @Suppress("DEPRECATION", "DEPRECATION_ERROR") + kotlin.js.Math.round(abs(value) * pow) / pow * sign(value) + } + return rounded.asDynamic().toFixed(decimals).unsafeCast() +} + +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().replace("+", "") } \ 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 4fb82932b48..524cbef9bb8 100644 --- a/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt +++ b/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt @@ -5,7 +5,32 @@ package kotlin.time +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols import java.util.* -internal actual fun formatToDecimals(value: Double, decimals: Int, unitName: String): String = - String.format(Locale.ROOT, "%.${decimals}f%s", value, unitName) \ No newline at end of file +private val rootFormatSymbols = DecimalFormatSymbols(Locale.ROOT).apply { exponentSeparator = "e" } +private val precisionFormats = Array(4) { null } + +private fun createFormatForDecimals(decimals: Int) = DecimalFormat("0", rootFormatSymbols).apply { + if (decimals > 0) minimumFractionDigits = decimals + roundingMode = RoundingMode.HALF_UP +} + +internal actual fun formatToExactDecimals(value: Double, decimals: Int): String { + val format = if (decimals < precisionFormats.size) { + precisionFormats[decimals] ?: createFormatForDecimals(decimals).also { precisionFormats[decimals] = it } + } else + createFormatForDecimals(decimals) + return format.format(value) +} + +internal actual fun formatUpToDecimals(value: Double, decimals: Int): String = + createFormatForDecimals(0) + .apply { maximumFractionDigits = decimals } + .format(value) + +private val scientificFormat = DecimalFormat("0E0", rootFormatSymbols).apply { maximumFractionDigits = 2 } +internal actual fun formatScientific(value: Double): String = + scientificFormat.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 18b378ae127..228d79825af 100644 --- a/libraries/stdlib/src/kotlin/time/Duration.kt +++ b/libraries/stdlib/src/kotlin/time/Duration.kt @@ -5,6 +5,8 @@ package kotlin.time +import kotlin.math.abs + private val storageUnit = DurationUnit.NANOSECONDS @Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") @@ -136,20 +138,44 @@ public inline class Duration internal constructor(internal val _value: Double) : override fun toString(): String = buildString { if (isInfinite()) { - if (isNegative()) append('-') - append("INFINITY") - } else { - // TODO: Find the most appropriate representation append(_value) - append(storageUnit.shortName()) + } else { + val absNs = absoluteValue().inNanoseconds + var scientific = false + var maxDecimals = 0 + val unit = when { + absNs == 0.0 -> return "0s" + absNs < 1e-6 -> DurationUnit.SECONDS.also { scientific = true } + absNs < 1 -> DurationUnit.NANOSECONDS.also { maxDecimals = 7 } + absNs < 1e3 -> DurationUnit.NANOSECONDS + absNs < 1e6 -> DurationUnit.MICROSECONDS + absNs < 1e9 -> DurationUnit.MILLISECONDS + absNs < 1000e9 -> DurationUnit.SECONDS + absNs < 60_000e9 -> DurationUnit.MINUTES + absNs < 3600_000e9 -> DurationUnit.HOURS + absNs < 86400e9 * 1e7 -> DurationUnit.DAYS + else -> DurationUnit.DAYS.also { scientific = true } + } + val value = inUnits(unit) + return when { + scientific -> formatScientific(value) + maxDecimals > 0 -> formatUpToDecimals(value, maxDecimals) + else -> formatToExactDecimals(value, precision(abs(value))) + } + unit.shortName() } } + private fun precision(value: Double): Int = when { + value < 1 -> 3 + value < 10 -> 2 + value < 100 -> 1 + else -> 0 + } + fun toString(unit: DurationUnit, decimals: Int = 0): String { require(decimals >= 0) { "decimals must be not negative, but was $decimals" } -// if (decimals == 0) return toString(unit) if (isInfinite()) return _value.toString() - return formatToDecimals(inUnits(unit), decimals, unit.shortName()) + return formatToExactDecimals(inUnits(unit), decimals) + unit.shortName() } @@ -226,4 +252,6 @@ val Double.days get() = Duration(this, DurationUnit.DAYS) -internal expect fun formatToDecimals(value: Double, decimals: Int, unitName: String): String +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 diff --git a/libraries/stdlib/test/time/DurationTest.kt b/libraries/stdlib/test/time/DurationTest.kt index 33f31bd78e1..21ee4e22fc6 100644 --- a/libraries/stdlib/test/time/DurationTest.kt +++ b/libraries/stdlib/test/time/DurationTest.kt @@ -6,6 +6,7 @@ package test.time import test.numbers.assertAlmostEquals +import test.* import kotlin.test.* import kotlin.time.* import kotlin.random.* @@ -298,9 +299,11 @@ class DurationTest { test(DurationUnit.NANOSECONDS, "0ns", "0.0ns", "0.02ns", "0.020ns", "0.0202ns") d = Duration.MAX_VALUE - // are such long representations ok? - test(DurationUnit.DAYS, "20806633505350875${"0".repeat(278)}d") - test(DurationUnit.NANOSECONDS, "17976931348623157${"0".repeat(292)}ns") + testOnJvm { + // are such long representations ok? + test(DurationUnit.DAYS, "20806633505350875${"0".repeat(278)}d") + test(DurationUnit.NANOSECONDS, "17976931348623157${"0".repeat(292)}ns") + } d = Duration.INFINITE test(DurationUnit.DAYS, "Infinity", "Infinity") @@ -308,4 +311,72 @@ class DurationTest { test(DurationUnit.NANOSECONDS, "-Infinity", "-Infinity") } + + @Test + fun toStringDefault() { + fun test(duration: Duration, vararg expectedOptions: String) { + val actual = duration.toString() + + if (!expectedOptions.contains(actual)) { + assertEquals(expectedOptions.toList(), duration.toString()) + } + if (duration > Duration.ZERO) + assertEquals("-$actual", (-duration).toString()) + } + + test(101.days, "101d") + test(45.3.days, "45.3d") + test(45.days, "45.0d") + + test(40.5.days, "972h") + test(40.hours + 15.minutes, "40.3h", "40.2h") + test(40.hours, "40.0h") + + test(12.5.hours, "750m") + test(30.minutes, "30.0m") + test(17.5.minutes, "17.5m") + + test(16.5.minutes, "990s") + test(90.36.seconds, "90.4s") + test(50.seconds, "50.0s") + test(1.3.seconds, "1.30s") + test(1.seconds, "1.00s") + + test(0.5.seconds, "500ms") + test(40.2.milliseconds, "40.2ms") + test(4.225.milliseconds, "4.23ms", "4.22ms") + test(4.245.milliseconds, "4.25ms") + test(1.milliseconds, "1.00ms") + + test(0.75.milliseconds, "750us") + test(75.35.microseconds, "75.4us", "75.3us") + test(7.25.microseconds, "7.25us") + test(1.035.microseconds, "1.04us", "1.03us") + test(1.005.microseconds, "1.01us", "1.00us") + + test(950.5.nanoseconds, "951ns", "950ns") + test(85.23.nanoseconds, "85.2ns") + test(8.235.nanoseconds, "8.24ns", "8.23ns") + test(1.3.nanoseconds, "1.30ns") + + test(0.75.nanoseconds, "0.75ns") + test(0.7512.nanoseconds, "0.7512ns") + test(0.023.nanoseconds, "0.023ns") + test(0.0034.nanoseconds, "0.0034ns") + test(0.0000035.nanoseconds, "0.0000035ns") + + test(Duration.ZERO, "0s") + test(365.days * 10000, "3650000d") + test(300.days * 100000, "3e7d", "3.00e7d") + test(365.days * 100000, "3.65e7d") + + val universeAge = 365.25.days * 13.799e9 + val planckTime = 5.4e-44.seconds + + test(universeAge, "5.04e12d") + test(planckTime, "5.4e-44s", "5.40e-44s") + test(Duration.MAX_VALUE, "2.08e294d") + test(Duration.INFINITE, "Infinity") + } + } \ No newline at end of file