Introduce functions to parse a duration from a string KT-45325

This commit is contained in:
Ilya Gorbunov
2021-07-02 20:12:28 +03:00
committed by Space
parent 7ab6f6c9b2
commit 1c6ab08220
6 changed files with 244 additions and 21 deletions
+12
View File
@@ -342,6 +342,18 @@ public final inline class Duration : kotlin.Comparable<kotlin.time.Duration> {
@kotlin.SinceKotlin(version = "1.5")
public final fun nanoseconds(value: kotlin.Long): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.5")
public final fun parse(value: kotlin.String): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.5")
public final fun parseIsoString(value: kotlin.String): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.5")
public final fun parseIsoStringOrNull(value: kotlin.String): kotlin.time.Duration?
@kotlin.SinceKotlin(version = "1.5")
public final fun parseOrNull(value: kotlin.String): kotlin.time.Duration?
@kotlin.SinceKotlin(version = "1.5")
public final fun seconds(value: kotlin.Double): kotlin.time.Duration
+12
View File
@@ -342,6 +342,18 @@ public final inline class Duration : kotlin.Comparable<kotlin.time.Duration> {
@kotlin.SinceKotlin(version = "1.5")
public final fun nanoseconds(value: kotlin.Long): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.5")
public final fun parse(value: kotlin.String): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.5")
public final fun parseIsoString(value: kotlin.String): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.5")
public final fun parseIsoStringOrNull(value: kotlin.String): kotlin.time.Duration?
@kotlin.SinceKotlin(version = "1.5")
public final fun parseOrNull(value: kotlin.String): kotlin.time.Duration?
@kotlin.SinceKotlin(version = "1.5")
public final fun seconds(value: kotlin.Double): kotlin.time.Duration
@@ -174,6 +174,63 @@ public value class Duration internal constructor(private val rawValue: Long) : C
@SinceKotlin("1.5")
public fun days(value: Double): Duration = value.toDuration(DurationUnit.DAYS)
/**
* Parses a string that represents a duration and returns the parsed [Duration] value.
*
* The following formats are accepted:
*
* - ISO-8601 Duration format, e.g. `P1DT2H3M4.058S`, see [toIsoString] and [parseIsoString].
* - The format of string returned by the default [Duration.toString] and `toString` in a specific unit,
* e.g. `10s`, `1h 30m` or `-(1h 30m)`.
*
* @throws IllegalArgumentException if the string doesn't represent a duration in any of the supported formats.
*/
@SinceKotlin("1.5")
public fun parse(value: String): Duration = try {
parseDuration(value, strictIso = false)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException("Invalid duration string format: $value", e.takeIf { it.message != null })
}
/**
* Parses a string that represents a duration in ISO-8601 format and returns the parsed [Duration] value.
*
* @throws IllegalArgumentException if the string doesn't represent a duration in ISO-8601 format.
*/
@SinceKotlin("1.5")
public fun parseIsoString(value: String): Duration = try {
parseDuration(value, strictIso = true)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException("Invalid ISO duration string format: $value", e.takeIf { it.message != null })
}
/**
* Parses a string that represents a duration and returns the parsed [Duration] value,
* or `null` if the string doesn't represent a duration in any of the supported formats.
*
* The following formats are accepted:
*
* - ISO-8601 Duration format, e.g. `P1DT2H3M4.058S`, see [toIsoString] and [parseIsoString].
* - The format of string returned by the default [Duration.toString] and `toString` in a specific unit,
* e.g. `10s`, `1h 30m` or `-(1h 30m)`.
*/
@SinceKotlin("1.5")
public fun parseOrNull(value: String): Duration? = try {
parseDuration(value, strictIso = false)
} catch (e: IllegalArgumentException) {
null
}
/**
* Parses a string that represents a duration in ISO-8601 format and returns the parsed [Duration] value,
* or `null` if the string doesn't represent a duration in ISO-8601 format.
*/
@SinceKotlin("1.5")
public fun parseIsoStringOrNull(value: String): Duration? = try {
parseDuration(value, strictIso = true)
} catch (e: IllegalArgumentException) {
null
}
}
// arithmetic operators
@@ -1008,6 +1065,98 @@ public inline operator fun Int.times(duration: Duration): Duration = duration *
public inline operator fun Double.times(duration: Duration): Duration = duration * this
@ExperimentalTime
private fun parseDuration(value: String, strictIso: Boolean): Duration {
val length = value.length
if (length == 0) throw IllegalArgumentException("The string is empty")
var index = 0
var result = Duration.ZERO
val infinityString = "Infinity"
when (value[index]) {
'+', '-' -> index++
}
when {
length <= index ->
throw IllegalArgumentException()
value[index] == 'P' -> {
index++
val signedDigits = "+-0123456789."
var isTimeComponent = false
var prevUnit: DurationUnit? = null
while (index < length) {
if (value[index] == 'T') {
if (isTimeComponent || index++ == length) throw IllegalArgumentException()
isTimeComponent = true
continue
}
val component = value.substringWhile(index) { it in signedDigits }
if (component.isEmpty()) throw IllegalArgumentException()
index += component.length
val unitChar = value.getOrElse(index) { throw IllegalArgumentException("Missing unit for value $component") }
index++
val unit = durationUnitByIsoChar(unitChar, isTimeComponent)
if (prevUnit != null && prevUnit <= unit) throw IllegalArgumentException("Unexpected order of duration components")
prevUnit = unit
if (unit == DurationUnit.SECONDS && '.' in component) {
// TODO: parse fractional part separately
result += component.toDouble().toDuration(unit)
} else {
result += component.toLong().toDuration(unit)
}
}
}
strictIso ->
throw IllegalArgumentException()
value.regionMatches(index, infinityString, 0, length = maxOf(length - index, infinityString.length), ignoreCase = true) -> {
result = Duration.INFINITE
}
else -> {
// parse default string format
val digits = "0123456789."
var prevUnit: DurationUnit? = null
var afterFirst = false
while (index < length) {
if (afterFirst) {
index = value.skipWhile(index) { it == ' ' }
}
afterFirst = true
val component = value.substringWhile(index) { it in digits }
if (component.isEmpty()) throw IllegalArgumentException()
index += component.length
val unitName = value.substringWhile(index) { it in 'a'..'z' }
index += unitName.length
val unit = durationUnitByShortName(unitName)
if (prevUnit != null && prevUnit <= unit) throw IllegalArgumentException("Unexpected order of duration components")
prevUnit = unit
if ('.' in component) {
// TODO: parse fractional part separately
result += component.toDouble().toDuration(unit)
if (index < length) throw IllegalArgumentException("Fractional component must be last")
} else {
result += component.toLong().toDuration(unit)
}
}
}
}
return if (value.startsWith('-')) -result else result
}
private inline fun String.substringWhile(startIndex: Int, predicate: (Char) -> Boolean): String =
substring(startIndex, skipWhile(startIndex, predicate))
private inline fun String.skipWhile(startIndex: Int, predicate: (Char) -> Boolean): Int {
var i = startIndex
while (i < length && predicate(this[i])) i++
return i
}
// The ranges are chosen so that they are:
// - symmetric relative to zero: this greatly simplifies operations with sign, e.g. unaryMinus and minus.
// - non-overlapping, but adjacent: the first value that doesn't fit in nanos range, can be exactly represented in millis.
@@ -76,3 +76,36 @@ internal fun DurationUnit.shortName(): String = when (this) {
DurationUnit.DAYS -> "d"
else -> error("Unknown unit: $this")
}
@SinceKotlin("1.5")
@ExperimentalTime
internal fun durationUnitByShortName(shortName: String): DurationUnit = when (shortName) {
"ns" -> DurationUnit.NANOSECONDS
"us" -> DurationUnit.MICROSECONDS
"ms" -> DurationUnit.MILLISECONDS
"s" -> DurationUnit.SECONDS
"m" -> DurationUnit.MINUTES
"h" -> DurationUnit.HOURS
"d" -> DurationUnit.DAYS
else -> throw IllegalArgumentException("Unknown duration unit short name: $shortName")
}
@SinceKotlin("1.5")
@ExperimentalTime
internal fun durationUnitByIsoChar(isoChar: Char, isTimeComponent: Boolean): DurationUnit =
when {
!isTimeComponent -> {
when (isoChar) {
'D' -> DurationUnit.DAYS
else -> throw IllegalArgumentException("Invalid or unsupported duration ISO non-time unit: $isoChar")
}
}
else -> {
when (isoChar) {
'H' -> DurationUnit.HOURS
'M' -> DurationUnit.MINUTES
'S' -> DurationUnit.SECONDS
else -> throw IllegalArgumentException("Invalid duration ISO time unit: $isoChar")
}
}
}
+34 -21
View File
@@ -454,38 +454,51 @@ class DurationTest {
}
@Test
fun toIsoString() {
fun isoStringFormatAndParse() {
fun test(duration: Duration, vararg isoStrings: String) {
assertEquals(isoStrings.first(), duration.toIsoString())
for (isoString in isoStrings) {
assertEquals(duration, Duration.parseIsoString(isoString), isoString)
assertEquals(duration, Duration.parse(isoString), isoString)
assertEquals(duration, Duration.parseIsoStringOrNull(isoString), isoString)
assertEquals(duration, Duration.parseOrNull(isoString), isoString)
}
}
// zero
assertEquals("PT0S", Duration.ZERO.toIsoString())
test(Duration.ZERO, "PT0S", "P0D", "PT0H", "PT0M", "P0DT0H", "PT0H0M", "PT0H0S")
// single unit
assertEquals("PT24H", Duration.days(1).toIsoString())
assertEquals("PT1H", Duration.hours(1).toIsoString())
assertEquals("PT1M", Duration.minutes(1).toIsoString())
assertEquals("PT1S", Duration.seconds(1).toIsoString())
assertEquals("PT0.001S", Duration.milliseconds(1).toIsoString())
assertEquals("PT0.000001S", Duration.microseconds(1).toIsoString())
assertEquals("PT0.000000001S", Duration.nanoseconds(1).toIsoString())
assertEquals("PT0.000000001S", Duration.nanoseconds(0.9).toIsoString())
test(Duration.days(1), "PT24H", "P1D", "PT1440M", "PT86400S")
test(Duration.hours(1), "PT1H")
test(Duration.minutes(1), "PT1M")
test(Duration.seconds(1), "PT1S")
test(Duration.milliseconds(1), "PT0.001S")
test(Duration.microseconds(1), "PT0.000001S")
test(Duration.nanoseconds(1), "PT0.000000001S", "PT0.0000000009S")
test(Duration.nanoseconds(0.9), "PT0.000000001S")
// rounded to zero
assertEquals("PT0S", Duration.nanoseconds(0.1).toIsoString())
test(Duration.nanoseconds(0.1), "PT0S")
test(Duration.ZERO, "PT0S", "PT0.0000000004S")
// several units combined
assertEquals("PT24H1M", (Duration.days(1) + Duration.minutes(1)).toIsoString())
assertEquals("PT24H0M1S", (Duration.days(1) + Duration.seconds(1)).toIsoString())
assertEquals("PT24H0M0.001S", (Duration.days(1) + Duration.milliseconds(1)).toIsoString())
assertEquals("PT1H30M", (Duration.hours(1) + Duration.minutes(30)).toIsoString())
assertEquals("PT1H0M0.500S", (Duration.hours(1) + Duration.milliseconds(500)).toIsoString())
assertEquals("PT2M0.500S", (Duration.minutes(2) + Duration.milliseconds(500)).toIsoString())
assertEquals("PT1M30.500S", (Duration.milliseconds(90_500)).toIsoString())
test(Duration.days(1) + Duration.minutes(1), "PT24H1M")
test(Duration.days(1) + Duration.seconds(1), "PT24H0M1S")
test(Duration.days(1) + Duration.milliseconds(1), "PT24H0M0.001S")
test(Duration.hours(1) + Duration.minutes(30), "PT1H30M")
test(Duration.hours(1) + Duration.milliseconds(500), "PT1H0M0.500S")
test(Duration.minutes(2) + Duration.milliseconds(500), "PT2M0.500S")
test(Duration.milliseconds(90_500), "PT1M30.500S")
// negative
assertEquals("-PT23H45M", (-Duration.days(1) + Duration.minutes(15)).toIsoString())
assertEquals("-PT24H15M", (-Duration.days(1) - Duration.minutes(15)).toIsoString())
test(-Duration.days(1) + Duration.minutes(15), "-PT23H45M", "PT-23H-45M", "PT-24H15M")
test(-Duration.days(1) - Duration.minutes(15), "-PT24H15M", "PT-24H-15M", "-PT25H-45M")
test(Duration.ZERO, "PT0S", "P1DT-24H", "PT-1H60M", "-PT1M-60S")
// infinite
assertEquals("PT9999999999999H", Duration.INFINITE.toIsoString())
test(Duration.INFINITE, "PT9999999999999H", "PT10000000000000H")
test(-Duration.INFINITE, "-PT9999999999999H", "-PT10000000000000H")
}
@Test
@@ -5730,6 +5730,10 @@ public final class kotlin/time/Duration$Companion {
public final fun nanoseconds-UwyO8pc (D)J
public final fun nanoseconds-UwyO8pc (I)J
public final fun nanoseconds-UwyO8pc (J)J
public final fun parse-UwyO8pc (Ljava/lang/String;)J
public final fun parseIsoString-UwyO8pc (Ljava/lang/String;)J
public final fun parseIsoStringOrNull-FghU774 (Ljava/lang/String;)Lkotlin/time/Duration;
public final fun parseOrNull-FghU774 (Ljava/lang/String;)Lkotlin/time/Duration;
public final fun seconds-UwyO8pc (D)J
public final fun seconds-UwyO8pc (I)J
public final fun seconds-UwyO8pc (J)J