diff --git a/core/api/kotlinx-datetime.api b/core/api/kotlinx-datetime.api index 1f554210..885d6afb 100644 --- a/core/api/kotlinx-datetime.api +++ b/core/api/kotlinx-datetime.api @@ -55,6 +55,7 @@ public final class kotlinx/datetime/DatePeriod : kotlinx/datetime/DateTimePeriod public final class kotlinx/datetime/DatePeriod$Companion { public final fun parse (Ljava/lang/String;)Lkotlinx/datetime/DatePeriod; + public final fun parseOrNull (Ljava/lang/String;)Lkotlinx/datetime/DatePeriod; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -81,6 +82,7 @@ public abstract class kotlinx/datetime/DateTimePeriod { public final class kotlinx/datetime/DateTimePeriod$Companion { public final fun parse (Ljava/lang/String;)Lkotlinx/datetime/DateTimePeriod; + public final fun parseOrNull (Ljava/lang/String;)Lkotlinx/datetime/DateTimePeriod; public final fun serializer ()Lkotlinx/serialization/KSerializer; } @@ -265,6 +267,8 @@ public final class kotlinx/datetime/InstantKt { public static final fun minus (Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/DateTimeUnit;Lkotlinx/datetime/TimeZone;)J public static final fun minus (Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/TimeZone;)Lkotlinx/datetime/DateTimePeriod; public static final fun monthsUntil (Lkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Lkotlinx/datetime/TimeZone;)I + public static final fun parseOrNull (Lkotlinx/datetime/Instant$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/Instant; + public static synthetic fun parseOrNull$default (Lkotlinx/datetime/Instant$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/Instant; public static final fun plus (Lkotlinx/datetime/Instant;ILkotlinx/datetime/DateTimeUnit$TimeBased;)Lkotlinx/datetime/Instant; public static final fun plus (Lkotlinx/datetime/Instant;Lkotlinx/datetime/DateTimeUnit$TimeBased;)Lkotlinx/datetime/Instant; public static final fun toInstant (Ljava/lang/String;)Lkotlinx/datetime/Instant; @@ -316,6 +320,8 @@ public final class kotlinx/datetime/LocalDateJvmKt { public static final fun daysUntil (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I public static final fun minus (Lkotlinx/datetime/LocalDate;ILkotlinx/datetime/DateTimeUnit$DateBased;)Lkotlinx/datetime/LocalDate; public static final fun monthsUntil (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I + public static final fun parseOrNull (Lkotlinx/datetime/LocalDate$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalDate; + public static synthetic fun parseOrNull$default (Lkotlinx/datetime/LocalDate$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/LocalDate; public static final fun periodUntil (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lkotlinx/datetime/DatePeriod; public static final fun plus (Lkotlinx/datetime/LocalDate;ILkotlinx/datetime/DateTimeUnit$DateBased;)Lkotlinx/datetime/LocalDate; public static final fun plus (Lkotlinx/datetime/LocalDate;JLkotlinx/datetime/DateTimeUnit$DateBased;)Lkotlinx/datetime/LocalDate; @@ -394,6 +400,8 @@ public final class kotlinx/datetime/LocalDateTimeKt { public static synthetic fun LocalDateTime$default (ILjava/time/Month;IIIIIILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; public static synthetic fun LocalDateTime$default (ILkotlinx/datetime/Month;IIIIIILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; public static final fun format (Lkotlinx/datetime/LocalDateTime;Lkotlinx/datetime/format/DateTimeFormat;)Ljava/lang/String; + public static final fun parseOrNull (Lkotlinx/datetime/LocalDateTime$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalDateTime; + public static synthetic fun parseOrNull$default (Lkotlinx/datetime/LocalDateTime$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; public static final fun toLocalDateTime (Ljava/lang/String;)Lkotlinx/datetime/LocalDateTime; } @@ -444,6 +452,8 @@ public final class kotlinx/datetime/LocalTimeKt { public static synthetic fun atDate$default (Lkotlinx/datetime/LocalTime;ILjava/time/Month;ILkotlin/Unit;ILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; public static synthetic fun atDate$default (Lkotlinx/datetime/LocalTime;ILkotlinx/datetime/Month;ILkotlin/Unit;ILjava/lang/Object;)Lkotlinx/datetime/LocalDateTime; public static final fun format (Lkotlinx/datetime/LocalTime;Lkotlinx/datetime/format/DateTimeFormat;)Ljava/lang/String; + public static final fun parseOrNull (Lkotlinx/datetime/LocalTime$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/LocalTime; + public static synthetic fun parseOrNull$default (Lkotlinx/datetime/LocalTime$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/LocalTime; public static final fun toLocalTime (Ljava/lang/String;)Lkotlinx/datetime/LocalTime; } @@ -542,6 +552,8 @@ public final class kotlinx/datetime/UtcOffset$Formats { public final class kotlinx/datetime/UtcOffsetJvmKt { public static final fun UtcOffset (Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;)Lkotlinx/datetime/UtcOffset; public static synthetic fun UtcOffset$default (Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lkotlinx/datetime/UtcOffset; + public static final fun parseOrNull (Lkotlinx/datetime/UtcOffset$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/UtcOffset; + public static synthetic fun parseOrNull$default (Lkotlinx/datetime/UtcOffset$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/UtcOffset; } public final class kotlinx/datetime/UtcOffsetKt { @@ -623,6 +635,7 @@ public final class kotlinx/datetime/format/DateTimeComponents$Formats { public final class kotlinx/datetime/format/DateTimeComponentsKt { public static final fun format (Lkotlinx/datetime/format/DateTimeFormat;Lkotlin/jvm/functions/Function1;)Ljava/lang/String; public static final fun parse (Lkotlinx/datetime/format/DateTimeComponents$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/format/DateTimeComponents; + public static final fun parseOrNull (Lkotlinx/datetime/format/DateTimeComponents$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/format/DateTimeComponents; } public abstract interface class kotlinx/datetime/format/DateTimeFormat { diff --git a/core/api/kotlinx-datetime.klib.api b/core/api/kotlinx-datetime.klib.api index 129b736d..ebe671a5 100644 --- a/core/api/kotlinx-datetime.klib.api +++ b/core/api/kotlinx-datetime.klib.api @@ -269,6 +269,7 @@ final class kotlinx.datetime/DatePeriod : kotlinx.datetime/DateTimePeriod { // k final object Companion { // kotlinx.datetime/DatePeriod.Companion|null[0] final fun parse(kotlin/String): kotlinx.datetime/DatePeriod // kotlinx.datetime/DatePeriod.Companion.parse|parse(kotlin.String){}[0] + final fun parseOrNull(kotlin/String): kotlinx.datetime/DatePeriod? // kotlinx.datetime/DatePeriod.Companion.parseOrNull|parseOrNull(kotlin.String){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/DatePeriod.Companion.serializer|serializer(){}[0] } } @@ -533,6 +534,7 @@ sealed class kotlinx.datetime/DateTimePeriod { // kotlinx.datetime/DateTimePerio final object Companion { // kotlinx.datetime/DateTimePeriod.Companion|null[0] final fun parse(kotlin/String): kotlinx.datetime/DateTimePeriod // kotlinx.datetime/DateTimePeriod.Companion.parse|parse(kotlin.String){}[0] + final fun parseOrNull(kotlin/String): kotlinx.datetime/DateTimePeriod? // kotlinx.datetime/DateTimePeriod.Companion.parseOrNull|parseOrNull(kotlin.String){}[0] final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/DateTimePeriod.Companion.serializer|serializer(){}[0] } } @@ -826,6 +828,7 @@ final fun (kotlin/String).kotlinx.datetime/toLocalDate(): kotlinx.datetime/Local final fun (kotlin/String).kotlinx.datetime/toLocalDateTime(): kotlinx.datetime/LocalDateTime // kotlinx.datetime/toLocalDateTime|toLocalDateTime@kotlin.String(){}[0] final fun (kotlin/String).kotlinx.datetime/toLocalTime(): kotlinx.datetime/LocalTime // kotlinx.datetime/toLocalTime|toLocalTime@kotlin.String(){}[0] final fun (kotlinx.datetime.format/DateTimeComponents.Companion).kotlinx.datetime.format/parse(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat): kotlinx.datetime.format/DateTimeComponents // kotlinx.datetime.format/parse|parse@kotlinx.datetime.format.DateTimeComponents.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] +final fun (kotlinx.datetime.format/DateTimeComponents.Companion).kotlinx.datetime.format/parseOrNull(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat): kotlinx.datetime.format/DateTimeComponents? // kotlinx.datetime.format/parseOrNull|parseOrNull@kotlinx.datetime.format.DateTimeComponents.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun (kotlinx.datetime.format/DateTimeFormat).kotlinx.datetime.format/format(kotlin/Function1): kotlin/String // kotlinx.datetime.format/format|format@kotlinx.datetime.format.DateTimeFormat(kotlin.Function1){}[0] final fun (kotlinx.datetime.format/DateTimeFormatBuilder).kotlinx.datetime.format/byUnicodePattern(kotlin/String) // kotlinx.datetime.format/byUnicodePattern|byUnicodePattern@kotlinx.datetime.format.DateTimeFormatBuilder(kotlin.String){}[0] final fun (kotlinx.datetime.format/DateTimeFormatBuilder).kotlinx.datetime.format/char(kotlin/Char) // kotlinx.datetime.format/char|char@kotlinx.datetime.format.DateTimeFormatBuilder(kotlin.Char){}[0] @@ -860,6 +863,7 @@ final fun (kotlinx.datetime/Instant).kotlinx.datetime/toLocalDateTime(kotlinx.da final fun (kotlinx.datetime/Instant).kotlinx.datetime/until(kotlinx.datetime/Instant, kotlinx.datetime/DateTimeUnit, kotlinx.datetime/TimeZone): kotlin/Long // kotlinx.datetime/until|until@kotlinx.datetime.Instant(kotlinx.datetime.Instant;kotlinx.datetime.DateTimeUnit;kotlinx.datetime.TimeZone){}[0] final fun (kotlinx.datetime/Instant).kotlinx.datetime/until(kotlinx.datetime/Instant, kotlinx.datetime/DateTimeUnit.TimeBased): kotlin/Long // kotlinx.datetime/until|until@kotlinx.datetime.Instant(kotlinx.datetime.Instant;kotlinx.datetime.DateTimeUnit.TimeBased){}[0] final fun (kotlinx.datetime/Instant).kotlinx.datetime/yearsUntil(kotlinx.datetime/Instant, kotlinx.datetime/TimeZone): kotlin/Int // kotlinx.datetime/yearsUntil|yearsUntil@kotlinx.datetime.Instant(kotlinx.datetime.Instant;kotlinx.datetime.TimeZone){}[0] +final fun (kotlinx.datetime/Instant.Companion).kotlinx.datetime/parseOrNull(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/Instant? // kotlinx.datetime/parseOrNull|parseOrNull@kotlinx.datetime.Instant.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/atStartOfDayIn(kotlinx.datetime/TimeZone): kotlinx.datetime/Instant // kotlinx.datetime/atStartOfDayIn|atStartOfDayIn@kotlinx.datetime.LocalDate(kotlinx.datetime.TimeZone){}[0] final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/atTime(kotlin/Int, kotlin/Int, kotlin/Int = ..., kotlin/Int = ...): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atTime|atTime@kotlinx.datetime.LocalDate(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/atTime(kotlinx.datetime/LocalTime): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atTime|atTime@kotlinx.datetime.LocalDate(kotlinx.datetime.LocalTime){}[0] @@ -878,18 +882,22 @@ final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/plus(kotlinx.datetime/Da final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/plus(kotlinx.datetime/DateTimeUnit.DateBased): kotlinx.datetime/LocalDate // kotlinx.datetime/plus|plus@kotlinx.datetime.LocalDate(kotlinx.datetime.DateTimeUnit.DateBased){}[0] final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/until(kotlinx.datetime/LocalDate, kotlinx.datetime/DateTimeUnit.DateBased): kotlin/Long // kotlinx.datetime/until|until@kotlinx.datetime.LocalDate(kotlinx.datetime.LocalDate;kotlinx.datetime.DateTimeUnit.DateBased){}[0] final fun (kotlinx.datetime/LocalDate).kotlinx.datetime/yearsUntil(kotlinx.datetime/LocalDate): kotlin/Int // kotlinx.datetime/yearsUntil|yearsUntil@kotlinx.datetime.LocalDate(kotlinx.datetime.LocalDate){}[0] +final fun (kotlinx.datetime/LocalDate.Companion).kotlinx.datetime/parseOrNull(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/LocalDate? // kotlinx.datetime/parseOrNull|parseOrNull@kotlinx.datetime.LocalDate.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun (kotlinx.datetime/LocalDateTime).kotlinx.datetime/format(kotlinx.datetime.format/DateTimeFormat): kotlin/String // kotlinx.datetime/format|format@kotlinx.datetime.LocalDateTime(kotlinx.datetime.format.DateTimeFormat){}[0] final fun (kotlinx.datetime/LocalDateTime).kotlinx.datetime/toInstant(kotlinx.datetime/TimeZone): kotlinx.datetime/Instant // kotlinx.datetime/toInstant|toInstant@kotlinx.datetime.LocalDateTime(kotlinx.datetime.TimeZone){}[0] final fun (kotlinx.datetime/LocalDateTime).kotlinx.datetime/toInstant(kotlinx.datetime/UtcOffset): kotlinx.datetime/Instant // kotlinx.datetime/toInstant|toInstant@kotlinx.datetime.LocalDateTime(kotlinx.datetime.UtcOffset){}[0] +final fun (kotlinx.datetime/LocalDateTime.Companion).kotlinx.datetime/parseOrNull(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/LocalDateTime? // kotlinx.datetime/parseOrNull|parseOrNull@kotlinx.datetime.LocalDateTime.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlin/Int, kotlin/Int, kotlin/Int): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlin.Int;kotlin.Int;kotlin.Int){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Unit = ...): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Unit){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlin/Int, kotlinx.datetime/Month, kotlin/Int): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlin.Int;kotlinx.datetime.Month;kotlin.Int){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlin/Int, kotlinx.datetime/Month, kotlin/Int, kotlin/Unit = ...): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlin.Int;kotlinx.datetime.Month;kotlin.Int;kotlin.Unit){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlinx.datetime/LocalDate): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlinx.datetime.LocalDate){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/format(kotlinx.datetime.format/DateTimeFormat): kotlin/String // kotlinx.datetime/format|format@kotlinx.datetime.LocalTime(kotlinx.datetime.format.DateTimeFormat){}[0] +final fun (kotlinx.datetime/LocalTime.Companion).kotlinx.datetime/parseOrNull(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/LocalTime? // kotlinx.datetime/parseOrNull|parseOrNull@kotlinx.datetime.LocalTime.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun (kotlinx.datetime/TimeZone).kotlinx.datetime/offsetAt(kotlinx.datetime/Instant): kotlinx.datetime/UtcOffset // kotlinx.datetime/offsetAt|offsetAt@kotlinx.datetime.TimeZone(kotlinx.datetime.Instant){}[0] final fun (kotlinx.datetime/UtcOffset).kotlinx.datetime/asTimeZone(): kotlinx.datetime/FixedOffsetTimeZone // kotlinx.datetime/asTimeZone|asTimeZone@kotlinx.datetime.UtcOffset(){}[0] final fun (kotlinx.datetime/UtcOffset).kotlinx.datetime/format(kotlinx.datetime.format/DateTimeFormat): kotlin/String // kotlinx.datetime/format|format@kotlinx.datetime.UtcOffset(kotlinx.datetime.format.DateTimeFormat){}[0] +final fun (kotlinx.datetime/UtcOffset.Companion).kotlinx.datetime/parseOrNull(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/UtcOffset? // kotlinx.datetime/parseOrNull|parseOrNull@kotlinx.datetime.UtcOffset.Companion(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] final fun <#A: kotlinx.datetime.format/DateTimeFormatBuilder> (#A).kotlinx.datetime.format/alternativeParsing(kotlin/Array>..., kotlin/Function1<#A, kotlin/Unit>) // kotlinx.datetime.format/alternativeParsing|alternativeParsing@0:0(kotlin.Array>...;kotlin.Function1<0:0,kotlin.Unit>){0§}[0] final fun <#A: kotlinx.datetime.format/DateTimeFormatBuilder> (#A).kotlinx.datetime.format/optional(kotlin/String = ..., kotlin/Function1<#A, kotlin/Unit>) // kotlinx.datetime.format/optional|optional@0:0(kotlin.String;kotlin.Function1<0:0,kotlin.Unit>){0§}[0] final fun kotlinx.datetime/DateTimePeriod(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Long = ...): kotlinx.datetime/DateTimePeriod // kotlinx.datetime/DateTimePeriod|DateTimePeriod(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Long){}[0] diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index b35908cd..de91fab4 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -232,11 +232,72 @@ public sealed class DateTimePeriod { * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [DateTimePeriod] are * exceeded. + * @see parseOrNull for a function that returns `null` instead of throwing an exception. * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.parsing */ - public fun parse(text: String): DateTimePeriod { - fun parseException(message: String, position: Int): Nothing = - throw DateTimeFormatException("Parse error at char $position: $message") + public fun parse(text: String): DateTimePeriod = parseImpl(text, { message, position -> + throw DateTimeFormatException("Parse error at char $position: $message") + }, ::DateTimePeriod) + + /** + * Parses a ISO 8601 duration string as a [DateTimePeriod] or returns `null` if the string could not be parsed + * into a [DateTimePeriod]. + * + * If the time components are absent or equal to zero, returns a [DatePeriod]. + * + * Note that the ISO 8601 duration is not the same as [Duration], + * but instead includes the date components, like [DateTimePeriod] does. + * + * Examples of durations in the ISO 8601 format: + * - `P1Y40D` is one year and 40 days + * - `-P1DT1H` is minus (one day and one hour) + * - `P1DT-1H` is one day minus one hour + * - `-PT0.000000001S` is minus one nanosecond + * + * The format is defined as follows: + * - First, optionally, a `-` or `+`. + * If `-` is present, the whole period after the `-` is negated: `-P-2M1D` is the same as `P2M-1D`. + * - Then, the letter `P`. + * - Optionally, the number of years, followed by `Y`. + * - Optionally, the number of months, followed by `M`. + * - Optionally, the number of weeks, followed by `W`. + * - Optionally, the number of days, followed by `D`. + * - The string can end here if there are no more time components. + * If there are time components, the letter `T` is required. + * - Optionally, the number of hours, followed by `H`. + * - Optionally, the number of minutes, followed by `M`. + * - Optionally, the number of seconds, followed by `S`. + * Seconds can optionally have a fractional part with up to nine digits. + * The fractional part is separated with a `.`. + * + * An explicit `+` or `-` sign can be prepended to any number. + * `-` means that the number is negative, and `+` has no effect. + * + * See ISO-8601-1:2019, 5.5.2.2a) and 5.5.2.2b). + * We combine the two formats into one by allowing the number of weeks to go after the number of months + * and before the number of days. + * + * @see parse for a function that throws an exception when the string is not in the correct format or the + * boundaries of [DateTimePeriod] are exceeded. + * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.parseOrNull + */ + public fun parseOrNull(text: String): DateTimePeriod? = parseImpl(text, { _, _ -> + return null + }) { years, months, days, hours, minutes, seconds, nanoseconds -> + try { + DateTimePeriod(years, months, days, hours, minutes, seconds, nanoseconds) + } catch (_: IllegalArgumentException) { + null + } + } + + private inline fun parseImpl( + text: String, + parseException: (message: String, position: Int) -> Nothing, + construct: ( + years: Int, months: Int, days: Int, hours: Int, minutes: Int, seconds: Int, nanoseconds: Long + ) -> T + ): T { val START = 0 val AFTER_P = 1 val AFTER_YEAR = 2 @@ -273,7 +334,7 @@ public sealed class DateTimePeriod { } if (!someComponentParsed) parseException("At least one component is required, but none were found", 0) - return DateTimePeriod(years, months, daysTotal, hours, minutes, seconds, nanoseconds.toLong()) + return construct(years, months, daysTotal, hours, minutes, seconds, nanoseconds.toLong()) } if (state == START) { if (i + 1 >= text.length && (text[i] == '+' || text[i] == '-')) @@ -324,17 +385,12 @@ public sealed class DateTimePeriod { if (i == text.length) parseException("Expected a designator after the numerical value", i) val wrongOrder = "Wrong component order: should be 'Y', 'M', 'W', 'D', then designator 'T', then 'H', 'M', 'S'" - fun Long.toIntThrowing(component: Char): Int { - if (this < Int.MIN_VALUE || this > Int.MAX_VALUE) - parseException("Value $this does not fit into an Int, which is required for component '$component'", iStart) - return toInt() - } when (text[i].uppercaseChar()) { 'Y' -> { if (state >= AFTER_YEAR) parseException(wrongOrder, i) state = AFTER_YEAR - years = number.toIntThrowing('Y') + years = number.toIntThrowing('Y', iStart, parseException) } 'M' -> { if (state >= AFTER_T) { @@ -342,38 +398,38 @@ public sealed class DateTimePeriod { if (state >= AFTER_MINUTE) parseException(wrongOrder, i) state = AFTER_MINUTE - minutes = number.toIntThrowing('M') + minutes = number.toIntThrowing('M', iStart, parseException) } else { // Months if (state >= AFTER_MONTH) parseException(wrongOrder, i) state = AFTER_MONTH - months = number.toIntThrowing('M') + months = number.toIntThrowing('M', iStart, parseException) } } 'W' -> { if (state >= AFTER_WEEK) parseException(wrongOrder, i) state = AFTER_WEEK - weeks = number.toIntThrowing('W') + weeks = number.toIntThrowing('W', iStart, parseException) } 'D' -> { if (state >= AFTER_DAY) parseException(wrongOrder, i) state = AFTER_DAY - days = number.toIntThrowing('D') + days = number.toIntThrowing('D', iStart, parseException) } 'H' -> { if (state >= AFTER_HOUR || state < AFTER_T) parseException(wrongOrder, i) state = AFTER_HOUR - hours = number.toIntThrowing('H') + hours = number.toIntThrowing('H', iStart, parseException) } 'S' -> { if (state >= AFTER_SECOND_AND_NANO || state < AFTER_T) parseException(wrongOrder, i) state = AFTER_SECOND_AND_NANO - seconds = number.toIntThrowing('S') + seconds = number.toIntThrowing('S', iStart, parseException) } '.', ',' -> { i += 1 @@ -392,12 +448,22 @@ public sealed class DateTimePeriod { if (state >= AFTER_SECOND_AND_NANO || state < AFTER_T) parseException(wrongOrder, i) state = AFTER_SECOND_AND_NANO - seconds = number.toIntThrowing('S') + seconds = number.toIntThrowing('S', iStart, parseException) } else -> parseException("Expected a designator after the numerical value", i) } i += 1 - } + } + } + + private inline fun Long.toIntThrowing( + component: Char, iStart: Int, parseException: (message: String, position: Int) -> Nothing + ): Int { + if (this < Int.MIN_VALUE || this > Int.MAX_VALUE) + parseException( + "Value $this does not fit into an Int, which is required for component '$component'", iStart + ) + return toInt() } } } @@ -478,6 +544,7 @@ public class DatePeriod internal constructor( * or any time components are not zero. * * @see DateTimePeriod.parse + * @see parseOrNull for a version of this function that returns `null` instead of throwing exceptions * @sample kotlinx.datetime.test.samples.DatePeriodSamples.parsing */ public fun parse(text: String): DatePeriod = @@ -485,6 +552,19 @@ public class DatePeriod internal constructor( is DatePeriod -> period else -> throw DateTimeFormatException("Period $period (parsed from string $text) is not date-based") } + + /** + * Parses the ISO 8601 duration representation as a [DatePeriod], for example, `P1Y2M30D`, or returns `null` + * if the string does not represent a valid [DatePeriod]. + * + * This function is equivalent to [DateTimePeriod.parse], but will fail if any of the time components are not + * zero. + * + * @see DateTimePeriod.parseOrNull + * @see parse for a version of this function that throws on incorrect input + * @sample kotlinx.datetime.test.samples.DatePeriodSamples.parseOrNull + */ + public fun parseOrNull(text: String): DatePeriod? = DateTimePeriod.parseOrNull(text) as? DatePeriod } } diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 3deedd94..c82e512a 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -378,6 +378,7 @@ public expect class Instant : Comparable { * * @see Instant.toString for formatting using the default format. * @see Instant.format for formatting using a custom format. + * @see Instant.Companion.parseOrNull for a variant that returns `null` on failure. * @sample kotlinx.datetime.test.samples.InstantSamples.parsing */ public fun parse( @@ -798,5 +799,37 @@ public fun Instant.format(format: DateTimeFormat, offset: Ut return format.format { setDateTimeOffset(instant, offset) } } +/** + * A shortcut for calling [DateTimeFormat.parseOrNull], followed by [DateTimeComponents.toInstantUsingOffset], + * returning `null` if any of the two operations fail. + * + * Parses a string that represents an instant, including date and time components and a mandatory + * time zone offset and returns the parsed [Instant] value. + * + * The string is considered to represent time on the UTC-SLS time scale instead of UTC. + * In practice, this means that, even if there is a leap second on the given day, it will not affect how the + * time is parsed, even if it's in the last 1000 seconds of the day. + * Instead, even if there is a negative leap second on the given day, 23:59:59 is still considered a valid time. + * 23:59:60 is invalid on UTC-SLS, so parsing it will fail. + * + * If the format is not specified, [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] is used. + * `2023-01-02T23:40:57.120Z` is an example of a string in this format. + * + * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded. + * + * @see Instant.toString for formatting using the default format. + * @see Instant.format for formatting using a custom format. + * @see Instant.parse for a variant that throws an exception on failure. + * @sample kotlinx.datetime.test.samples.InstantSamples.parseOrNull + */ +public fun Instant.Companion.parseOrNull( + input: CharSequence, + format: DateTimeFormat = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET +): Instant? = try { + format.parseOrNull(input)?.toInstantUsingOffset() +} catch (e: IllegalArgumentException) { + null +} + internal const val DISTANT_PAST_SECONDS = -3217862419201 internal const val DISTANT_FUTURE_SECONDS = 3093527980800 diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 5225f2be..55d863f3 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -5,6 +5,9 @@ package kotlinx.datetime +import kotlinx.datetime.LocalDate.Companion.Format +import kotlinx.datetime.LocalDate.Formats +import kotlinx.datetime.LocalDateTime.Companion.Format import kotlinx.datetime.format.* import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable @@ -49,11 +52,12 @@ import kotlin.internal.* * [toEpochDays] is the inverse operation. * See sample 2. * - * [parse] and [toString] methods can be used to obtain a [LocalDate] from and convert it to a string in the - * ISO 8601 extended format. + * [parse], [parseOrNull][LocalDate.Companion.parseOrNull], and [toString] methods + * can be used to obtain a [LocalDate] from and convert it to a string in the ISO 8601 extended format. * See sample 3. * - * [parse] and [LocalDate.format] both support custom formats created with [Format] or defined in [Formats]. + * [parse], [parseOrNull][LocalDate.Companion.parseOrNull], and [LocalDate.format] + * all support custom formats created with [Format] or defined in [Formats]. * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDate]: @@ -76,10 +80,13 @@ public expect class LocalDate : Comparable { * If [format] is not specified, [Formats.ISO] is used. * `2023-01-02` is an example of a string in this format. * + * See [Formats] and [Format] for predefined and custom formats. + * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDate] are exceeded. * * @see LocalDate.toString for formatting using the default format. * @see LocalDate.format for formatting using a custom format. + * @see LocalDate.Companion.parseOrNull for a version of this function that returns `null` on faulty input. * @sample kotlinx.datetime.test.samples.LocalDateSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoDateFormat()): LocalDate @@ -267,6 +274,26 @@ public expect class LocalDate : Comparable { public override fun toString(): String } +/** + * A shortcut for calling [DateTimeFormat.parseOrNull]. + * + * Parses a string that represents a date and returns the parsed [LocalDate] value, + * or `null` if the string does not match the format or does not represent a valid [LocalDate]. + * + * If [format] is not specified, [LocalDate.Formats.ISO] is used. + * `2023-01-02` is an example of a string in this format. + * + * See [LocalDate.Formats] and [LocalDate.Format] for predefined and custom formats. + * + * @see LocalDate.toString for formatting using the default format. + * @see LocalDate.format for formatting using a custom format. + * @see LocalDate.parse for a version of this function that throws an exception on faulty input. + * @sample kotlinx.datetime.test.samples.LocalDateSamples.parseOrNull + */ +public expect fun LocalDate.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat = getIsoDateFormat() +): LocalDate? + /** * @suppress */ diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 8e5085ca..cfca17b9 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -91,11 +91,13 @@ import kotlin.jvm.JvmName * Some additional constructors that directly accept the values from date and time fields are provided for convenience. * See sample 2. * - * [parse] and [toString] methods can be used to obtain a [LocalDateTime] from and convert it to a string in the - * ISO 8601 extended format (for example, `2023-01-02T22:35:01`). + * [parse], [parseOrNull][LocalDateTime.Companion.parseOrNull], and [toString] methods + * can be used to obtain a [LocalDateTime] from and convert it to a string in the ISO 8601 extended format + * (for example, `2023-01-02T22:35:01`). * See sample 3. * - * [parse] and [LocalDateTime.format] both support custom formats created with [Format] or defined in [Formats]. + * [parse], [parseOrNull][LocalDateTime.Companion.parseOrNull], and [LocalDateTime.format] + * all support custom formats created with [Format] or defined in [Formats]. * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDateTime]: @@ -128,6 +130,9 @@ public expect class LocalDateTime : Comparable { * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDateTime] are * exceeded. * + * @see LocalDateTime.toString for formatting using the default format. + * @see LocalDateTime.format for formatting using a custom format. + * @see LocalDateTime.Companion.parseOrNull for a version of this function that returns `null` on faulty input. * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoDateTimeFormat()): LocalDateTime @@ -389,6 +394,26 @@ public expect class LocalDateTime : Comparable { public override fun toString(): String } +/** + * A shortcut for calling [DateTimeFormat.parseOrNull]. + * + * Parses a string that represents a date and returns the parsed [LocalDateTime] value, + * or `null` if the string does not match the format or does not represent a valid [LocalDateTime]. + * + * If [format] is not specified, [LocalDateTime.Formats.ISO] is used. + * `2023-01-02T23:40:57.120` is an example of a string in this format. + * + * See [LocalDateTime.Formats] and [LocalDateTime.Format] for predefined and custom formats. + * + * @see LocalDateTime.toString for formatting using the default format. + * @see LocalDateTime.format for formatting using a custom format. + * @see LocalDateTime.parse for a version of this function that throws an exception on faulty input. + * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.parseOrNull + */ +public expect fun LocalDateTime.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat = getIsoDateTimeFormat() +): LocalDateTime? + /** * @suppress */ diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 7cf01459..12abb377 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -65,10 +65,12 @@ import kotlin.jvm.JvmName * [toSecondOfDay], [toMillisecondOfDay], and [toNanosecondOfDay] are the inverse operations. * See sample 2. * - * [parse] and [toString] methods can be used to obtain a [LocalTime] from and convert it to a string in the - * ISO 8601 extended format. See sample 3. + * [parse], [parseOrNull][LocalTime.Companion.parseOrNull], and [toString] methods + * can be used to obtain a [LocalTime] from and convert it to a string in the ISO 8601 extended format. + * See sample 3. * - * [parse] and [LocalTime.format] both support custom formats created with [Format] or defined in [Formats]. + * [parse], [parseOrNull][LocalTime.Companion.parseOrNull], and [LocalTime.format] + * all support custom formats created with [Format] or defined in [Formats]. * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalTime]: @@ -92,11 +94,14 @@ public expect class LocalTime : Comparable { * If [format] is not specified, [Formats.ISO] is used. * `23:40:57.120` is an example of a string in this format. * + * See [Formats] and [Format] for predefined and custom formats. + * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalTime] are * exceeded. * * @see LocalTime.toString for formatting using the default format. * @see LocalTime.format for formatting using a custom format. + * @see LocalTime.Companion.parseOrNull for a version of this function that returns `null` on faulty input. * @sample kotlinx.datetime.test.samples.LocalTimeSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoTimeFormat()): LocalTime @@ -346,6 +351,26 @@ public expect class LocalTime : Comparable { public override fun toString(): String } +/** + * A shortcut for calling [DateTimeFormat.parseOrNull]. + * + * Parses a string that represents a date and returns the parsed [LocalTime] value, + * or `null` if the string does not match the format or does not represent a valid [LocalTime]. + * + * If [format] is not specified, [LocalTime.Formats.ISO] is used. + * `23:40:57.120` is an example of a string in this format. + * + * See [LocalTime.Formats] and [LocalTime.Format] for predefined and custom formats. + * + * @see LocalTime.toString for formatting using the default format. + * @see LocalTime.format for formatting using a custom format. + * @see LocalTime.parse for a version of this function that throws an exception on faulty input. + * @sample kotlinx.datetime.test.samples.LocalTimeSamples.parseOrNull + */ +public expect fun LocalTime.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat = getIsoTimeFormat() +): LocalTime? + /** * Formats this value using the given [format]. * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 97c5e410..e1f38c77 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -40,11 +40,13 @@ import kotlinx.serialization.Serializable * * There is also a [ZERO] constant for the offset of zero. * - * [parse] and [toString] methods can be used to obtain a [UtcOffset] from and convert it to a string in the + * [parse], [parseOrNull][UtcOffset.Companion.parseOrNull], and [toString] methods + * can be used to obtain a [UtcOffset] from and convert it to a string in the * ISO 8601 extended format (for example, `+01:30`). * See sample 2. * - * [parse] and [UtcOffset.format] both support custom formats created with [Format] or defined in [Formats]. + * [parse], [parseOrNull][UtcOffset.Companion.parseOrNull], and [UtcOffset.format] + * all support custom formats created with [Format] or defined in [Formats]. * See sample 3. * * To serialize and deserialize [UtcOffset] values with `kotlinx-serialization`, use the [UtcOffsetSerializer]. @@ -85,6 +87,10 @@ public expect class UtcOffset { * * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [UtcOffset] are * exceeded. + * + * @see UtcOffset.toString for formatting using the default format. + * @see UtcOffset.format for formatting using a custom format. + * @see UtcOffset.Companion.parseOrNull for a version of this function that returns `null` on faulty input. * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.parsing */ public fun parse(input: CharSequence, format: DateTimeFormat = getIsoUtcOffsetFormat()): UtcOffset @@ -184,6 +190,26 @@ public expect class UtcOffset { public override fun toString(): String } +/** + * A shortcut for calling [DateTimeFormat.parseOrNull]. + * + * Parses a string that represents a date and returns the parsed [UtcOffset] value, + * or `null` if the string does not match the format or does not represent a valid [UtcOffset]. + * + * If [format] is not specified, [UtcOffset.Formats.ISO] is used. + * `+01:30` is an example of a string in this format. + * + * See [UtcOffset.Formats] and [UtcOffset.Format] for predefined and custom formats. + * + * @see UtcOffset.toString for formatting using the default format. + * @see UtcOffset.format for formatting using a custom format. + * @see UtcOffset.parse for a version of this function that throws an exception on faulty input. + * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.parseOrNull + */ +public expect fun UtcOffset.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat = getIsoUtcOffsetFormat() +): UtcOffset? + /** * Formats this value using the given [format]. * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 67e43067..1c4ad76c 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -535,6 +535,7 @@ public fun DateTimeFormat.format(block: DateTimeComponents.( * matches. * * @throws IllegalArgumentException if the text does not match the format. + * @see DateTimeComponents.Companion.parseOrNull for a function that returns `null` if the string does not match the pattern * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.parsing */ public fun DateTimeComponents.Companion.parse( @@ -543,6 +544,23 @@ public fun DateTimeComponents.Companion.parse( ): DateTimeComponents = format.parse(input) +/** + * Parses a [DateTimeComponents] from [input] using the given format or returns `null` if the string does not match the + * pattern. + * Equivalent to calling [DateTimeFormat.parseOrNull] on [format] with [input]. + * + * [DateTimeComponents] does not perform any validation, so even invalid values may be parsed successfully if the string pattern + * matches. + * + * @see DateTimeComponents.Companion.parse for a function that throws an exception if the string does not match the pattern + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.parseOrNull + */ +public fun DateTimeComponents.Companion.parseOrNull( + input: CharSequence, + format: DateTimeFormat +): DateTimeComponents? = + format.parseOrNull(input) + internal class DateTimeComponentsContents internal constructor( val date: IncompleteLocalDate = IncompleteLocalDate(), val time: IncompleteLocalTime = IncompleteLocalTime(), diff --git a/core/common/test/DateTimePeriodTest.kt b/core/common/test/DateTimePeriodTest.kt index 3aec7b8f..5a4e576f 100644 --- a/core/common/test/DateTimePeriodTest.kt +++ b/core/common/test/DateTimePeriodTest.kt @@ -76,64 +76,69 @@ class DateTimePeriodTest { assertEquals("P1DT-0.999999999S", DateTimePeriod(days = 1, seconds = -1, nanoseconds = 1L).toString()) } + private inline fun assertFailsToParse(text: String): T { + assertNull(DateTimePeriod.parseOrNull(text)) + return assertFailsWith { DateTimePeriod.parse(text) } + } + @Test fun parseIsoString() { - assertEquals(DateTimePeriod(years = 1), DateTimePeriod.parse("P1Y")) - assertEquals(DatePeriod(years = 1, months = 1), DateTimePeriod.parse("P1Y1M")) - assertEquals(DateTimePeriod(months = 11), DateTimePeriod.parse("P11M")) - assertEquals(DateTimePeriod(months = 10, days = 5), DateTimePeriod.parse("P10M5D")) - assertEquals(DateTimePeriod(years = 1, days = 40), DateTimePeriod.parse("P1Y40D")) - - assertEquals(DateTimePeriod(months = 14), DateTimePeriod.parse("P14M")) - assertPeriodComponents(DateTimePeriod.parse("P14M") as DatePeriod, years = 1, months = 2) - - assertEquals(DateTimePeriod(hours = 1), DateTimePeriod.parse("PT1H")) - assertEquals(DateTimePeriod(), DateTimePeriod.parse("P0D")) - assertEquals(DatePeriod(), DateTimePeriod.parse("P0D")) + fun assertParsesTo(value: DateTimePeriod, string: String) { + assertEquals(value, DateTimePeriod.parse(string)) + assertEquals(value, DateTimePeriod.parseOrNull(string)) + } - assertEquals(DateTimePeriod(days = 1, hours = -1), DateTimePeriod.parse("P1DT-1H")) - assertEquals(DateTimePeriod(days = -1, hours = -1), DateTimePeriod.parse("-P1DT1H")) - assertEquals(DateTimePeriod(months = -1), DateTimePeriod.parse("-P1M")) + assertParsesTo(DateTimePeriod(years = 1), "P1Y") + assertParsesTo(DatePeriod(years = 1, months = 1), "P1Y1M") + assertParsesTo(DateTimePeriod(months = 11), "P11M") + assertParsesTo(DateTimePeriod(months = 10, days = 5), "P10M5D") + assertParsesTo(DateTimePeriod(years = 1, days = 40), "P1Y40D") - assertEquals(DateTimePeriod(years = -1, months = -2, days = -3, hours = -4, minutes = -5, seconds = 0, nanoseconds = 500_000_000), - DateTimePeriod.parse("P-1Y-2M-3DT-4H-5M0.500000000S")) + // Successful parsing tests + assertParsesTo(DateTimePeriod(months = 14), "P14M") + assertPeriodComponents(DateTimePeriod.parse("P14M") as DatePeriod, years = 1, months = 2) + assertParsesTo(DateTimePeriod(hours = 1), "PT1H") + assertParsesTo(DateTimePeriod(), "P0D") + assertParsesTo(DatePeriod(), "P0D") + assertParsesTo(DateTimePeriod(days = 1, hours = -1), "P1DT-1H") + assertParsesTo(DateTimePeriod(days = -1, hours = -1), "-P1DT1H") + assertParsesTo(DateTimePeriod(months = -1), "-P1M") + assertParsesTo(DateTimePeriod(years = -1, months = -2, days = -3, hours = -4, minutes = -5, seconds = 0, nanoseconds = 500_000_000), + "P-1Y-2M-3DT-4H-5M0.500000000S") assertPeriodComponents(DateTimePeriod.parse("P-1Y-2M-3DT-4H-5M0.500000000S"), years = -1, months = -2, days = -3, hours = -4, minutes = -4, seconds = -59, nanoseconds = -500_000_000) - - assertEquals(DateTimePeriod(nanoseconds = 999_999_999_999_999L), DateTimePeriod.parse("PT277H46M39.999999999S")) + assertParsesTo(DateTimePeriod(nanoseconds = 999_999_999_999_999L), "PT277H46M39.999999999S") assertPeriodComponents(DateTimePeriod.parse("PT277H46M39.999999999S"), hours = 277, minutes = 46, seconds = 39, nanoseconds = 999_999_999) - - assertEquals(DateTimePeriod(nanoseconds = 999_999_999), DateTimePeriod.parse("PT0.999999999S")) - assertEquals(DateTimePeriod(nanoseconds = -1), DateTimePeriod.parse("-PT0.000000001S")) - assertEquals(DateTimePeriod(days = 1, nanoseconds = -1), DateTimePeriod.parse("P1DT-0.000000001S")) - assertEquals(DateTimePeriod(nanoseconds = -999_999_999), DateTimePeriod.parse("-PT0.999999999S")) - assertEquals(DateTimePeriod(days = 1, nanoseconds = -999_999_999), DateTimePeriod.parse("P1DT-0.999999999S")) + assertParsesTo(DateTimePeriod(nanoseconds = 999_999_999), "PT0.999999999S") + assertParsesTo(DateTimePeriod(nanoseconds = -1), "-PT0.000000001S") + assertParsesTo(DateTimePeriod(days = 1, nanoseconds = -1), "P1DT-0.000000001S") + assertParsesTo(DateTimePeriod(nanoseconds = -999_999_999), "-PT0.999999999S") + assertParsesTo(DateTimePeriod(days = 1, nanoseconds = -999_999_999), "P1DT-0.999999999S") assertPeriodComponents(DateTimePeriod.parse("P1DT-0.999999999S"), days = 1, nanoseconds = -999_999_999) - assertEquals(DatePeriod(days = 1), DateTimePeriod.parse("P000000000000000000000000000001D")) - - assertFailsWith { DateTimePeriod.parse("P") } + assertParsesTo(DatePeriod(days = 1), "P000000000000000000000000000001D") + // Failing parsing tests - IllegalArgumentException + assertFailsToParse("P") // overflow of `Int.MAX_VALUE` years - assertFailsWith { DateTimePeriod.parse("P768614336404564651Y") } - assertFailsWith { DateTimePeriod.parse("P1Y9223372036854775805M") } - - assertFailsWith { DateTimePeriod.parse("PT+-2H") } - - // too large a number in a field - assertFailsWith { DateTimePeriod.parse("P3000000000Y") } - assertFailsWith { DateTimePeriod.parse("P3000000000M") } - assertFailsWith { DateTimePeriod.parse("P3000000000D") } - assertFailsWith { DateTimePeriod.parse("P3000000000H") } - assertFailsWith { DateTimePeriod.parse("P3000000000M") } - assertFailsWith { DateTimePeriod.parse("P3000000000S") } + assertFailsToParse("P768614336404564651Y") + assertFailsToParse("P1Y9223372036854775805M") + assertFailsToParse("PT+-2H") + + // Failing parsing tests - DateTimeFormatException + assertFailsToParse("P3000000000Y") + assertFailsToParse("P3000000000M") + assertFailsToParse("P3000000000D") + assertFailsToParse("P3000000000H") + assertFailsToParse("P3000000000M") + assertFailsToParse("P3000000000S") // wrong order of signifiers - assertFailsWith { DateTimePeriod.parse("P1Y2D3M") } - assertFailsWith { DateTimePeriod.parse("P0DT1M2H") } + assertFailsToParse("P1Y2D3M") + assertFailsToParse("P0DT1M2H") // loss of precision in fractional seconds - assertFailsWith { DateTimePeriod.parse("P0.000000000001S") } + assertFailsToParse("P0.000000000001S") // non-zero time components when parsing DatePeriod assertFailsWith { DatePeriod.parse("P1DT1H") } diff --git a/core/common/test/LocalDateTest.kt b/core/common/test/LocalDateTest.kt index f6ded203..02956cd6 100644 --- a/core/common/test/LocalDateTest.kt +++ b/core/common/test/LocalDateTest.kt @@ -38,17 +38,18 @@ class LocalDateTest { @Test fun parseIsoString() { fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, dayOfWeek: Int? = null, dayOfYear: Int? = null) { + val result = LocalDate.parse(value) checkComponents(LocalDate.parse(value), year, month, day, dayOfWeek, dayOfYear) + assertEquals(result, LocalDate.parseOrNull(value)) assertEquals(value, LocalDate(year, month, day).toString()) } checkParsedComponents("2019-10-01", 2019, 10, 1, 2, 274) checkParsedComponents("2016-02-29", 2016, 2, 29, 1, 60) checkParsedComponents("2017-10-01", 2017, 10, 1, 7, 274) - assertInvalidFormat { LocalDate.parse("102017-10-01") } - assertInvalidFormat { LocalDate.parse("2017--10-01") } - assertInvalidFormat { LocalDate.parse("2017-+10-01") } - assertInvalidFormat { LocalDate.parse("2017-10-+01") } - assertInvalidFormat { LocalDate.parse("2017-10--01") } + invalidDateStrings.forEach { + assertInvalidFormat { LocalDate.parse(it) } + assertNull(LocalDate.parseOrNull(it), it) + } // this date is currently larger than the largest representable one any of the platforms: assertInvalidFormat { LocalDate.parse("+1000000000-10-01") } // threetenbp @@ -314,3 +315,18 @@ private val LocalDate.previous: LocalDate get() = } else { LocalDate(year - 1, 12, 31) } + +val invalidDateStrings = listOf( + "102017-10-01", + "2017--10-01", + "2017-+10-01", + "2017-10-+01", + "2017-10--01", + "2017-00-01", + "2017-13-01", + "2017-9-00", + "2017-10-00", + "2017-10-32", + "2017-10-01T00:00", + "2021-02-29", +) \ No newline at end of file diff --git a/core/common/test/LocalDateTimeTest.kt b/core/common/test/LocalDateTimeTest.kt index fff490a3..ebbd583c 100644 --- a/core/common/test/LocalDateTimeTest.kt +++ b/core/common/test/LocalDateTimeTest.kt @@ -19,20 +19,35 @@ class LocalDateTimeTest { @Test fun localDateTimeParsing() { fun checkParsedComponents(value: String, year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int, dayOfWeek: Int? = null, dayOfYear: Int? = null) { - checkComponents(value.toLocalDateTime(), year, month, day, hour, minute, second, nanosecond, dayOfWeek, dayOfYear) + val result = LocalDateTime.parse(value) + checkComponents(result, year, month, day, hour, minute, second, nanosecond, dayOfWeek, dayOfYear) + assertEquals(result, LocalDateTime.parseOrNull(value)) + assertEquals(value, result.toString()) + } + fun assertInvalidLocalDateTimeString(string: String) { + assertInvalidFormat { LocalDateTime.parse(string) } + assertNull(LocalDateTime.parseOrNull(string), string) } checkParsedComponents("2019-10-01T18:43:15.100500", 2019, 10, 1, 18, 43, 15, 100500000, 2, 274) checkParsedComponents("2019-10-01T18:43:15", 2019, 10, 1, 18, 43, 15, 0, 2, 274) checkParsedComponents("2019-10-01T18:12", 2019, 10, 1, 18, 12, 0, 0, 2, 274) + invalidDateStrings.forEach { dateString -> + assertInvalidLocalDateTimeString("${dateString}T18:43:15") + } + invalidTimeStrings.forEach { timeString -> + assertInvalidLocalDateTimeString("2024-01-01T${timeString}") + } - assertFailsWith { LocalDateTime.parse("x") } - assertFailsWith { "+1000000000-03-26T04:00:00".toLocalDateTime() } + assertInvalidLocalDateTimeString("x") + assertInvalidLocalDateTimeString("+1000000000-03-26T04:00:00") for (i in 1..30) { checkComponents(LocalDateTime.parse("+${"0".repeat(i)}2024-01-01T23:59"), 2024, 1, 1, 23, 59) checkComponents(LocalDateTime.parse("-${"0".repeat(i)}2024-01-01T23:59:03"), -2024, 1, 1, 23, 59, 3) } + + /* Based on the ThreeTenBp project. * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ @@ -45,8 +60,8 @@ class LocalDateTimeTest { @Test fun localDtToInstantConversion() { - val ldt1 = "2019-10-01T18:43:15.100500".toLocalDateTime() - val ldt2 = "2019-10-01T19:50:00.500600".toLocalDateTime() + val ldt1 = LocalDateTime.parse("2019-10-01T18:43:15.100500") + val ldt2 = LocalDateTime.parse("2019-10-01T19:50:00.500600") val diff = with(TimeZone.UTC) { ldt2.toInstant() - ldt1.toInstant() } assertEquals(with(Duration) { 1.hours + 7.minutes - 15.seconds + 400100.microseconds }, diff) @@ -56,8 +71,8 @@ class LocalDateTimeTest { @Test fun localDtToInstantConversionRespectsTimezones() { - val ldt1 = "2011-03-26T04:00:00".toLocalDateTime() - val ldt2 = "2011-03-27T04:00:00".toLocalDateTime() + val ldt1 = LocalDateTime.parse("2011-03-26T04:00:00") + val ldt2 = LocalDateTime.parse("2011-03-27T04:00:00") val diff = with(TimeZone.of("Europe/Moscow")) { ldt2.toInstant() - ldt1.toInstant() } assertEquals(23.hours, diff) } diff --git a/core/common/test/LocalTimeTest.kt b/core/common/test/LocalTimeTest.kt index 09f0f5f6..6b648480 100644 --- a/core/common/test/LocalTimeTest.kt +++ b/core/common/test/LocalTimeTest.kt @@ -16,14 +16,24 @@ class LocalTimeTest { @Test fun localTimeParsing() { fun checkParsedComponents(value: String, hour: Int, minute: Int, second: Int, nanosecond: Int) { - checkComponents(value.toLocalTime(), hour, minute, second, nanosecond) + val result = LocalTime.parse(value) + checkComponents(LocalTime.parse(value), hour, minute, second, nanosecond) + assertEquals(result, LocalTime.parseOrNull(value)) + assertEquals(value, result.toString()) + } + fun assertInvalidLocalDateTimeString(string: String) { + assertInvalidFormat { LocalDateTime.parse(string) } + assertNull(LocalDateTime.parseOrNull(string), string) } checkParsedComponents("18:43:15.100500", 18, 43, 15, 100500000) checkParsedComponents("18:43:15", 18, 43, 15, 0) checkParsedComponents("18:12", 18, 12, 0, 0) - assertFailsWith { LocalTime.parse("x") } - assertFailsWith { "+10000000004:00:00".toLocalDateTime() } + invalidTimeStrings.forEach { + assertInvalidFormat { LocalDateTime.parse(it) } + assertNull(LocalDateTime.parseOrNull(it), it) + } + assertFailsWith { LocalDateTime.parse("+10000000004:00:00") } /* Based on the ThreeTenBp project. * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos @@ -233,3 +243,11 @@ private fun LocalTime.plusSeconds(secondsToAdd: Long): LocalTime { val newSecond: Int = newSofd % SECONDS_PER_MINUTE return LocalTime(newHour, newMinute, newSecond, nanosecond) } + +val invalidTimeStrings = listOf( + "x", + "24:43:15", + "18:60:15", + "18:43:60", + "18:43:15.1234567891", +) \ No newline at end of file diff --git a/core/common/test/UtcOffsetTest.kt b/core/common/test/UtcOffsetTest.kt index c9d488fc..d89c0a00 100644 --- a/core/common/test/UtcOffsetTest.kt +++ b/core/common/test/UtcOffsetTest.kt @@ -89,9 +89,11 @@ class UtcOffsetTest { fun invalidUtcOffsetStrings() { for (v in invalidUtcOffsetStrings) { assertFailsWith("Should fail: $v") { UtcOffset.parse(v) } + assertNull(UtcOffset.parseOrNull(v), "Should fail: $v") } for (v in fixedOffsetTimeZoneIds) { assertFailsWith("Time zone name should not be parsed as UtcOffset: $v") { UtcOffset.parse(v) } + assertNull(UtcOffset.parseOrNull(v), "Time zone name should not be parsed as UtcOffset: $v") } } diff --git a/core/common/test/samples/DateTimePeriodSamples.kt b/core/common/test/samples/DateTimePeriodSamples.kt index bd308bc4..8b477d7e 100644 --- a/core/common/test/samples/DateTimePeriodSamples.kt +++ b/core/common/test/samples/DateTimePeriodSamples.kt @@ -87,6 +87,21 @@ class DateTimePeriodSamples { } } + @Test + fun parseOrNull() { + // Parsing a string representation of a DateTimePeriod or failing + val parsedPeriod = DateTimePeriod.parseOrNull("P14M-16DT5H") + check(parsedPeriod != null) + with (parsedPeriod) { + check(years == 1) + check(months == 2) + check(days == -16) + check(hours == 5) + } + check(DateTimePeriod.parseOrNull("this is not a period") == null) + check(DateTimePeriod.parseOrNull("P9999999999999999999999999999M") == null) + } + @Test fun constructorFunction() { // Constructing a DateTimePeriod using its constructor function @@ -145,4 +160,19 @@ class DatePeriodSamples { val datePeriodWithTimeComponents = DatePeriod.parse("P1Y2M3DT1H-60M") check(datePeriodWithTimeComponents == DatePeriod(years = 1, months = 2, days = 3)) } + + @Test + fun parseOrNull() { + // Parsing a string representation of a DatePeriod or failing + with(DatePeriod.parseOrNull("P1Y16M60D")) { + check(this != null) + check(this == DatePeriod(years = 2, months = 4, days = 60)) + } + with(DatePeriod.parseOrNull("P1Y2M3DT1H-60M")) { + check(this != null) + check(this == DatePeriod(years = 1, months = 2, days = 3)) + } + check(DatePeriod.parseOrNull("this is not a period") == null) + check(DatePeriod.parseOrNull("P9999999999999999999999999999M") == null) + } } diff --git a/core/common/test/samples/InstantSamples.kt b/core/common/test/samples/InstantSamples.kt index 8fec354b..10c2ce33 100644 --- a/core/common/test/samples/InstantSamples.kt +++ b/core/common/test/samples/InstantSamples.kt @@ -119,6 +119,14 @@ class InstantSamples { check(Instant.parse("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochSeconds(0)) } + @Test + fun parseOrNull() { + // Parsing an Instant from a string using predefined and custom formats + check(Instant.parseOrNull("1970-01-01T00:00:00Z") == Instant.fromEpochSeconds(0)) + check(Instant.parseOrNull("Thu, 01 Jan 1970 03:30:00 +0330", DateTimeComponents.Formats.RFC_1123) == Instant.fromEpochSeconds(0)) + check(Instant.parseOrNull("The moment of Charlie Chaplin's birth") == null) + } + @Test fun isDistantPast() { // Checking if an instant is so far in the past that it's probably irrelevant diff --git a/core/common/test/samples/LocalDateSamples.kt b/core/common/test/samples/LocalDateSamples.kt index 5beaf929..a7fbceb2 100644 --- a/core/common/test/samples/LocalDateSamples.kt +++ b/core/common/test/samples/LocalDateSamples.kt @@ -29,6 +29,18 @@ class LocalDateSamples { check(LocalDate.parse("Apr 16, 2024", customFormat) == LocalDate(2024, Month.APRIL, 16)) } + @Test + fun parseOrNull() { + // Parsing LocalDate values using predefined and custom formats or failing + check(LocalDate.parseOrNull("2024-04-16") == LocalDate(2024, Month.APRIL, 16)) + val customFormat = LocalDate.Format { + monthName(MonthNames.ENGLISH_ABBREVIATED); char(' '); day(); chars(", "); year() + } + check(LocalDate.parseOrNull("Apr 16, 2024", customFormat) == LocalDate(2024, Month.APRIL, 16)) + check(LocalDate.parseOrNull("no date here", customFormat) == null) + check(LocalDate.parseOrNull("Apr 99, 2024", customFormat) == null) + } + @Test fun fromAndToEpochDays() { // Converting LocalDate values to the number of days since 1970-01-01 and back diff --git a/core/common/test/samples/LocalDateTimeSamples.kt b/core/common/test/samples/LocalDateTimeSamples.kt index eea04c30..e5750ab2 100644 --- a/core/common/test/samples/LocalDateTimeSamples.kt +++ b/core/common/test/samples/LocalDateTimeSamples.kt @@ -48,6 +48,24 @@ class LocalDateTimeSamples { LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_000_000)) } + @Test + fun parseOrNull() { + // Parsing LocalDateTime values using predefined and custom formats or failing + check(LocalDateTime.parseOrNull("2024-02-15T08:30:15.123456789") == + LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_456_789)) + val customFormat = LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second() + char(','); secondFraction(fixedLength = 3) + } + check(LocalDateTime.parseOrNull("2024-02-15 08:30:15,123", customFormat) == + LocalDate(2024, 2, 15).atTime(8, 30, 15, 123_000_000)) + check(LocalDateTime.parseOrNull("2024-02-15 08:30:15", customFormat) == null) + check(LocalDateTime.parseOrNull("2024-02-15 99:99:99,999", customFormat) == null) + + } + @Test fun customFormat() { // Parsing and formatting LocalDateTime values using a custom format diff --git a/core/common/test/samples/LocalTimeSamples.kt b/core/common/test/samples/LocalTimeSamples.kt index 3bbb19e8..fce7248a 100644 --- a/core/common/test/samples/LocalTimeSamples.kt +++ b/core/common/test/samples/LocalTimeSamples.kt @@ -72,6 +72,20 @@ class LocalTimeSamples { check(LocalTime.parse("08:30:15,123", customFormat) == LocalTime(8, 30, 15, 123_000_000)) } + @Test + fun parseOrNull() { + // Parsing a LocalTime from a string using predefined and custom formats + check(LocalTime.parseOrNull("08:30:15.123456789") == LocalTime(8, 30, 15, 123_456_789)) + val customFormat = LocalTime.Format { + hour(); char(':'); minute(); char(':'); second() + alternativeParsing({ char(',') }) { char('.') } // parse either a dot or a comma + secondFraction(fixedLength = 3) + } + check(LocalTime.parseOrNull("08:30:15,123", customFormat) == LocalTime(8, 30, 15, 123_000_000)) + check(LocalTime.parseOrNull("ten past twelve", customFormat) == null) + check(LocalTime.parseOrNull("99:99:99,123", customFormat) == null) + } + @Test fun fromAndToSecondOfDay() { // Converting a LocalTime to the number of seconds since the start of the day and back diff --git a/core/common/test/samples/UtcOffsetSamples.kt b/core/common/test/samples/UtcOffsetSamples.kt index ec651c71..fd81b2c3 100644 --- a/core/common/test/samples/UtcOffsetSamples.kt +++ b/core/common/test/samples/UtcOffsetSamples.kt @@ -64,6 +64,17 @@ class UtcOffsetSamples { check(UtcOffset.parse("+130", customFormat).totalSeconds == 5400) } + @Test + fun parseOrNull() { + // Parsing a UtcOffset from a string using predefined and custom formats or failing + check(UtcOffset.parseOrNull("+01:30")?.totalSeconds == 5400) + check(UtcOffset.parseOrNull("+0130", UtcOffset.Formats.FOUR_DIGITS)?.totalSeconds == 5400) + val customFormat = UtcOffset.Format { offsetHours(Padding.NONE); offsetMinutesOfHour() } + check(UtcOffset.parseOrNull("+130", customFormat)?.totalSeconds == 5400) + check(UtcOffset.parseOrNull("Europe/Berlin", customFormat) == null) + check(UtcOffset.parseOrNull("+9999", customFormat) == null) + } + @Test fun toStringSample() { // Converting a UtcOffset to a string diff --git a/core/common/test/samples/format/DateTimeComponentsSamples.kt b/core/common/test/samples/format/DateTimeComponentsSamples.kt index fbfd5d19..4cc37c2c 100644 --- a/core/common/test/samples/format/DateTimeComponentsSamples.kt +++ b/core/common/test/samples/format/DateTimeComponentsSamples.kt @@ -403,6 +403,47 @@ class DateTimeComponentsSamples { } } + @Test + fun parseOrNull() { + // Parsing partial, complex, or broken data using parseOrNull + // DateTimeComponents can be used to parse complex data that consists of multiple components + val compoundFormat = DateTimeComponents.Format { + date(LocalDate.Formats.ISO) + char(' ') + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + char(' ') + offsetHours(); char(':'); offsetMinutesOfHour(); optional { char(':'); offsetSecondsOfMinute() } + } + val parsedCompoundData = DateTimeComponents.parseOrNull("2023-01-02 03:46:58.531 +03:30", compoundFormat) + check(parsedCompoundData != null) + check(parsedCompoundData.toLocalTime() == LocalTime(3, 46, 58, 531_000_000)) + check(parsedCompoundData.toLocalDate() == LocalDate(2023, 1, 2)) + check(parsedCompoundData.toUtcOffset() == UtcOffset(3, 30)) + check(parsedCompoundData.toInstantUsingOffset() == Instant.parse("2023-01-02T03:46:58.531+03:30")) + // It can also be used to parse partial data that is missing some components + val partialFormat = DateTimeComponents.Format { + year(); char('-'); monthNumber() + } + val parsedPartialData = DateTimeComponents.parseOrNull("2023-01", partialFormat) + check(parsedPartialData != null) + check(parsedPartialData.year == 2023) + check(parsedPartialData.month == Month.JANUARY) + try { + parsedPartialData.toLocalDate() + fail("Expected an exception") + } catch (e: IllegalArgumentException) { + // expected: the day is missing, so LocalDate cannot be constructed + } + // Invalid data can be parsed successfully + val parsedInvalidData = DateTimeComponents.parseOrNull("2025-99", partialFormat) + check(parsedInvalidData != null) + check(parsedInvalidData.year == 2025) + check(parsedInvalidData.monthNumber == 99) + // Non-parseable strings return null without an exception + val invalidData = DateTimeComponents.parseOrNull("invalid date-time", compoundFormat) + check(invalidData == null) + } + class Formats { @Test fun rfc1123parsing() { diff --git a/core/commonKotlin/src/LocalDate.kt b/core/commonKotlin/src/LocalDate.kt index 147b89cd..27fd5cfd 100644 --- a/core/commonKotlin/src/LocalDate.kt +++ b/core/commonKotlin/src/LocalDate.kt @@ -210,6 +210,10 @@ public actual class LocalDate actual constructor(public actual val year: Int, mo actual override fun toString(): String = format(Formats.ISO) } +public actual fun LocalDate.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): LocalDate? = format.parseOrNull(input) + @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plus(1, unit) diff --git a/core/commonKotlin/src/LocalDateTime.kt b/core/commonKotlin/src/LocalDateTime.kt index 9375ae91..710e91e4 100644 --- a/core/commonKotlin/src/LocalDateTime.kt +++ b/core/commonKotlin/src/LocalDateTime.kt @@ -84,6 +84,10 @@ public actual constructor(public actual val date: LocalDate, public actual val t } } +public actual fun LocalDateTime.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): LocalDateTime? = format.parseOrNull(input) + // org.threeten.bp.LocalDateTime#until internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Long { var endDate: LocalDate = other.date diff --git a/core/commonKotlin/src/LocalTime.kt b/core/commonKotlin/src/LocalTime.kt index 28ebd78d..b8681c5c 100644 --- a/core/commonKotlin/src/LocalTime.kt +++ b/core/commonKotlin/src/LocalTime.kt @@ -136,6 +136,10 @@ public actual class LocalTime actual constructor( } +public actual fun LocalTime.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): LocalTime? = format.parseOrNull(input) + internal val ISO_TIME_OPTIONAL_SECONDS_TRAILING_ZEROS by lazy { LocalTimeFormat.build { hour() diff --git a/core/commonKotlin/src/UtcOffset.kt b/core/commonKotlin/src/UtcOffset.kt index e8329d0c..7db7923c 100644 --- a/core/commonKotlin/src/UtcOffset.kt +++ b/core/commonKotlin/src/UtcOffset.kt @@ -93,6 +93,10 @@ public actual class UtcOffset private constructor(public actual val totalSeconds } } +public actual fun UtcOffset.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): UtcOffset? = format.parseOrNull(input) + @ThreadLocal private var utcOffsetCache: MutableMap = mutableMapOf() diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index 7e7cda70..29ad509c 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -5,6 +5,7 @@ @file:JvmName("LocalDateJvmKt") package kotlinx.datetime +import kotlinx.datetime.LocalDate.Formats import kotlinx.datetime.format.* import kotlinx.datetime.internal.safeAdd import kotlinx.datetime.internal.safeMultiply @@ -106,6 +107,21 @@ public actual class LocalDate internal constructor( private fun writeReplace(): Any = Ser(Ser.DATE_TAG, this) } +public actual fun LocalDate.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): LocalDate? = + if (format === Formats.ISO) { + try { + val sanitizedInput = removeLeadingZerosFromLongYearFormLocalDate(input.toString()) + jtLocalDate.parse(sanitizedInput).let(::LocalDate) + } catch (_: DateTimeParseException) { + null + } + } else { + format.parseOrNull(input) + } + + /** * @suppress */ diff --git a/core/jvm/src/LocalDateTimeJvm.kt b/core/jvm/src/LocalDateTimeJvm.kt index 235be907..5ea6f0c0 100644 --- a/core/jvm/src/LocalDateTimeJvm.kt +++ b/core/jvm/src/LocalDateTimeJvm.kt @@ -6,6 +6,7 @@ @file:JvmMultifileClass package kotlinx.datetime +import kotlinx.datetime.LocalDateTime.Formats import kotlinx.datetime.format.* import kotlinx.datetime.internal.removeLeadingZerosFromLongYearFormLocalDateTime import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer @@ -115,6 +116,20 @@ public actual class LocalDateTime internal constructor( private fun writeReplace(): Any = Ser(Ser.DATE_TIME_TAG, this) } +public actual fun LocalDateTime.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): LocalDateTime? = + if (format === Formats.ISO) { + try { + val sanitizedInput = removeLeadingZerosFromLongYearFormLocalDateTime(input.toString()) + jtLocalDateTime.parse(sanitizedInput).let(::LocalDateTime) + } catch (_: DateTimeParseException) { + null + } + } else { + format.parseOrNull(input) + } + /** * @suppress */ diff --git a/core/jvm/src/LocalTimeJvm.kt b/core/jvm/src/LocalTimeJvm.kt index 98f42011..5c9ea9ce 100644 --- a/core/jvm/src/LocalTimeJvm.kt +++ b/core/jvm/src/LocalTimeJvm.kt @@ -7,6 +7,7 @@ package kotlinx.datetime +import kotlinx.datetime.LocalTime.Formats import kotlinx.datetime.format.* import kotlinx.datetime.internal.* import kotlinx.datetime.serializers.LocalTimeIso8601Serializer @@ -95,6 +96,18 @@ public actual class LocalTime internal constructor( private fun writeReplace(): Any = Ser(Ser.TIME_TAG, this) } +public actual fun LocalTime.Companion.parseOrNull(input: CharSequence, format: DateTimeFormat): LocalTime? = + if (format === Formats.ISO) { + try { + jtLocalTime.parse(input).let(::LocalTime) + } catch (_: DateTimeParseException) { + null + } + } else { + format.parseOrNull(input) + } + + @Deprecated( "Use kotlinx.datetime.Month", ReplaceWith("atDate(year, month.toKotlinMonth(), dayOfMonth)") diff --git a/core/jvm/src/UtcOffsetJvm.kt b/core/jvm/src/UtcOffsetJvm.kt index 7f9ed703..b9d4f0c9 100644 --- a/core/jvm/src/UtcOffsetJvm.kt +++ b/core/jvm/src/UtcOffsetJvm.kt @@ -5,6 +5,7 @@ package kotlinx.datetime +import kotlinx.datetime.UtcOffset.Formats import kotlinx.datetime.format.* import kotlinx.datetime.serializers.UtcOffsetSerializer import kotlinx.serialization.Serializable @@ -66,6 +67,15 @@ public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: I throw IllegalArgumentException(e) } +public actual fun UtcOffset.Companion.parseOrNull( + input: CharSequence, format: DateTimeFormat +): UtcOffset? = when { + format === Formats.ISO -> parseWithFormatOrNull(input, isoFormat) + format === Formats.ISO_BASIC -> parseWithFormatOrNull(input, isoBasicFormat) + format === Formats.FOUR_DIGITS -> parseWithFormatOrNull(input, fourDigitsFormat) + else -> format.parseOrNull(input) +} + private val isoFormat by lazy { DateTimeFormatterBuilder().parseCaseInsensitive().appendOffsetId().toFormatter() } @@ -81,3 +91,9 @@ private fun parseWithFormat(input: CharSequence, format: DateTimeFormatter) = tr } catch (e: DateTimeException) { throw DateTimeFormatException(e) } + +private fun parseWithFormatOrNull(input: CharSequence, format: DateTimeFormatter) = try { + format.parse(input, ZoneOffset::from).let(::UtcOffset) +} catch (_: DateTimeException) { + null +}