Skip to content

Commit d7c5f7b

Browse files
authored
fix: parse Instant correctly from epoch offset even when exponential notation is used (#679)
1 parent 9e5c5ac commit d7c5f7b

File tree

4 files changed

+58
-4
lines changed

4 files changed

+58
-4
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "7448bdc7-3a4b-4338-8342-3bfa364ecc7c",
3+
"type": "bugfix",
4+
"description": "Parse timestamps correctly when they are written in exponential notation (e.g., `1.924390954E9`)",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#665"
7+
]
8+
}

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/time/Parsers.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,49 @@ internal fun parseIso8601(input: String): ParsedDatetime {
154154
return ParsedDatetime(date.year, date.month, date.day, ts.hour, ts.min, ts.sec, ts.ns, ts.offsetSec)
155155
}
156156

157+
private val exponentialNotationNumber = """(-)?(\d+(.(\d+))?)E(-?\d+)""".toRegex(RegexOption.IGNORE_CASE)
158+
159+
private fun expandExponent(input: String): String =
160+
exponentialNotationNumber.matchEntire(input)?.let { match ->
161+
val (baseSign, base, _, _, exp) = match.destructured
162+
buildString {
163+
append(baseSign)
164+
val (mantissa, oldDecimalPos) = base.removeChar('.')
165+
val shift = exp.toIntOrNull() ?: throw ParseException(input, "Failed to read exponent", 0)
166+
val newDecimalPos = oldDecimalPos + shift
167+
val (int, frac) = mantissa.splitAt(newDecimalPos)
168+
append(int)
169+
append('.')
170+
append(frac)
171+
}
172+
} ?: input
173+
174+
private fun String.splitAt(position: Int, padChar: Char = '0'): Pair<String, String> = when {
175+
position <= 0 -> padChar.toString() to padStart(length - position, padChar)
176+
position >= length -> padEnd(position, padChar) to padChar.toString()
177+
else -> substring(0, position) to substring(position)
178+
}
179+
180+
private fun String.removeChar(char: Char): Pair<String, Int> = when {
181+
contains(char) -> {
182+
val pos = indexOf(char)
183+
val removed = substring(0, pos) + substring(pos + 1)
184+
removed to pos
185+
}
186+
else -> this to length
187+
}
188+
157189
/**
158190
* Parse an epoch timestamp (with or without fractional seconds) into an instant
159191
*/
160192
internal fun parseEpoch(input: String): Instant {
161-
val (pos0, secs) = takeMNDigitsLong(1, 19)(input, 0)
162-
return if (pos0 == input.length) {
193+
val expandedInput = expandExponent(input)
194+
195+
val (pos0, secs) = takeMNDigitsLong(1, 19)(expandedInput, 0)
196+
return if (pos0 == expandedInput.length) {
163197
Instant.fromEpochSeconds(secs, 0)
164198
} else {
165-
val (_, ns) = preceded(char('.'), fraction(1, 9, 9))(input, pos0)
199+
val (_, ns) = preceded(char('.'), fraction(1, 9, 9))(expandedInput, pos0)
166200
Instant.fromEpochSeconds(secs, ns)
167201
}
168202
}

runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/time/ParseEpochTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ class ParseEpochTest {
1818
Triple("1604588357.1", 1604588357L, 100_000_000),
1919
Triple("1604588357.0345", 1604588357L, 34_500_000),
2020
Triple("1604588357.000001", 1604588357L, 1000),
21-
Triple("1604588357.000000001", 1604588357L, 1)
21+
Triple("1604588357.000000001", 1604588357L, 1),
22+
Triple("1.604588357E9", 1604588357L, 0),
23+
Triple("1.604588357e9", 1604588357L, 0),
24+
Triple("1604.588357E6", 1604588357L, 0),
25+
Triple("1604588357000E-3", 1604588357L, 0),
26+
Triple("0.001604588357E12", 1604588357L, 0),
27+
Triple("1.6045883570345E9", 1604588357L, 34_500_000),
28+
Triple("1.604588357000001E9", 1604588357L, 1000),
29+
Triple("1.604588357000000001E9", 1604588357L, 1),
2230
)
2331

2432
for ((idx, test) in tests.withIndex()) {

runtime/serde/serde-json/common/test/aws/smithy/kotlin/runtime/serde/json/JsonDeserializerTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class JsonDeserializerTest {
2121
"\"-Infinity\"" to Double.NEGATIVE_INFINITY,
2222
"\"Infinity\"" to Double.POSITIVE_INFINITY,
2323
"\"NaN\"" to Double.NaN,
24+
"1.5e5" to 150000.0,
25+
"1.5e-5" to 0.000015,
2426
)
2527

2628
for ((input, expected) in tests) {
@@ -44,6 +46,8 @@ class JsonDeserializerTest {
4446
"\"-Infinity\"" to Float.NEGATIVE_INFINITY,
4547
"\"Infinity\"" to Float.POSITIVE_INFINITY,
4648
"\"NaN\"" to Float.NaN,
49+
"1.5e5" to 150000f,
50+
"1.5e-5" to 0.000015f,
4751
)
4852

4953
for ((input, expected) in tests) {

0 commit comments

Comments
 (0)