Skip to content

Commit de82b0f

Browse files
committed
deprecated old java date-time parsing options in favor of explicitly "java"-named ones. Introduced Kotlin DateTimeFormat options and using Kotlin by default unless java arguments are specified.
1 parent 0bc0e3a commit de82b0f

7 files changed

Lines changed: 342 additions & 167 deletions

File tree

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

Lines changed: 235 additions & 81 deletions
Large diffs are not rendered by default.

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

Lines changed: 92 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
44
import kotlinx.datetime.LocalDate
55
import kotlinx.datetime.LocalDateTime
66
import kotlinx.datetime.LocalTime
7+
import kotlinx.datetime.format.DateTimeFormat
78
import kotlinx.datetime.toDeprecatedInstant
8-
import kotlinx.datetime.toKotlinLocalDate
9-
import kotlinx.datetime.toKotlinLocalDateTime
10-
import kotlinx.datetime.toKotlinLocalTime
119
import org.jetbrains.kotlinx.dataframe.AnyFrame
1210
import org.jetbrains.kotlinx.dataframe.AnyRow
1311
import org.jetbrains.kotlinx.dataframe.ColumnsSelector
@@ -25,12 +23,12 @@ import org.jetbrains.kotlinx.dataframe.api.isColumnGroup
2523
import org.jetbrains.kotlinx.dataframe.api.isFrameColumn
2624
import org.jetbrains.kotlinx.dataframe.api.isSubtypeOf
2725
import org.jetbrains.kotlinx.dataframe.api.map
26+
import org.jetbrains.kotlinx.dataframe.api.parser
2827
import org.jetbrains.kotlinx.dataframe.columns.TypeSuggestion
2928
import org.jetbrains.kotlinx.dataframe.columns.size
3029
import org.jetbrains.kotlinx.dataframe.exceptions.TypeConversionException
3130
import org.jetbrains.kotlinx.dataframe.hasNulls
3231
import org.jetbrains.kotlinx.dataframe.impl.api.Parsers.resetToDefault
33-
import org.jetbrains.kotlinx.dataframe.impl.canParse
3432
import org.jetbrains.kotlinx.dataframe.impl.catchSilent
3533
import org.jetbrains.kotlinx.dataframe.impl.createStarProjectedType
3634
import org.jetbrains.kotlinx.dataframe.impl.io.FastDoubleParser
@@ -43,7 +41,6 @@ import java.net.URI
4341
import java.net.URL
4442
import java.text.ParsePosition
4543
import java.time.format.DateTimeFormatter
46-
import java.time.format.DateTimeFormatterBuilder
4744
import java.time.temporal.Temporal
4845
import java.time.temporal.TemporalQuery
4946
import java.util.Locale
@@ -62,6 +59,8 @@ import java.time.Instant as JavaInstant
6259
import java.time.LocalDate as JavaLocalDate
6360
import java.time.LocalDateTime as JavaLocalDateTime
6461
import java.time.LocalTime as JavaLocalTime
62+
import java.time.format.DateTimeFormatter as JavaDateTimeFormatter
63+
import java.time.format.DateTimeFormatterBuilder as JavaDateTimeFormatterBuilder
6564
import kotlin.time.Instant as StdlibInstant
6665
import kotlinx.datetime.Instant as DeprecatedInstant
6766

@@ -142,7 +141,25 @@ internal class StringParserWithFormat<T>(
142141
*/
143142
internal object Parsers : GlobalParserOptions {
144143

145-
private val formatters: MutableList<DateTimeFormatter> = mutableListOf()
144+
private val customDateTimeFormats: MutableList<DateTimeFormat<*>> = mutableListOf()
145+
146+
private val defaultJavaFormatters: List<JavaDateTimeFormatter> by lazy {
147+
listOf(
148+
JavaDateTimeFormatter.ISO_LOCAL_DATE_TIME,
149+
JavaDateTimeFormatter.ISO_DATE_TIME,
150+
JavaDateTimeFormatterBuilder()
151+
.parseCaseInsensitive()
152+
.append(JavaDateTimeFormatter.ISO_LOCAL_DATE)
153+
.appendLiteral(' ')
154+
.append(JavaDateTimeFormatter.ISO_LOCAL_TIME)
155+
.toFormatter(),
156+
)
157+
}
158+
159+
private val customJavaFormatters: MutableList<JavaDateTimeFormatter> = mutableListOf()
160+
161+
private val javaFormatters: List<JavaDateTimeFormatter>
162+
get() = defaultJavaFormatters + customJavaFormatters
146163

147164
private val nullStrings: MutableSet<String> = mutableSetOf()
148165

@@ -158,8 +175,12 @@ internal object Parsers : GlobalParserOptions {
158175

159176
override var parseExperimentalInstant by Delegates.notNull<Boolean>()
160177

161-
override fun addDateTimePattern(pattern: String) {
162-
formatters.add(DateTimeFormatter.ofPattern(pattern))
178+
override fun addJavaDateTimePattern(pattern: String) {
179+
customJavaFormatters.add(JavaDateTimeFormatter.ofPattern(pattern))
180+
}
181+
182+
override fun addDateTimeFormat(format: DateTimeFormat<*>) {
183+
customDateTimeFormats.add(format)
163184
}
164185

165186
override fun addNullString(str: String) {
@@ -180,20 +201,14 @@ internal object Parsers : GlobalParserOptions {
180201
_locale = value
181202
}
182203

204+
val javaDateTimeArgumentsProvided
205+
get() = customJavaFormatters.isNotEmpty()
206+
183207
override fun resetToDefault() {
184-
formatters.clear()
208+
customJavaFormatters.clear()
185209
nullStrings.clear()
186210
skipTypesSet.clear()
187-
formatters.add(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
188-
formatters.add(DateTimeFormatter.ISO_DATE_TIME)
189-
190-
DateTimeFormatterBuilder()
191-
.parseCaseInsensitive()
192-
.append(DateTimeFormatter.ISO_LOCAL_DATE)
193-
.appendLiteral(' ')
194-
.append(DateTimeFormatter.ISO_LOCAL_TIME)
195-
.toFormatter()
196-
.let { formatters.add(it) }
211+
customDateTimeFormats.clear()
197212

198213
useFastDoubleParser = true
199214
parseExperimentalUuid = false
@@ -214,7 +229,7 @@ internal object Parsers : GlobalParserOptions {
214229
*
215230
* See more about resolved and unresolved parsing in the [DateTimeFormatter] documentation.
216231
*/
217-
private fun <T : Temporal> DateTimeFormatter.parseOrNull(str: String, query: TemporalQuery<T>): T? =
232+
private fun <T : Temporal> JavaDateTimeFormatter.parseOrNull(str: String, query: TemporalQuery<T>): T? =
218233
catchSilent {
219234
// first try to parse unresolved, since it doesn't throw exceptions on invalid values
220235
val parsePosition = ParsePosition(0)
@@ -233,28 +248,24 @@ internal object Parsers : GlobalParserOptions {
233248

234249
private fun String.toJavaInstantOrNull(): JavaInstant? =
235250
// Default format used by java.time.Instant.parse
236-
DateTimeFormatter.ISO_INSTANT
251+
JavaDateTimeFormatter.ISO_INSTANT
237252
.parseOrNull(this, JavaInstant::from)
238253

239-
private fun String.toJavaLocalDateTimeOrNull(formatter: DateTimeFormatter?): JavaLocalDateTime? {
254+
private fun String.toJavaLocalDateTimeOrNull(formatter: JavaDateTimeFormatter?): JavaLocalDateTime? {
240255
if (formatter != null) {
241256
return formatter.parseOrNull(this, JavaLocalDateTime::from)
242257
} else {
243-
DateTimeFormatter.ISO_LOCAL_DATE_TIME
258+
JavaDateTimeFormatter.ISO_LOCAL_DATE_TIME
244259
.parseOrNull(this, JavaLocalDateTime::from)
245260
?.let { return it }
246-
for (format in formatters) {
261+
for (format in javaFormatters) {
247262
format.parseOrNull(this, JavaLocalDateTime::from)
248263
?.let { return it }
249264
}
250265
}
251266
return null
252267
}
253268

254-
private fun String.toLocalDateTimeOrNull(formatter: DateTimeFormatter?): LocalDateTime? =
255-
toJavaLocalDateTimeOrNull(formatter) // since we accept a Java DateTimeFormatter
256-
?.toKotlinLocalDateTime()
257-
258269
private fun String.toUrlOrNull(): URL? = if (isUrl(this)) catchSilent { URI(this).toURL() } else null
259270

260271
private fun String.toBooleanOrNull() =
@@ -268,44 +279,36 @@ internal object Parsers : GlobalParserOptions {
268279
else -> null
269280
}
270281

271-
private fun String.toJavaLocalDateOrNull(formatter: DateTimeFormatter?): JavaLocalDate? {
282+
private fun String.toJavaLocalDateOrNull(formatter: JavaDateTimeFormatter?): JavaLocalDate? {
272283
if (formatter != null) {
273284
return formatter.parseOrNull(this, JavaLocalDate::from)
274285
} else {
275-
DateTimeFormatter.ISO_LOCAL_DATE
286+
JavaDateTimeFormatter.ISO_LOCAL_DATE
276287
.parseOrNull(this, JavaLocalDate::from)
277288
?.let { return it }
278-
for (format in formatters) {
289+
for (format in javaFormatters) {
279290
format.parseOrNull(this, JavaLocalDate::from)
280291
?.let { return it }
281292
}
282293
}
283294
return null
284295
}
285296

286-
private fun String.toLocalDateOrNull(formatter: DateTimeFormatter?): LocalDate? =
287-
toJavaLocalDateOrNull(formatter) // since we accept a Java DateTimeFormatter
288-
?.toKotlinLocalDate()
289-
290-
private fun String.toJavaLocalTimeOrNull(formatter: DateTimeFormatter?): JavaLocalTime? {
297+
private fun String.toJavaLocalTimeOrNull(formatter: JavaDateTimeFormatter?): JavaLocalTime? {
291298
if (formatter != null) {
292299
return formatter.parseOrNull(this, JavaLocalTime::from)
293300
} else {
294-
DateTimeFormatter.ISO_LOCAL_TIME
301+
JavaDateTimeFormatter.ISO_LOCAL_TIME
295302
.parseOrNull(this, JavaLocalTime::from)
296303
?.let { return it }
297-
for (format in formatters) {
304+
for (format in javaFormatters) {
298305
format.parseOrNull(this, JavaLocalTime::from)
299306
?.let { return it }
300307
}
301308
}
302309
return null
303310
}
304311

305-
private fun String.toLocalTimeOrNull(formatter: DateTimeFormatter?): LocalTime? =
306-
toJavaLocalTimeOrNull(formatter) // since we accept a Java DateTimeFormatter
307-
?.toKotlinLocalTime()
308-
309312
private fun String.toJavaDurationOrNull(): JavaDuration? =
310313
if (javaDurationCanParse(this)) {
311314
catchSilent { JavaDuration.parse(this) } // will likely succeed
@@ -458,47 +461,64 @@ internal object Parsers : GlobalParserOptions {
458461
stringParser<JavaInstant>(coveredBy = setOf(typeOf<DeprecatedInstant>())) {
459462
it.toJavaInstantOrNull()
460463
},
461-
// kotlinx.datetime.LocalDateTime
464+
// kotlinx.datetime class created by a user-created DataTimeFormat, skipped if java datetime args are provided
465+
stringParserWithOptions<Any> { options ->
466+
if (options?.javaDateTimeArgumentsProvided == true || this@Parsers.javaDateTimeArgumentsProvided) {
467+
return@stringParserWithOptions SKIP_PARSER
468+
}
469+
470+
val formats = options?.dateTimeFormats ?: this@Parsers.customDateTimeFormats
471+
if (formats.isEmpty()) return@stringParserWithOptions SKIP_PARSER
472+
parseBy {
473+
formats.firstNotNullOfOrNull { format ->
474+
format.parseOrNull(it)
475+
}
476+
}
477+
},
478+
// kotlinx.datetime.LocalDateTime, skipped if java datetime args are provided
462479
stringParserWithOptions<LocalDateTime> { options ->
463-
val formatter = options?.getDateTimeFormatter()
464-
val parser = { it: String -> it.toLocalDateTimeOrNull(formatter) }
465-
parser
480+
if (options?.javaDateTimeArgumentsProvided == true || this@Parsers.javaDateTimeArgumentsProvided) {
481+
return@stringParserWithOptions SKIP_PARSER
482+
}
483+
LocalDateTime.Formats.ISO::parseOrNull
466484
},
467-
// java.time.LocalDateTime, will be skipped if kotlinx.datetime.LocalDateTime is already checked
468-
stringParserWithOptions<JavaLocalDateTime>(coveredBy = setOf(typeOf<LocalDateTime>())) { options ->
469-
val formatter = options?.getDateTimeFormatter()
470-
val parser = { it: String -> it.toJavaLocalDateTimeOrNull(formatter) }
471-
parser
485+
// java.time.LocalDateTime
486+
stringParserWithOptions<JavaLocalDateTime> { options ->
487+
val formatter = options?.getJavaDateTimeFormatter()
488+
parseBy { it.toJavaLocalDateTimeOrNull(formatter) }
472489
},
473-
// kotlinx.datetime.LocalDate
490+
// kotlinx.datetime.LocalDate, skipped if java datetime args are provided
474491
stringParserWithOptions<LocalDate> { options ->
475-
val formatter = options?.getDateTimeFormatter()
476-
val parser = { it: String -> it.toLocalDateOrNull(formatter) }
477-
parser
492+
if (options?.javaDateTimeArgumentsProvided == true || this@Parsers.javaDateTimeArgumentsProvided) {
493+
return@stringParserWithOptions SKIP_PARSER
494+
}
495+
parseBy {
496+
LocalDate.Formats.ISO.parseOrNull(it)
497+
?: LocalDate.Formats.ISO_BASIC.parseOrNull(it)
498+
}
478499
},
479-
// java.time.LocalDate, will be skipped if kotlinx.datetime.LocalDate is already checked
480-
stringParserWithOptions<JavaLocalDate>(coveredBy = setOf(typeOf<LocalDate>())) { options ->
481-
val formatter = options?.getDateTimeFormatter()
482-
val parser = { it: String -> it.toJavaLocalDateOrNull(formatter) }
483-
parser
500+
// java.time.LocalDate
501+
stringParserWithOptions<JavaLocalDate> { options ->
502+
val formatter = options?.getJavaDateTimeFormatter()
503+
parseBy { it.toJavaLocalDateOrNull(formatter) }
484504
},
485505
// kotlin.time.Duration
486506
stringParser<Duration>(body = Duration::parseOrNull),
487507
// java.time.Duration, will be skipped if kotlin.time.Duration is already checked
488508
stringParser<JavaDuration>(coveredBy = setOf(typeOf<Duration>())) {
489509
it.toJavaDurationOrNull()
490510
},
491-
// kotlinx.datetime.LocalTime
511+
// kotlinx.datetime.LocalTime, skipped if java datetime args are provided
492512
stringParserWithOptions<LocalTime> { options ->
493-
val formatter = options?.getDateTimeFormatter()
494-
val parser = { it: String -> it.toLocalTimeOrNull(formatter) }
495-
parser
513+
if (options?.javaDateTimeArgumentsProvided == true || this@Parsers.javaDateTimeArgumentsProvided) {
514+
return@stringParserWithOptions SKIP_PARSER
515+
}
516+
LocalTime.Formats.ISO::parseOrNull
496517
},
497-
// java.time.LocalTime, will be skipped if kotlinx.datetime.LocalTime is already checked
498-
stringParserWithOptions<JavaLocalTime>(coveredBy = setOf(typeOf<LocalTime>())) { options ->
499-
val formatter = options?.getDateTimeFormatter()
500-
val parser = { it: String -> it.toJavaLocalTimeOrNull(formatter) }
501-
parser
518+
// java.time.LocalTime
519+
stringParserWithOptions<JavaLocalTime> { options ->
520+
val formatter = options?.getJavaDateTimeFormatter()
521+
parseBy { it.toJavaLocalTimeOrNull(formatter) }
502522
},
503523
// java.net.URL
504524
stringParser<URL> { it.toUrlOrNull() },
@@ -591,14 +611,14 @@ internal object Parsers : GlobalParserOptions {
591611
val parser = get(clazz) ?: error("Can not convert String to $clazz")
592612
val formatter = pattern?.let {
593613
if (locale == null) {
594-
DateTimeFormatter.ofPattern(it)
614+
JavaDateTimeFormatter.ofPattern(it)
595615
} else {
596-
DateTimeFormatter.ofPattern(it, locale)
616+
JavaDateTimeFormatter.ofPattern(it, locale)
597617
}
598618
}
599619
val options = if (formatter != null || locale != null) {
600620
ParserOptions(
601-
dateTimeFormatter = formatter,
621+
javaDateTimeFormatter = formatter,
602622
locale = locale,
603623
)
604624
} else {

core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/util/deprecationMessages.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ internal const val COMPARE_RESULT_EQUALS =
152152

153153
internal const val INSERT_UNDER = "This `ColumnPath` overload is deprecated in favor of `.under { path }`. $MESSAGE_1_0"
154154

155+
internal const val ADD_DATE_TIME_PATTERN =
156+
"This function is deprecated in favor of `addDateTimeFormat` built on kotlinx-datetime. Alternatively, use `addJavaDateTimePattern` for Java-based parsing. $MESSAGE_1_0"
157+
155158
// endregion
156159

157160
// region WARNING in 1.0, ERROR in 1.1

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class ParseTests {
6464
val date by columnOf("January 1, 2020")
6565
val pattern = "MMMM d, yyyy"
6666

67-
val parsed = date.parse(ParserOptions(dateTimePattern = pattern)).cast<LocalDate>()
67+
val parsed = date.parse(ParserOptions(javaDateTimePattern = pattern)).cast<LocalDate>()
6868

6969
parsed.type() shouldBe typeOf<LocalDate>()
7070
with(parsed[0]) {
@@ -76,10 +76,10 @@ class ParseTests {
7676
date.convertToLocalDate(pattern) shouldBe parsed
7777
with(date.toDataFrame()) {
7878
convert { date }.toLocalDate(pattern)[date.name] shouldBe parsed
79-
parse(ParserOptions(dateTimePattern = pattern))[date.name] shouldBe parsed
79+
parse(ParserOptions(javaDateTimePattern = pattern))[date.name] shouldBe parsed
8080
}
8181

82-
DataFrame.parser.addDateTimePattern(pattern)
82+
DataFrame.parser.addJavaDateTimePattern(pattern)
8383

8484
date.parse() shouldBe parsed
8585
date.convertToLocalDate() shouldBe parsed
@@ -99,7 +99,7 @@ class ParseTests {
9999
val pattern = "d MMM yyyy HH:mm:ss"
100100
val locale = Locale.forLanguageTag("en-US")
101101

102-
val parsed = dateTime.parse(ParserOptions(dateTimePattern = pattern, locale = locale)).cast<LocalDateTime>()
102+
val parsed = dateTime.parse(ParserOptions(javaDateTimePattern = pattern, locale = locale)).cast<LocalDateTime>()
103103

104104
parsed.type() shouldBe typeOf<LocalDateTime>()
105105
with(parsed[0]) {
@@ -114,10 +114,10 @@ class ParseTests {
114114
dateTime.convertToLocalDateTime(pattern, locale) shouldBe parsed
115115
with(dateTime.toDataFrame()) {
116116
convert { dateTime }.toLocalDateTime(pattern)[dateTime.name] shouldBe parsed
117-
parse(ParserOptions(dateTimePattern = pattern))[dateTime.name] shouldBe parsed
117+
parse(ParserOptions(javaDateTimePattern = pattern))[dateTime.name] shouldBe parsed
118118
}
119119

120-
DataFrame.parser.addDateTimePattern(pattern)
120+
DataFrame.parser.addJavaDateTimePattern(pattern)
121121

122122
dateTime.parse(ParserOptions(locale = locale)) shouldBe parsed
123123
dateTime.convertToLocalDateTime(pattern, locale) shouldBe parsed
@@ -133,7 +133,7 @@ class ParseTests {
133133
val time by columnOf(" 13-05-30")
134134
val pattern = "HH-mm-ss"
135135

136-
val parsed = time.parse(ParserOptions(dateTimePattern = pattern)).cast<LocalTime>()
136+
val parsed = time.parse(ParserOptions(javaDateTimePattern = pattern)).cast<LocalTime>()
137137

138138
parsed.type() shouldBe typeOf<LocalTime>()
139139
with(parsed[0]) {
@@ -144,10 +144,10 @@ class ParseTests {
144144
time.convertToLocalTime(pattern) shouldBe parsed
145145
with(time.toDataFrame()) {
146146
convert { time }.toLocalTime(pattern)[time] shouldBe parsed
147-
parse(ParserOptions(dateTimePattern = pattern))[time] shouldBe parsed
147+
parse(ParserOptions(javaDateTimePattern = pattern))[time] shouldBe parsed
148148
}
149149

150-
DataFrame.parser.addDateTimePattern(pattern)
150+
DataFrame.parser.addJavaDateTimePattern(pattern)
151151

152152
time.parse() shouldBe parsed
153153
time.convertToLocalTime() shouldBe parsed

core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/io/ParserTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ParserTests {
3939
fun `parse datetime with custom format`() {
4040
val col by columnOf("04.02.2021 -- 19:44:32")
4141
col.tryParse().type() shouldBe typeOf<String>()
42-
DataFrame.parser.addDateTimePattern("dd.MM.uuuu -- HH:mm:ss")
42+
DataFrame.parser.addJavaDateTimePattern("dd.MM.uuuu -- HH:mm:ss")
4343
val parsed = col.parse()
4444
parsed.type() shouldBe typeOf<LocalDateTime>()
4545
parsed.cast<LocalDateTime>()[0].year shouldBe 2021

0 commit comments

Comments
 (0)