Skip to content

Commit af19cfd

Browse files
committed
creating low-in-exceptions DateTimeFormatter.parseOrNull function for parsing implementation
1 parent e0c4de7 commit af19cfd

File tree

2 files changed

+60
-35
lines changed

2 files changed

+60
-35
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public interface GlobalParserOptions {
4141

4242
public data class ParserOptions(
4343
val locale: Locale? = null,
44+
// TODO, migrate to kotlinx.datetime.format.DateTimeFormat? https://github.com/Kotlin/dataframe/issues/876
4445
val dateTimeFormatter: DateTimeFormatter? = null,
4546
val dateTimePattern: String? = null,
4647
val nullStrings: Set<String>? = null,

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

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import java.text.NumberFormat
4343
import java.text.ParsePosition
4444
import java.time.format.DateTimeFormatter
4545
import java.time.format.DateTimeFormatterBuilder
46+
import java.time.temporal.Temporal
47+
import java.time.temporal.TemporalQuery
4648
import java.util.Locale
4749
import kotlin.reflect.KClass
4850
import kotlin.reflect.KType
@@ -152,46 +154,60 @@ internal object Parsers : GlobalParserOptions {
152154
resetToDefault()
153155
}
154156

155-
private fun String.toJavaLocalDateTimeOrNull(formatter: DateTimeFormatter?): JavaLocalDateTime? {
156-
if (formatter != null) {
157-
return catchSilent { JavaLocalDateTime.parse(this, formatter) }
158-
} else {
159-
catchSilent { JavaLocalDateTime.parse(this) }?.let { return it }
160-
for (format in formatters) {
161-
catchSilent { JavaLocalDateTime.parse(this, format) }?.let { return it }
157+
/**
158+
* Parses a [string][str] using the given [java formatter][DateTimeFormatter] and [query]
159+
* while avoiding exceptions. This avoidance is achieved by first trying to parse the string _unresovled_.
160+
* If this is unsuccessful, we can simply return `null` without throwing an exception. Only if the string can
161+
* successfully be parsed unresolved, we try to parse it _resolved_.
162+
*
163+
* See more about resolved and unresolved parsing in the [DateTimeFormatter] documentation.
164+
*/
165+
private fun <T : Temporal> DateTimeFormatter.parseOrNull(str: String, query: TemporalQuery<T>): T? =
166+
catchSilent {
167+
// first try to parse unresolved, since it doesn't throw exceptions on invalid values
168+
val parsePosition = ParsePosition(0)
169+
if (parseUnresolved(str, parsePosition) != null && parsePosition.errorIndex == -1) {
170+
// do the parsing again, but now resolved, since the chance of exception is low
171+
parse(str, query)
172+
} else {
173+
null
162174
}
163175
}
164-
return null
165-
}
166176

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()
177+
private fun String.toInstantOrNull(): Instant? =
178+
// low chance throwing exception, thanks to using parseOrNull instead of parse
179+
catchSilent {
180+
// Default format used by Instant.parse
181+
DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
182+
.parseOrNull(this)
183+
?.toInstantUsingOffset()
173184
}
174185
// fallback on the java instant to catch things like "2022-01-23T04:29:60", a.k.a. leap seconds
175186
?: toJavaInstantOrNull()?.toKotlinInstant()
176-
}
177187

178-
private fun String.toJavaInstantOrNull(): JavaInstant? {
188+
private fun String.toJavaInstantOrNull(): JavaInstant? =
179189
// 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
190+
DateTimeFormatter.ISO_INSTANT
191+
.parseOrNull(this, JavaInstant::from)
192+
193+
private fun String.toJavaLocalDateTimeOrNull(formatter: DateTimeFormatter?): JavaLocalDateTime? {
194+
if (formatter != null) {
195+
return formatter.parseOrNull(this, JavaLocalDateTime::from)
196+
} else {
197+
DateTimeFormatter.ISO_LOCAL_DATE_TIME
198+
.parseOrNull(this, JavaLocalDateTime::from)
199+
?.let { return it }
200+
for (format in formatters) {
201+
format.parseOrNull(this, JavaLocalDateTime::from)
202+
?.let { return it }
189203
}
190204
}
205+
return null
191206
}
192207

193208
private fun String.toLocalDateTimeOrNull(formatter: DateTimeFormatter?): LocalDateTime? =
194-
toJavaLocalDateTimeOrNull(formatter)?.toKotlinLocalDateTime()
209+
toJavaLocalDateTimeOrNull(formatter) // since we accept a Java DateTimeFormatter
210+
?.toKotlinLocalDateTime()
195211

196212
private fun String.toUrlOrNull(): URL? = if (isURL(this)) catchSilent { URL(this) } else null
197213

@@ -208,33 +224,41 @@ internal object Parsers : GlobalParserOptions {
208224

209225
private fun String.toJavaLocalDateOrNull(formatter: DateTimeFormatter?): JavaLocalDate? {
210226
if (formatter != null) {
211-
return catchSilent { JavaLocalDate.parse(this, formatter) }
227+
return formatter.parseOrNull(this, JavaLocalDate::from)
212228
} else {
213-
catchSilent { JavaLocalDate.parse(this) }?.let { return it }
229+
DateTimeFormatter.ISO_LOCAL_DATE
230+
.parseOrNull(this, JavaLocalDate::from)
231+
?.let { return it }
214232
for (format in formatters) {
215-
catchSilent { JavaLocalDate.parse(this, format) }?.let { return it }
233+
format.parseOrNull(this, JavaLocalDate::from)
234+
?.let { return it }
216235
}
217236
}
218237
return null
219238
}
220239

221240
private fun String.toLocalDateOrNull(formatter: DateTimeFormatter?): LocalDate? =
222-
toJavaLocalDateOrNull(formatter)?.toKotlinLocalDate()
241+
toJavaLocalDateOrNull(formatter) // since we accept a Java DateTimeFormatter
242+
?.toKotlinLocalDate()
223243

224244
private fun String.toJavaLocalTimeOrNull(formatter: DateTimeFormatter?): JavaLocalTime? {
225245
if (formatter != null) {
226-
return catchSilent { JavaLocalTime.parse(this, formatter) }
246+
return formatter.parseOrNull(this, JavaLocalTime::from)
227247
} else {
228-
catchSilent { JavaLocalTime.parse(this) }?.let { return it }
248+
DateTimeFormatter.ISO_LOCAL_TIME
249+
.parseOrNull(this, JavaLocalTime::from)
250+
?.let { return it }
229251
for (format in formatters) {
230-
catchSilent { JavaLocalTime.parse(this, format) }?.let { return it }
252+
format.parseOrNull(this, JavaLocalTime::from)
253+
?.let { return it }
231254
}
232255
}
233256
return null
234257
}
235258

236259
private fun String.toLocalTimeOrNull(formatter: DateTimeFormatter?): LocalTime? =
237-
toJavaLocalTimeOrNull(formatter)?.toKotlinLocalTime()
260+
toJavaLocalTimeOrNull(formatter) // since we accept a Java DateTimeFormatter
261+
?.toKotlinLocalTime()
238262

239263
private fun String.toJavaDurationOrNull(): JavaDuration? =
240264
if (javaDurationCanParse(this)) {

0 commit comments

Comments
 (0)