@@ -4,10 +4,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
44import kotlinx.datetime.LocalDate
55import kotlinx.datetime.LocalDateTime
66import kotlinx.datetime.LocalTime
7+ import kotlinx.datetime.format.DateTimeFormat
78import kotlinx.datetime.toDeprecatedInstant
8- import kotlinx.datetime.toKotlinLocalDate
9- import kotlinx.datetime.toKotlinLocalDateTime
10- import kotlinx.datetime.toKotlinLocalTime
119import org.jetbrains.kotlinx.dataframe.AnyFrame
1210import org.jetbrains.kotlinx.dataframe.AnyRow
1311import org.jetbrains.kotlinx.dataframe.ColumnsSelector
@@ -25,12 +23,12 @@ import org.jetbrains.kotlinx.dataframe.api.isColumnGroup
2523import org.jetbrains.kotlinx.dataframe.api.isFrameColumn
2624import org.jetbrains.kotlinx.dataframe.api.isSubtypeOf
2725import org.jetbrains.kotlinx.dataframe.api.map
26+ import org.jetbrains.kotlinx.dataframe.api.parser
2827import org.jetbrains.kotlinx.dataframe.columns.TypeSuggestion
2928import org.jetbrains.kotlinx.dataframe.columns.size
3029import org.jetbrains.kotlinx.dataframe.exceptions.TypeConversionException
3130import org.jetbrains.kotlinx.dataframe.hasNulls
3231import org.jetbrains.kotlinx.dataframe.impl.api.Parsers.resetToDefault
33- import org.jetbrains.kotlinx.dataframe.impl.canParse
3432import org.jetbrains.kotlinx.dataframe.impl.catchSilent
3533import org.jetbrains.kotlinx.dataframe.impl.createStarProjectedType
3634import org.jetbrains.kotlinx.dataframe.impl.io.FastDoubleParser
@@ -43,7 +41,6 @@ import java.net.URI
4341import java.net.URL
4442import java.text.ParsePosition
4543import java.time.format.DateTimeFormatter
46- import java.time.format.DateTimeFormatterBuilder
4744import java.time.temporal.Temporal
4845import java.time.temporal.TemporalQuery
4946import java.util.Locale
@@ -62,6 +59,8 @@ import java.time.Instant as JavaInstant
6259import java.time.LocalDate as JavaLocalDate
6360import java.time.LocalDateTime as JavaLocalDateTime
6461import java.time.LocalTime as JavaLocalTime
62+ import java.time.format.DateTimeFormatter as JavaDateTimeFormatter
63+ import java.time.format.DateTimeFormatterBuilder as JavaDateTimeFormatterBuilder
6564import kotlin.time.Instant as StdlibInstant
6665import kotlinx.datetime.Instant as DeprecatedInstant
6766
@@ -142,7 +141,25 @@ internal class StringParserWithFormat<T>(
142141 */
143142internal 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 {
0 commit comments