Skip to content

Commit abe03dc

Browse files
committed
added toInstantOrNull functions avoiding exceptions when parsing
1 parent 95b9df6 commit abe03dc

File tree

2 files changed

+128
-7
lines changed
  • core/src
    • main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api
    • test/kotlin/org/jetbrains/kotlinx/dataframe/api

2 files changed

+128
-7
lines changed

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/impl/api/parse.kt

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.datetime.LocalDate
55
import kotlinx.datetime.LocalDateTime
66
import kotlinx.datetime.LocalTime
77
import kotlinx.datetime.format.DateTimeComponents
8+
import kotlinx.datetime.toKotlinInstant
89
import kotlinx.datetime.toKotlinLocalDate
910
import kotlinx.datetime.toKotlinLocalDateTime
1011
import kotlinx.datetime.toKotlinLocalTime
@@ -163,6 +164,32 @@ internal object Parsers : GlobalParserOptions {
163164
return null
164165
}
165166

167+
private fun String.toInstantOrNull(): Instant? {
168+
// Default format used by Instant.parse
169+
val format = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
170+
return catchSilent {
171+
// low chance throwing exception, thanks to using parseOrNull instead of parse
172+
format.parseOrNull(this)?.toInstantUsingOffset()
173+
}
174+
// fallback on the java instant to catch things like "2022-01-23T04:29:60", a.k.a. leap seconds
175+
?: toJavaInstantOrNull()?.toKotlinInstant()
176+
}
177+
178+
private fun String.toJavaInstantOrNull(): JavaInstant? {
179+
// Default format used by java.time.Instant.parse
180+
val format = DateTimeFormatter.ISO_INSTANT
181+
return catchSilent {
182+
// low chance throwing exception, thanks to using parseUnresolved instead of parse
183+
val parsePosition = ParsePosition(0)
184+
val accessor = format.parseUnresolved(this, parsePosition)
185+
if (accessor != null && parsePosition.errorIndex == -1) {
186+
JavaInstant.from(accessor)
187+
} else {
188+
null
189+
}
190+
}
191+
}
192+
166193
private fun String.toLocalDateTimeOrNull(formatter: DateTimeFormatter?): LocalDateTime? =
167194
toJavaLocalDateTimeOrNull(formatter)?.toKotlinLocalDateTime()
168195

@@ -276,13 +303,19 @@ internal object Parsers : GlobalParserOptions {
276303
// Long
277304
stringParser<Long> { it.toLongOrNull() },
278305
// kotlinx.datetime.Instant
279-
stringParser<Instant>(catch = true) {
280-
// same as Instant.parse(it), but with one fewer potential exception thrown/caught
281-
val format = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
282-
format.parse(it).toInstantUsingOffset()
306+
stringParser<Instant> {
307+
it.toInstantOrNull()
283308
},
309+
// stringParser<Instant>(true) {
310+
// Instant.parse(it)
311+
// }, // TODO remove
284312
// java.time.Instant, will be skipped if kotlinx.datetime.Instant is already checked
285-
stringParser<JavaInstant>(catch = true, coveredBy = setOf(typeOf<Instant>())) { JavaInstant.parse(it) },
313+
stringParser<JavaInstant>(coveredBy = setOf(typeOf<Instant>())) {
314+
it.toJavaInstantOrNull()
315+
},
316+
// stringParser<JavaInstant>(catch = true /*coveredBy = setOf(typeOf<Instant>())*/) {
317+
// JavaInstant.parse(it)
318+
// }, // TODO remove
286319
// kotlinx.datetime.LocalDateTime
287320
stringParserWithOptions<LocalDateTime> { options ->
288321
val formatter = options?.getDateTimeFormatter()
@@ -308,9 +341,19 @@ internal object Parsers : GlobalParserOptions {
308341
parser
309342
},
310343
// kotlin.time.Duration
311-
stringParser<Duration> { it.toDurationOrNull() },
344+
stringParser<Duration> {
345+
it.toDurationOrNull()
346+
},
347+
// stringParser<Duration>(true) {
348+
// Duration.parse(it)
349+
// }, // TODO remove
312350
// java.time.Duration, will be skipped if kotlin.time.Duration is already checked
313-
stringParser<JavaDuration>(coveredBy = setOf(typeOf<Duration>())) { it.toJavaDurationOrNull() },
351+
stringParser<JavaDuration>(coveredBy = setOf(typeOf<Duration>())) {
352+
it.toJavaDurationOrNull()
353+
},
354+
// stringParser<JavaDuration>(true/*coveredBy = setOf(typeOf<Duration>())*/) {
355+
// JavaDuration.parse(it)
356+
// }, // TODO remove
314357
// kotlinx.datetime.LocalTime
315358
stringParserWithOptions<LocalTime> { options ->
316359
val formatter = options?.getDateTimeFormatter()

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/parse.kt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
package org.jetbrains.kotlinx.dataframe.api
22

3+
import io.kotest.matchers.should
34
import io.kotest.matchers.shouldBe
5+
import kotlinx.datetime.DateTimeUnit
46
import kotlinx.datetime.Instant
57
import kotlinx.datetime.LocalDate
68
import kotlinx.datetime.LocalDateTime
79
import kotlinx.datetime.LocalTime
810
import kotlinx.datetime.Month
11+
import kotlinx.datetime.format.DateTimeComponents
12+
import kotlinx.datetime.plus
13+
import kotlinx.datetime.toJavaInstant
14+
import kotlinx.datetime.toKotlinInstant
915
import org.jetbrains.kotlinx.dataframe.DataFrame
16+
import org.jetbrains.kotlinx.dataframe.impl.api.Parsers
17+
import org.jetbrains.kotlinx.dataframe.impl.catchSilent
1018
import org.jetbrains.kotlinx.dataframe.type
1119
import org.junit.Test
1220
import java.util.Locale
21+
import kotlin.random.Random
1322
import kotlin.reflect.typeOf
1423
import kotlin.time.Duration.Companion.days
1524
import kotlin.time.Duration.Companion.hours
1625
import kotlin.time.Duration.Companion.minutes
1726
import kotlin.time.Duration.Companion.seconds
27+
import java.time.Instant as JavaInstant
1828

1929
class ParseTests {
2030
@Test
@@ -142,9 +152,77 @@ class ParseTests {
142152
columnOf("2022-01-23T04:29:40").parse().type shouldBe typeOf<LocalDateTime>()
143153
}
144154

155+
@Test
156+
fun `can parse instants`() {
157+
val instantParser = Parsers[typeOf<Instant>()]!!
158+
val javaInstantParser = Parsers[typeOf<JavaInstant>()]!!
159+
160+
// from the kotlinx-datetime tests, java instants treat leap seconds etc. like this
161+
fun parseInstantLikeJavaDoesOrNull(input: String): Instant? = catchSilent {
162+
DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET.parseOrNull(input)?.apply {
163+
when {
164+
hour == 24 && minute == 0 && second == 0 && nanosecond == 0 -> {
165+
setDate(toLocalDate().plus(1, DateTimeUnit.DAY))
166+
hour = 0
167+
}
168+
169+
hour == 23 && minute == 59 && second == 60 -> second = 59
170+
}
171+
}?.toInstantUsingOffset()
172+
}
173+
174+
fun formatTwoDigits(i: Int) = if (i < 10) "0$i" else "$i"
175+
176+
for (hour in 23..25) {
177+
for (minute in listOf(0..5, 58..62).flatten()) {
178+
for (second in listOf(0..5, 58..62).flatten()) {
179+
val input = "2020-03-16T$hour:${formatTwoDigits(minute)}:${formatTwoDigits(second)}Z"
180+
181+
val myParserRes = instantParser.applyOptions(null)(input) as Instant?
182+
val myJavaParserRes = javaInstantParser.applyOptions(null)(input) as JavaInstant?
183+
val instantRes = catchSilent { Instant.parse(input) }
184+
val instantLikeJava = parseInstantLikeJavaDoesOrNull(input)
185+
val javaInstantRes = catchSilent { JavaInstant.parse(input) }
186+
187+
// our parser has a fallback mechanism built in, like this
188+
myParserRes shouldBe (instantRes ?: javaInstantRes?.toKotlinInstant())
189+
myParserRes shouldBe instantLikeJava
190+
191+
myJavaParserRes shouldBe javaInstantRes
192+
193+
myParserRes?.toJavaInstant() shouldBe instantLikeJava?.toJavaInstant()
194+
instantLikeJava?.toJavaInstant() shouldBe myJavaParserRes
195+
myJavaParserRes shouldBe javaInstantRes
196+
}
197+
}
198+
}
199+
}
200+
145201
@Test
146202
fun `parse duration`() {
147203
columnOf("1d 15m", "20h 35m 11s").parse() shouldBe
148204
columnOf(1.days + 15.minutes, 20.hours + 35.minutes + 11.seconds)
149205
}
206+
207+
@Test
208+
fun `Parse normal string column`() {
209+
val df = dataFrameOf(List(5_000) { "_$it" }).fill(100) {
210+
Random.nextInt().toChar().toString() + Random.nextInt().toChar()
211+
}
212+
213+
df.parse()
214+
}
215+
216+
/**
217+
* Asserts that all elements of the iterable are equal to each other
218+
*/
219+
private fun <T> Iterable<T>.shouldAllBeEqual(): Iterable<T> {
220+
this should {
221+
it.reduce { a, b ->
222+
a shouldBe b
223+
b
224+
}
225+
}
226+
return this
227+
}
150228
}

0 commit comments

Comments
 (0)