Introduce functions to parse a duration from a string KT-45325
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user