Implement and test default string formatting in JVM and JS
Relax Duration.toString tests for JS and Native rounding specifics
This commit is contained in:
@@ -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<String>()
|
||||
}
|
||||
|
||||
internal actual fun formatUpToDecimals(value: Double, decimals: Int): String {
|
||||
return value.asDynamic().toLocaleString("en-us", json("maximumFractionDigits" to decimals)).unsafeCast<String>()
|
||||
}
|
||||
|
||||
internal actual fun formatScientific(value: Double): String {
|
||||
return value.asDynamic().toExponential(2).unsafeCast<String>().replace("+", "")
|
||||
}
|
||||
@@ -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)
|
||||
private val rootFormatSymbols = DecimalFormatSymbols(Locale.ROOT).apply { exponentSeparator = "e" }
|
||||
private val precisionFormats = Array<DecimalFormat?>(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)
|
||||
@@ -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
|
||||
@@ -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<Any>(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")
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user