diff --git a/README.md b/README.md index 32ef9e58..438c453d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ The library provides a basic set of types for working with date and time: - `Clock` to obtain the current instant; - `LocalDateTime` to represent date and time components without a reference to the particular time zone; - `LocalDate` to represent the components of date only; +- `YearMonth` to represent only the year and month components; - `LocalTime` to represent the components of time only; - `TimeZone` and `FixedOffsetTimeZone` provide time zone information to convert between `Instant` and `LocalDateTime`; - `Month` and `DayOfWeek` enums; @@ -67,6 +68,9 @@ Here is some basic advice on how to choose which of the date-carrying types to u - Use `LocalDate` to represent the date of an event that does not have a specific time associated with it (like a birth date). +- Use `YearMonth` to represent the year and month of an event that does not have a specific day associated with it + or has a day-of-month that is inferred from the context (like a credit card expiration date). + - Use `LocalTime` to represent the time of an event that does not have a specific date associated with it. ## Operations @@ -150,6 +154,16 @@ Note, that today's date really depends on the time zone in which you're observin val knownDate = LocalDate(2020, 2, 21) ``` +### Getting year and month components + +A `YearMonth` represents a year and month without a day. You can obtain one from a `LocalDate` +by taking its `yearMonth` property. + +```kotlin +val day = LocalDate(2020, 2, 21) +val yearMonth: YearMonth = day.yearMonth +``` + ### Getting local time components A `LocalTime` represents local time without date. You can obtain one from an `Instant` @@ -273,10 +287,10 @@ collection of all datetime fields, can be used instead. ```kotlin // import kotlinx.datetime.format.* -val yearMonth = DateTimeComponents.Format { year(); char('-'); monthNumber() } - .parse("2024-01") -println(yearMonth.year) -println(yearMonth.monthNumber) +val monthDay = DateTimeComponents.Format { monthNumber(); char('/'); dayOfMonth() } + .parse("12/25") +println(monthDay.dayOfMonth) // 25 +println(monthDay.monthNumber) // 12 val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET .parse("2023-01-07T23:16:15.53+02:00") diff --git a/core/api/kotlinx-datetime.api b/core/api/kotlinx-datetime.api index e5429a44..5690cac6 100644 --- a/core/api/kotlinx-datetime.api +++ b/core/api/kotlinx-datetime.api @@ -26,6 +26,7 @@ public final class kotlinx/datetime/ConvertersKt { public static final fun toJavaLocalTime (Lkotlinx/datetime/LocalTime;)Ljava/time/LocalTime; public static final fun toJavaMonth (Lkotlinx/datetime/Month;)Ljava/time/Month; public static final fun toJavaPeriod (Lkotlinx/datetime/DatePeriod;)Ljava/time/Period; + public static final fun toJavaYearMonth (Lkotlinx/datetime/YearMonth;)Ljava/time/YearMonth; public static final fun toJavaZoneId (Lkotlinx/datetime/TimeZone;)Ljava/time/ZoneId; public static final fun toJavaZoneOffset (Lkotlinx/datetime/FixedOffsetTimeZone;)Ljava/time/ZoneOffset; public static final fun toJavaZoneOffset (Lkotlinx/datetime/UtcOffset;)Ljava/time/ZoneOffset; @@ -39,6 +40,7 @@ public final class kotlinx/datetime/ConvertersKt { public static final fun toKotlinMonth (Ljava/time/Month;)Lkotlinx/datetime/Month; public static final fun toKotlinTimeZone (Ljava/time/ZoneId;)Lkotlinx/datetime/TimeZone; public static final fun toKotlinUtcOffset (Ljava/time/ZoneOffset;)Lkotlinx/datetime/UtcOffset; + public static final fun toKotlinYearMonth (Ljava/time/YearMonth;)Lkotlinx/datetime/YearMonth; public static final fun toKotlinZoneOffset (Ljava/time/ZoneOffset;)Lkotlinx/datetime/FixedOffsetTimeZone; } @@ -543,6 +545,7 @@ public final class kotlinx/datetime/Ser : java/io/Externalizable { public static final field DATE_TIME_TAG I public static final field TIME_TAG I public static final field UTC_OFFSET_TAG I + public static final field YEAR_MONTH_TAG I public fun ()V public fun (ILjava/lang/Object;)V public fun readExternal (Ljava/io/ObjectInput;)V @@ -615,6 +618,117 @@ public final class kotlinx/datetime/UtcOffsetKt { public static final fun format (Lkotlinx/datetime/UtcOffset;Lkotlinx/datetime/format/DateTimeFormat;)Ljava/lang/String; } +public final class kotlinx/datetime/YearMonth : java/io/Serializable, java/lang/Comparable { + public static final field Companion Lkotlinx/datetime/YearMonth$Companion; + public fun (II)V + public fun (ILkotlinx/datetime/Month;)V + public synthetic fun compareTo (Ljava/lang/Object;)I + public fun compareTo (Lkotlinx/datetime/YearMonth;)I + public fun equals (Ljava/lang/Object;)Z + public final fun getDays ()Lkotlinx/datetime/LocalDateRange; + public final fun getFirstDay ()Lkotlinx/datetime/LocalDate; + public final fun getLastDay ()Lkotlinx/datetime/LocalDate; + public final fun getMonth ()Lkotlinx/datetime/Month; + public final fun getNumberOfDays ()I + public final fun getYear ()I + public fun hashCode ()I + public final fun rangeTo (Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonthRange; + public final fun rangeUntil (Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonthRange; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/datetime/YearMonth$Companion { + public final fun Format (Lkotlin/jvm/functions/Function1;)Lkotlinx/datetime/format/DateTimeFormat; + public final fun parse (Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;)Lkotlinx/datetime/YearMonth; + public static synthetic fun parse$default (Lkotlinx/datetime/YearMonth$Companion;Ljava/lang/CharSequence;Lkotlinx/datetime/format/DateTimeFormat;ILjava/lang/Object;)Lkotlinx/datetime/YearMonth; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class kotlinx/datetime/YearMonth$Formats { + public static final field INSTANCE Lkotlinx/datetime/YearMonth$Formats; + public final fun getISO ()Lkotlinx/datetime/format/DateTimeFormat; +} + +public final class kotlinx/datetime/YearMonthKt { + public static final fun format (Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/format/DateTimeFormat;)Ljava/lang/String; + public static final fun getYearMonth (Lkotlinx/datetime/LocalDate;)Lkotlinx/datetime/YearMonth; + public static final fun minus (Lkotlinx/datetime/YearMonth;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lkotlinx/datetime/YearMonth; + public static final fun minus (Lkotlinx/datetime/YearMonth;JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lkotlinx/datetime/YearMonth; + public static final fun minusMonth (Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonth; + public static final fun minusYear (Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonth; + public static final fun monthsUntil (Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/YearMonth;)I + public static final fun onDay (Lkotlinx/datetime/YearMonth;I)Lkotlinx/datetime/LocalDate; + public static final fun plus (Lkotlinx/datetime/YearMonth;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lkotlinx/datetime/YearMonth; + public static final fun plus (Lkotlinx/datetime/YearMonth;JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lkotlinx/datetime/YearMonth; + public static final fun plusMonth (Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonth; + public static final fun plusYear (Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonth; + public static final fun until (Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/DateTimeUnit$MonthBased;)J + public static final fun yearsUntil (Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/YearMonth;)I +} + +public class kotlinx/datetime/YearMonthProgression : java/util/Collection, kotlin/jvm/internal/markers/KMappedMarker { + public static final field Companion Lkotlinx/datetime/YearMonthProgression$Companion; + public synthetic fun add (Ljava/lang/Object;)Z + public fun add (Lkotlinx/datetime/YearMonth;)Z + public fun addAll (Ljava/util/Collection;)Z + public fun clear ()V + public final fun contains (Ljava/lang/Object;)Z + public fun contains (Lkotlinx/datetime/YearMonth;)Z + public fun containsAll (Ljava/util/Collection;)Z + public fun equals (Ljava/lang/Object;)Z + public final fun getFirst ()Lkotlinx/datetime/YearMonth; + public final fun getLast ()Lkotlinx/datetime/YearMonth; + public fun getSize ()I + public fun hashCode ()I + public fun isEmpty ()Z + public fun iterator ()Ljava/util/Iterator; + public fun remove (Ljava/lang/Object;)Z + public fun removeAll (Ljava/util/Collection;)Z + public fun removeIf (Ljava/util/function/Predicate;)Z + public fun retainAll (Ljava/util/Collection;)Z + public final fun size ()I + public fun toArray ()[Ljava/lang/Object; + public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/datetime/YearMonthProgression$Companion { +} + +public final class kotlinx/datetime/YearMonthRange : kotlinx/datetime/YearMonthProgression, kotlin/ranges/ClosedRange, kotlin/ranges/OpenEndRange { + public static final field Companion Lkotlinx/datetime/YearMonthRange$Companion; + public fun (Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/YearMonth;)V + public synthetic fun contains (Ljava/lang/Comparable;)Z + public fun contains (Lkotlinx/datetime/YearMonth;)Z + public synthetic fun getEndExclusive ()Ljava/lang/Comparable; + public fun getEndExclusive ()Lkotlinx/datetime/YearMonth; + public synthetic fun getEndInclusive ()Ljava/lang/Comparable; + public fun getEndInclusive ()Lkotlinx/datetime/YearMonth; + public synthetic fun getStart ()Ljava/lang/Comparable; + public fun getStart ()Lkotlinx/datetime/YearMonth; + public fun isEmpty ()Z + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/datetime/YearMonthRange$Companion { + public final fun getEMPTY ()Lkotlinx/datetime/YearMonthRange; +} + +public final class kotlinx/datetime/YearMonthRangeKt { + public static final fun downTo (Lkotlinx/datetime/YearMonth;Lkotlinx/datetime/YearMonth;)Lkotlinx/datetime/YearMonthProgression; + public static final fun first (Lkotlinx/datetime/YearMonthProgression;)Lkotlinx/datetime/YearMonth; + public static final fun firstOrNull (Lkotlinx/datetime/YearMonthProgression;)Lkotlinx/datetime/YearMonth; + public static final fun last (Lkotlinx/datetime/YearMonthProgression;)Lkotlinx/datetime/YearMonth; + public static final fun lastOrNull (Lkotlinx/datetime/YearMonthProgression;)Lkotlinx/datetime/YearMonth; + public static final fun random (Lkotlinx/datetime/YearMonthProgression;Lkotlin/random/Random;)Lkotlinx/datetime/YearMonth; + public static synthetic fun random$default (Lkotlinx/datetime/YearMonthProgression;Lkotlin/random/Random;ILjava/lang/Object;)Lkotlinx/datetime/YearMonth; + public static final fun randomOrNull (Lkotlinx/datetime/YearMonthProgression;Lkotlin/random/Random;)Lkotlinx/datetime/YearMonth; + public static synthetic fun randomOrNull$default (Lkotlinx/datetime/YearMonthProgression;Lkotlin/random/Random;ILjava/lang/Object;)Lkotlinx/datetime/YearMonth; + public static final fun reversed (Lkotlinx/datetime/YearMonthProgression;)Lkotlinx/datetime/YearMonthProgression; + public static final fun step (Lkotlinx/datetime/YearMonthProgression;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lkotlinx/datetime/YearMonthProgression; + public static final fun step (Lkotlinx/datetime/YearMonthProgression;JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lkotlinx/datetime/YearMonthProgression; +} + public final class kotlinx/datetime/format/AmPmMarker : java/lang/Enum { public static final field AM Lkotlinx/datetime/format/AmPmMarker; public static final field PM Lkotlinx/datetime/format/AmPmMarker; @@ -668,11 +782,13 @@ public final class kotlinx/datetime/format/DateTimeComponents { public final fun setTime (Lkotlinx/datetime/LocalTime;)V public final fun setTimeZoneId (Ljava/lang/String;)V public final fun setYear (Ljava/lang/Integer;)V + public final fun setYearMonth (Lkotlinx/datetime/YearMonth;)V public final fun toInstantUsingOffset ()Lkotlinx/datetime/Instant; public final fun toLocalDate ()Lkotlinx/datetime/LocalDate; public final fun toLocalDateTime ()Lkotlinx/datetime/LocalDateTime; public final fun toLocalTime ()Lkotlinx/datetime/LocalTime; public final fun toUtcOffset ()Lkotlinx/datetime/UtcOffset; + public final fun toYearMonth ()Lkotlinx/datetime/YearMonth; } public final class kotlinx/datetime/format/DateTimeComponents$Companion { @@ -706,16 +822,12 @@ public abstract interface class kotlinx/datetime/format/DateTimeFormatBuilder { public abstract fun chars (Ljava/lang/String;)V } -public abstract interface class kotlinx/datetime/format/DateTimeFormatBuilder$WithDate : kotlinx/datetime/format/DateTimeFormatBuilder { +public abstract interface class kotlinx/datetime/format/DateTimeFormatBuilder$WithDate : kotlinx/datetime/format/DateTimeFormatBuilder$WithYearMonth { public abstract fun date (Lkotlinx/datetime/format/DateTimeFormat;)V public abstract fun day (Lkotlinx/datetime/format/Padding;)V public abstract fun dayOfMonth (Lkotlinx/datetime/format/Padding;)V public abstract fun dayOfWeek (Lkotlinx/datetime/format/DayOfWeekNames;)V public abstract fun dayOfYear (Lkotlinx/datetime/format/Padding;)V - public abstract fun monthName (Lkotlinx/datetime/format/MonthNames;)V - public abstract fun monthNumber (Lkotlinx/datetime/format/Padding;)V - public abstract fun year (Lkotlinx/datetime/format/Padding;)V - public abstract fun yearTwoDigits (I)V } public final class kotlinx/datetime/format/DateTimeFormatBuilder$WithDate$DefaultImpls { @@ -723,8 +835,6 @@ public final class kotlinx/datetime/format/DateTimeFormatBuilder$WithDate$Defaul public static fun dayOfMonth (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithDate;Lkotlinx/datetime/format/Padding;)V public static synthetic fun dayOfMonth$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithDate;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V public static synthetic fun dayOfYear$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithDate;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V - public static synthetic fun monthNumber$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithDate;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V - public static synthetic fun year$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithDate;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V } public abstract interface class kotlinx/datetime/format/DateTimeFormatBuilder$WithDateTime : kotlinx/datetime/format/DateTimeFormatBuilder$WithDate, kotlinx/datetime/format/DateTimeFormatBuilder$WithTime { @@ -779,6 +889,19 @@ public final class kotlinx/datetime/format/DateTimeFormatBuilder$WithUtcOffset$D public static synthetic fun offsetSecondsOfMinute$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithUtcOffset;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V } +public abstract interface class kotlinx/datetime/format/DateTimeFormatBuilder$WithYearMonth : kotlinx/datetime/format/DateTimeFormatBuilder { + public abstract fun monthName (Lkotlinx/datetime/format/MonthNames;)V + public abstract fun monthNumber (Lkotlinx/datetime/format/Padding;)V + public abstract fun year (Lkotlinx/datetime/format/Padding;)V + public abstract fun yearMonth (Lkotlinx/datetime/format/DateTimeFormat;)V + public abstract fun yearTwoDigits (I)V +} + +public final class kotlinx/datetime/format/DateTimeFormatBuilder$WithYearMonth$DefaultImpls { + public static synthetic fun monthNumber$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithYearMonth;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V + public static synthetic fun year$default (Lkotlinx/datetime/format/DateTimeFormatBuilder$WithYearMonth;Lkotlinx/datetime/format/Padding;ILjava/lang/Object;)V +} + public final class kotlinx/datetime/format/DateTimeFormatBuilderKt { public static final fun alternativeParsing (Lkotlinx/datetime/format/DateTimeFormatBuilder;[Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V public static final fun char (Lkotlinx/datetime/format/DateTimeFormatBuilder;C)V @@ -1030,3 +1153,21 @@ public final class kotlinx/datetime/serializers/UtcOffsetSerializer : kotlinx/se public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/datetime/UtcOffset;)V } +public final class kotlinx/datetime/serializers/YearMonthComponentSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lkotlinx/datetime/serializers/YearMonthComponentSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/datetime/YearMonth; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/datetime/YearMonth;)V +} + +public final class kotlinx/datetime/serializers/YearMonthIso8601Serializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lkotlinx/datetime/serializers/YearMonthIso8601Serializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlinx/datetime/YearMonth; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlinx/datetime/YearMonth;)V +} + diff --git a/core/api/kotlinx-datetime.klib.api b/core/api/kotlinx-datetime.klib.api index 2fb3de66..3490f4a8 100644 --- a/core/api/kotlinx-datetime.klib.api +++ b/core/api/kotlinx-datetime.klib.api @@ -95,15 +95,11 @@ sealed interface <#A: kotlin/Any?> kotlinx.datetime.format/DateTimeFormat { // k sealed interface kotlinx.datetime.format/DateTimeFormatBuilder { // kotlinx.datetime.format/DateTimeFormatBuilder|null[0] abstract fun chars(kotlin/String) // kotlinx.datetime.format/DateTimeFormatBuilder.chars|chars(kotlin.String){}[0] - sealed interface WithDate : kotlinx.datetime.format/DateTimeFormatBuilder { // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate|null[0] + sealed interface WithDate : kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth { // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate|null[0] abstract fun date(kotlinx.datetime.format/DateTimeFormat) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.date|date(kotlinx.datetime.format.DateTimeFormat){}[0] abstract fun day(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.day|day(kotlinx.datetime.format.Padding){}[0] abstract fun dayOfWeek(kotlinx.datetime.format/DayOfWeekNames) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.dayOfWeek|dayOfWeek(kotlinx.datetime.format.DayOfWeekNames){}[0] abstract fun dayOfYear(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.dayOfYear|dayOfYear(kotlinx.datetime.format.Padding){}[0] - abstract fun monthName(kotlinx.datetime.format/MonthNames) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.monthName|monthName(kotlinx.datetime.format.MonthNames){}[0] - abstract fun monthNumber(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.monthNumber|monthNumber(kotlinx.datetime.format.Padding){}[0] - abstract fun year(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.year|year(kotlinx.datetime.format.Padding){}[0] - abstract fun yearTwoDigits(kotlin/Int) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.yearTwoDigits|yearTwoDigits(kotlin.Int){}[0] open fun dayOfMonth(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithDate.dayOfMonth|dayOfMonth(kotlinx.datetime.format.Padding){}[0] } @@ -133,6 +129,14 @@ sealed interface kotlinx.datetime.format/DateTimeFormatBuilder { // kotlinx.date abstract fun offsetMinutesOfHour(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithUtcOffset.offsetMinutesOfHour|offsetMinutesOfHour(kotlinx.datetime.format.Padding){}[0] abstract fun offsetSecondsOfMinute(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithUtcOffset.offsetSecondsOfMinute|offsetSecondsOfMinute(kotlinx.datetime.format.Padding){}[0] } + + sealed interface WithYearMonth : kotlinx.datetime.format/DateTimeFormatBuilder { // kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth|null[0] + abstract fun monthName(kotlinx.datetime.format/MonthNames) // kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth.monthName|monthName(kotlinx.datetime.format.MonthNames){}[0] + abstract fun monthNumber(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth.monthNumber|monthNumber(kotlinx.datetime.format.Padding){}[0] + abstract fun year(kotlinx.datetime.format/Padding = ...) // kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth.year|year(kotlinx.datetime.format.Padding){}[0] + abstract fun yearMonth(kotlinx.datetime.format/DateTimeFormat) // kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth.yearMonth|yearMonth(kotlinx.datetime.format.DateTimeFormat){}[0] + abstract fun yearTwoDigits(kotlin/Int) // kotlinx.datetime.format/DateTimeFormatBuilder.WithYearMonth.yearTwoDigits|yearTwoDigits(kotlin.Int){}[0] + } } final class kotlinx.datetime.format/DateTimeComponents { // kotlinx.datetime.format/DateTimeComponents|null[0] @@ -197,11 +201,13 @@ final class kotlinx.datetime.format/DateTimeComponents { // kotlinx.datetime.for final fun setDateTimeOffset(kotlinx.datetime/LocalDateTime, kotlinx.datetime/UtcOffset) // kotlinx.datetime.format/DateTimeComponents.setDateTimeOffset|setDateTimeOffset(kotlinx.datetime.LocalDateTime;kotlinx.datetime.UtcOffset){}[0] final fun setOffset(kotlinx.datetime/UtcOffset) // kotlinx.datetime.format/DateTimeComponents.setOffset|setOffset(kotlinx.datetime.UtcOffset){}[0] final fun setTime(kotlinx.datetime/LocalTime) // kotlinx.datetime.format/DateTimeComponents.setTime|setTime(kotlinx.datetime.LocalTime){}[0] + final fun setYearMonth(kotlinx.datetime/YearMonth) // kotlinx.datetime.format/DateTimeComponents.setYearMonth|setYearMonth(kotlinx.datetime.YearMonth){}[0] final fun toInstantUsingOffset(): kotlinx.datetime/Instant // kotlinx.datetime.format/DateTimeComponents.toInstantUsingOffset|toInstantUsingOffset(){}[0] final fun toLocalDate(): kotlinx.datetime/LocalDate // kotlinx.datetime.format/DateTimeComponents.toLocalDate|toLocalDate(){}[0] final fun toLocalDateTime(): kotlinx.datetime/LocalDateTime // kotlinx.datetime.format/DateTimeComponents.toLocalDateTime|toLocalDateTime(){}[0] final fun toLocalTime(): kotlinx.datetime/LocalTime // kotlinx.datetime.format/DateTimeComponents.toLocalTime|toLocalTime(){}[0] final fun toUtcOffset(): kotlinx.datetime/UtcOffset // kotlinx.datetime.format/DateTimeComponents.toUtcOffset|toUtcOffset(){}[0] + final fun toYearMonth(): kotlinx.datetime/YearMonth // kotlinx.datetime.format/DateTimeComponents.toYearMonth|toYearMonth(){}[0] final object Companion { // kotlinx.datetime.format/DateTimeComponents.Companion|null[0] final fun Format(kotlin/Function1): kotlinx.datetime.format/DateTimeFormat // kotlinx.datetime.format/DateTimeComponents.Companion.Format|Format(kotlin.Function1){}[0] @@ -511,6 +517,62 @@ final class kotlinx.datetime/UtcOffset { // kotlinx.datetime/UtcOffset|null[0] } } +final class kotlinx.datetime/YearMonth : kotlin/Comparable { // kotlinx.datetime/YearMonth|null[0] + constructor (kotlin/Int, kotlin/Int) // kotlinx.datetime/YearMonth.|(kotlin.Int;kotlin.Int){}[0] + constructor (kotlin/Int, kotlinx.datetime/Month) // kotlinx.datetime/YearMonth.|(kotlin.Int;kotlinx.datetime.Month){}[0] + + final val days // kotlinx.datetime/YearMonth.days|{}days[0] + final fun (): kotlinx.datetime/LocalDateRange // kotlinx.datetime/YearMonth.days.|(){}[0] + final val firstDay // kotlinx.datetime/YearMonth.firstDay|{}firstDay[0] + final fun (): kotlinx.datetime/LocalDate // kotlinx.datetime/YearMonth.firstDay.|(){}[0] + final val lastDay // kotlinx.datetime/YearMonth.lastDay|{}lastDay[0] + final fun (): kotlinx.datetime/LocalDate // kotlinx.datetime/YearMonth.lastDay.|(){}[0] + final val month // kotlinx.datetime/YearMonth.month|{}month[0] + final fun (): kotlinx.datetime/Month // kotlinx.datetime/YearMonth.month.|(){}[0] + final val numberOfDays // kotlinx.datetime/YearMonth.numberOfDays|{}numberOfDays[0] + final fun (): kotlin/Int // kotlinx.datetime/YearMonth.numberOfDays.|(){}[0] + final val year // kotlinx.datetime/YearMonth.year|{}year[0] + final fun (): kotlin/Int // kotlinx.datetime/YearMonth.year.|(){}[0] + + final fun compareTo(kotlinx.datetime/YearMonth): kotlin/Int // kotlinx.datetime/YearMonth.compareTo|compareTo(kotlinx.datetime.YearMonth){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.datetime/YearMonth.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // kotlinx.datetime/YearMonth.hashCode|hashCode(){}[0] + final fun rangeTo(kotlinx.datetime/YearMonth): kotlinx.datetime/YearMonthRange // kotlinx.datetime/YearMonth.rangeTo|rangeTo(kotlinx.datetime.YearMonth){}[0] + final fun rangeUntil(kotlinx.datetime/YearMonth): kotlinx.datetime/YearMonthRange // kotlinx.datetime/YearMonth.rangeUntil|rangeUntil(kotlinx.datetime.YearMonth){}[0] + final fun toString(): kotlin/String // kotlinx.datetime/YearMonth.toString|toString(){}[0] + + final object Companion { // kotlinx.datetime/YearMonth.Companion|null[0] + final fun Format(kotlin/Function1): kotlinx.datetime.format/DateTimeFormat // kotlinx.datetime/YearMonth.Companion.Format|Format(kotlin.Function1){}[0] + final fun parse(kotlin/CharSequence, kotlinx.datetime.format/DateTimeFormat = ...): kotlinx.datetime/YearMonth // kotlinx.datetime/YearMonth.Companion.parse|parse(kotlin.CharSequence;kotlinx.datetime.format.DateTimeFormat){}[0] + final fun serializer(): kotlinx.serialization/KSerializer // kotlinx.datetime/YearMonth.Companion.serializer|serializer(){}[0] + } + + final object Formats { // kotlinx.datetime/YearMonth.Formats|null[0] + final val ISO // kotlinx.datetime/YearMonth.Formats.ISO|{}ISO[0] + final fun (): kotlinx.datetime.format/DateTimeFormat // kotlinx.datetime/YearMonth.Formats.ISO.|(){}[0] + } +} + +final class kotlinx.datetime/YearMonthRange : kotlin.ranges/ClosedRange, kotlin.ranges/OpenEndRange, kotlinx.datetime/YearMonthProgression { // kotlinx.datetime/YearMonthRange|null[0] + constructor (kotlinx.datetime/YearMonth, kotlinx.datetime/YearMonth) // kotlinx.datetime/YearMonthRange.|(kotlinx.datetime.YearMonth;kotlinx.datetime.YearMonth){}[0] + + final val endExclusive // kotlinx.datetime/YearMonthRange.endExclusive|{}endExclusive[0] + final fun (): kotlinx.datetime/YearMonth // kotlinx.datetime/YearMonthRange.endExclusive.|(){}[0] + final val endInclusive // kotlinx.datetime/YearMonthRange.endInclusive|{}endInclusive[0] + final fun (): kotlinx.datetime/YearMonth // kotlinx.datetime/YearMonthRange.endInclusive.|(){}[0] + final val start // kotlinx.datetime/YearMonthRange.start|{}start[0] + final fun (): kotlinx.datetime/YearMonth // kotlinx.datetime/YearMonthRange.start.|(){}[0] + + final fun contains(kotlinx.datetime/YearMonth): kotlin/Boolean // kotlinx.datetime/YearMonthRange.contains|contains(kotlinx.datetime.YearMonth){}[0] + final fun isEmpty(): kotlin/Boolean // kotlinx.datetime/YearMonthRange.isEmpty|isEmpty(){}[0] + final fun toString(): kotlin/String // kotlinx.datetime/YearMonthRange.toString|toString(){}[0] + + final object Companion { // kotlinx.datetime/YearMonthRange.Companion|null[0] + final val EMPTY // kotlinx.datetime/YearMonthRange.Companion.EMPTY|{}EMPTY[0] + final fun (): kotlinx.datetime/YearMonthRange // kotlinx.datetime/YearMonthRange.Companion.EMPTY.|(){}[0] + } +} + open class kotlinx.datetime/LocalDateProgression : kotlin.collections/Collection { // kotlinx.datetime/LocalDateProgression|null[0] final val first // kotlinx.datetime/LocalDateProgression.first|{}first[0] final fun (): kotlinx.datetime/LocalDate // kotlinx.datetime/LocalDateProgression.first.|(){}[0] @@ -552,6 +614,25 @@ open class kotlinx.datetime/TimeZone { // kotlinx.datetime/TimeZone|null[0] } } +open class kotlinx.datetime/YearMonthProgression : kotlin.collections/Collection { // kotlinx.datetime/YearMonthProgression|null[0] + final val first // kotlinx.datetime/YearMonthProgression.first|{}first[0] + final fun (): kotlinx.datetime/YearMonth // kotlinx.datetime/YearMonthProgression.first.|(){}[0] + final val last // kotlinx.datetime/YearMonthProgression.last|{}last[0] + final fun (): kotlinx.datetime/YearMonth // kotlinx.datetime/YearMonthProgression.last.|(){}[0] + open val size // kotlinx.datetime/YearMonthProgression.size|{}size[0] + open fun (): kotlin/Int // kotlinx.datetime/YearMonthProgression.size.|(){}[0] + + open fun contains(kotlinx.datetime/YearMonth): kotlin/Boolean // kotlinx.datetime/YearMonthProgression.contains|contains(kotlinx.datetime.YearMonth){}[0] + open fun containsAll(kotlin.collections/Collection): kotlin/Boolean // kotlinx.datetime/YearMonthProgression.containsAll|containsAll(kotlin.collections.Collection){}[0] + open fun equals(kotlin/Any?): kotlin/Boolean // kotlinx.datetime/YearMonthProgression.equals|equals(kotlin.Any?){}[0] + open fun hashCode(): kotlin/Int // kotlinx.datetime/YearMonthProgression.hashCode|hashCode(){}[0] + open fun isEmpty(): kotlin/Boolean // kotlinx.datetime/YearMonthProgression.isEmpty|isEmpty(){}[0] + open fun iterator(): kotlin.collections/Iterator // kotlinx.datetime/YearMonthProgression.iterator|iterator(){}[0] + open fun toString(): kotlin/String // kotlinx.datetime/YearMonthProgression.toString|toString(){}[0] + + final object Companion // kotlinx.datetime/YearMonthProgression.Companion|null[0] +} + sealed class kotlinx.datetime/DateTimePeriod { // kotlinx.datetime/DateTimePeriod|null[0] abstract val days // kotlinx.datetime/DateTimePeriod.days|{}days[0] abstract fun (): kotlin/Int // kotlinx.datetime/DateTimePeriod.days.|(){}[0] @@ -849,6 +930,22 @@ final object kotlinx.datetime.serializers/UtcOffsetSerializer : kotlinx.serializ final fun serialize(kotlinx.serialization.encoding/Encoder, kotlinx.datetime/UtcOffset) // kotlinx.datetime.serializers/UtcOffsetSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlinx.datetime.UtcOffset){}[0] } +final object kotlinx.datetime.serializers/YearMonthComponentSerializer : kotlinx.serialization/KSerializer { // kotlinx.datetime.serializers/YearMonthComponentSerializer|null[0] + final val descriptor // kotlinx.datetime.serializers/YearMonthComponentSerializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.datetime.serializers/YearMonthComponentSerializer.descriptor.|(){}[0] + + final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlinx.datetime/YearMonth // kotlinx.datetime.serializers/YearMonthComponentSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, kotlinx.datetime/YearMonth) // kotlinx.datetime.serializers/YearMonthComponentSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlinx.datetime.YearMonth){}[0] +} + +final object kotlinx.datetime.serializers/YearMonthIso8601Serializer : kotlinx.serialization/KSerializer { // kotlinx.datetime.serializers/YearMonthIso8601Serializer|null[0] + final val descriptor // kotlinx.datetime.serializers/YearMonthIso8601Serializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.datetime.serializers/YearMonthIso8601Serializer.descriptor.|(){}[0] + + final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlinx.datetime/YearMonth // kotlinx.datetime.serializers/YearMonthIso8601Serializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, kotlinx.datetime/YearMonth) // kotlinx.datetime.serializers/YearMonthIso8601Serializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlinx.datetime.YearMonth){}[0] +} + final val kotlinx.datetime/isDistantFuture // kotlinx.datetime/isDistantFuture|@kotlinx.datetime.Instant{}isDistantFuture[0] final fun (kotlinx.datetime/Instant).(): kotlin/Boolean // kotlinx.datetime/isDistantFuture.|@kotlinx.datetime.Instant(){}[0] final val kotlinx.datetime/isDistantPast // kotlinx.datetime/isDistantPast|@kotlinx.datetime.Instant{}isDistantPast[0] @@ -857,6 +954,8 @@ final val kotlinx.datetime/isoDayNumber // kotlinx.datetime/isoDayNumber|@kotlin final fun (kotlinx.datetime/DayOfWeek).(): kotlin/Int // kotlinx.datetime/isoDayNumber.|@kotlinx.datetime.DayOfWeek(){}[0] final val kotlinx.datetime/number // kotlinx.datetime/number|@kotlinx.datetime.Month{}number[0] final fun (kotlinx.datetime/Month).(): kotlin/Int // kotlinx.datetime/number.|@kotlinx.datetime.Month(){}[0] +final val kotlinx.datetime/yearMonth // kotlinx.datetime/yearMonth|@kotlinx.datetime.LocalDate{}yearMonth[0] + final fun (kotlinx.datetime/LocalDate).(): kotlinx.datetime/YearMonth // kotlinx.datetime/yearMonth.|@kotlinx.datetime.LocalDate(){}[0] final fun (kotlin.time/Duration).kotlinx.datetime/toDateTimePeriod(): kotlinx.datetime/DateTimePeriod // kotlinx.datetime/toDateTimePeriod|toDateTimePeriod@kotlin.time.Duration(){}[0] final fun (kotlin.time/TimeSource).kotlinx.datetime/asClock(kotlinx.datetime/Instant): kotlinx.datetime/Clock // kotlinx.datetime/asClock|asClock@kotlin.time.TimeSource(kotlinx.datetime.Instant){}[0] @@ -941,6 +1040,29 @@ final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/format(kotlinx.datetime. 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/YearMonth).kotlinx.datetime/downTo(kotlinx.datetime/YearMonth): kotlinx.datetime/YearMonthProgression // kotlinx.datetime/downTo|downTo@kotlinx.datetime.YearMonth(kotlinx.datetime.YearMonth){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/format(kotlinx.datetime.format/DateTimeFormat): kotlin/String // kotlinx.datetime/format|format@kotlinx.datetime.YearMonth(kotlinx.datetime.format.DateTimeFormat){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/minus(kotlin/Int, kotlinx.datetime/DateTimeUnit.MonthBased): kotlinx.datetime/YearMonth // kotlinx.datetime/minus|minus@kotlinx.datetime.YearMonth(kotlin.Int;kotlinx.datetime.DateTimeUnit.MonthBased){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/minus(kotlin/Long, kotlinx.datetime/DateTimeUnit.MonthBased): kotlinx.datetime/YearMonth // kotlinx.datetime/minus|minus@kotlinx.datetime.YearMonth(kotlin.Long;kotlinx.datetime.DateTimeUnit.MonthBased){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/minusMonth(): kotlinx.datetime/YearMonth // kotlinx.datetime/minusMonth|minusMonth@kotlinx.datetime.YearMonth(){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/minusYear(): kotlinx.datetime/YearMonth // kotlinx.datetime/minusYear|minusYear@kotlinx.datetime.YearMonth(){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/monthsUntil(kotlinx.datetime/YearMonth): kotlin/Int // kotlinx.datetime/monthsUntil|monthsUntil@kotlinx.datetime.YearMonth(kotlinx.datetime.YearMonth){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/onDay(kotlin/Int): kotlinx.datetime/LocalDate // kotlinx.datetime/onDay|onDay@kotlinx.datetime.YearMonth(kotlin.Int){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/plus(kotlin/Int, kotlinx.datetime/DateTimeUnit.MonthBased): kotlinx.datetime/YearMonth // kotlinx.datetime/plus|plus@kotlinx.datetime.YearMonth(kotlin.Int;kotlinx.datetime.DateTimeUnit.MonthBased){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/plus(kotlin/Long, kotlinx.datetime/DateTimeUnit.MonthBased): kotlinx.datetime/YearMonth // kotlinx.datetime/plus|plus@kotlinx.datetime.YearMonth(kotlin.Long;kotlinx.datetime.DateTimeUnit.MonthBased){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/plusMonth(): kotlinx.datetime/YearMonth // kotlinx.datetime/plusMonth|plusMonth@kotlinx.datetime.YearMonth(){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/plusYear(): kotlinx.datetime/YearMonth // kotlinx.datetime/plusYear|plusYear@kotlinx.datetime.YearMonth(){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/until(kotlinx.datetime/YearMonth, kotlinx.datetime/DateTimeUnit.MonthBased): kotlin/Long // kotlinx.datetime/until|until@kotlinx.datetime.YearMonth(kotlinx.datetime.YearMonth;kotlinx.datetime.DateTimeUnit.MonthBased){}[0] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/yearsUntil(kotlinx.datetime/YearMonth): kotlin/Int // kotlinx.datetime/yearsUntil|yearsUntil@kotlinx.datetime.YearMonth(kotlinx.datetime.YearMonth){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/first(): kotlinx.datetime/YearMonth // kotlinx.datetime/first|first@kotlinx.datetime.YearMonthProgression(){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/firstOrNull(): kotlinx.datetime/YearMonth? // kotlinx.datetime/firstOrNull|firstOrNull@kotlinx.datetime.YearMonthProgression(){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/last(): kotlinx.datetime/YearMonth // kotlinx.datetime/last|last@kotlinx.datetime.YearMonthProgression(){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/lastOrNull(): kotlinx.datetime/YearMonth? // kotlinx.datetime/lastOrNull|lastOrNull@kotlinx.datetime.YearMonthProgression(){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/random(kotlin.random/Random = ...): kotlinx.datetime/YearMonth // kotlinx.datetime/random|random@kotlinx.datetime.YearMonthProgression(kotlin.random.Random){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/randomOrNull(kotlin.random/Random = ...): kotlinx.datetime/YearMonth? // kotlinx.datetime/randomOrNull|randomOrNull@kotlinx.datetime.YearMonthProgression(kotlin.random.Random){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/reversed(): kotlinx.datetime/YearMonthProgression // kotlinx.datetime/reversed|reversed@kotlinx.datetime.YearMonthProgression(){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/step(kotlin/Int, kotlinx.datetime/DateTimeUnit.MonthBased): kotlinx.datetime/YearMonthProgression // kotlinx.datetime/step|step@kotlinx.datetime.YearMonthProgression(kotlin.Int;kotlinx.datetime.DateTimeUnit.MonthBased){}[0] +final fun (kotlinx.datetime/YearMonthProgression).kotlinx.datetime/step(kotlin/Long, kotlinx.datetime/DateTimeUnit.MonthBased): kotlinx.datetime/YearMonthProgression // kotlinx.datetime/step|step@kotlinx.datetime.YearMonthProgression(kotlin.Long;kotlinx.datetime.DateTimeUnit.MonthBased){}[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] @@ -965,6 +1087,9 @@ final fun (kotlinx.datetime/LocalDateTime).kotlinx.datetime/toNSDateComponents() // Targets: [apple] final fun (kotlinx.datetime/TimeZone).kotlinx.datetime/toNSTimeZone(): platform.Foundation/NSTimeZone // kotlinx.datetime/toNSTimeZone|toNSTimeZone@kotlinx.datetime.TimeZone(){}[0] +// Targets: [apple] +final fun (kotlinx.datetime/YearMonth).kotlinx.datetime/toNSDateComponents(): platform.Foundation/NSDateComponents // kotlinx.datetime/toNSDateComponents|toNSDateComponents@kotlinx.datetime.YearMonth(){}[0] + // Targets: [apple] final fun (platform.Foundation/NSDate).kotlinx.datetime/toKotlinInstant(): kotlinx.datetime/Instant // kotlinx.datetime/toKotlinInstant|toKotlinInstant@platform.Foundation.NSDate(){}[0] diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index ec2aa746..527acde0 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -5,11 +5,8 @@ package kotlinx.datetime -import kotlinx.datetime.internal.clampToInt -import kotlinx.datetime.internal.safeAdd -import kotlinx.datetime.internal.safeMultiplyOrClamp +import kotlinx.datetime.internal.* import kotlin.random.Random -import kotlin.random.nextLong private class LocalDateProgressionIterator(private val iterator: LongIterator) : Iterator { override fun hasNext(): Boolean = iterator.hasNext() @@ -67,7 +64,7 @@ internal constructor(internal val longProgression: LongProgression) : Collection * Returns [Int.MAX_VALUE] if the number of dates overflows [Int] */ override val size: Int - get() = longProgression.size + get() = longProgression.sizeUnsafe /** * Returns true iff every element in [elements] is a member of the progression. @@ -82,7 +79,7 @@ internal constructor(internal val longProgression: LongProgression) : Collection @Suppress("USELESS_CAST") if ((value as Any?) !is LocalDate) return false - return longProgression.contains(value.toEpochDays()) + return longProgression.containsUnsafe(value.toEpochDays()) } override fun equals(other: Any?): Boolean = @@ -261,13 +258,13 @@ public infix fun LocalDate.downTo(that: LocalDate): LocalDateProgression = * Takes the step into account; * will not return any value within the range that would be skipped over by the progression. * - * @throws IllegalArgumentException if the progression is empty. + * @throws NoSuchElementException if the progression is empty. * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random */ public fun LocalDateProgression.random(random: Random = Random): LocalDate = if (isEmpty()) throw NoSuchElementException("Cannot get random in empty range: $this") - else longProgression.random(random).let(LocalDate.Companion::fromEpochDays) + else longProgression.randomUnsafe(random).let(LocalDate.Companion::fromEpochDays) /** * Returns a random [LocalDate] within the bounds of the [LocalDateProgression] or null if the progression is empty. @@ -277,29 +274,5 @@ public fun LocalDateProgression.random(random: Random = Random): LocalDate = * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random */ -public fun LocalDateProgression.randomOrNull(random: Random = Random): LocalDate? = longProgression.randomOrNull(random) - ?.let(LocalDate.Companion::fromEpochDays) - -// this implementation is incorrect in general -// (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).random()` throws an exception), -// but for the range of epoch days in LocalDate it's good enough -private fun LongProgression.random(random: Random = Random): Long = - random.nextLong(0L..(last - first) / step) * step + first - -// incorrect in general; see `random` just above -private fun LongProgression.randomOrNull(random: Random = Random): Long? = if (isEmpty()) null else random(random) - -// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)` -// returns `false` incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5), -// but for the range of epoch days in LocalDate it's good enough -private fun LongProgression.contains(value: Long): Boolean = - value in (if (step > 0) first..last else last..first) && (value - first) % step == 0L - -// this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0), -// but for the range of epoch days in LocalDate it's good enough -private val LongProgression.size: Int - get() = if (isEmpty()) 0 else try { - (safeAdd(last, -first) / step + 1).clampToInt() - } catch (e: ArithmeticException) { - Int.MAX_VALUE - } +public fun LocalDateProgression.randomOrNull(random: Random = Random): LocalDate? = longProgression.randomUnsafeOrNull(random) + ?.let(LocalDate.Companion::fromEpochDays) \ No newline at end of file diff --git a/core/common/src/YearMonth.kt b/core/common/src/YearMonth.kt new file mode 100644 index 00000000..e2761311 --- /dev/null +++ b/core/common/src/YearMonth.kt @@ -0,0 +1,387 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime + +import kotlinx.datetime.format.* +import kotlinx.datetime.internal.* +import kotlinx.datetime.serializers.YearMonthIso8601Serializer +import kotlinx.serialization.Serializable + +/** + * The year-month part of [LocalDate], without the day-of-month. + * + * This class represents years and months without a reference to a particular time zone. + * As such, these objects may denote different time intervals in different time zones: for someone in Berlin, + * `2020-08` started and ended at different moments from those for someone in Tokyo. + * + * ### Arithmetic operations + * + * The arithmetic on [YearMonth] values is defined independently of the time zone (so `2020-08` plus one month + * is `2020-09` everywhere). + * + * Operations with [DateTimeUnit.MonthBased] are provided for [YearMonth]: + * - [YearMonth.plus] and [YearMonth.minus] allow expressing concepts like "two months later". + * - [YearMonth.until] and its shortcuts [YearMonth.monthsUntil] and [YearMonth.yearsUntil] + * can be used to find the number of months or years between two dates. + * + * ### Platform specifics + * + * The range of supported years is platform-dependent, but at least is enough to represent year-months of all instants + * between [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] in any time zone. + * + * On the JVM, + * there are `YearMonth.toJavaYearMonth()` and `java.time.YearMonth.toKotlinYearMonth()` + * extension functions to convert between `kotlinx.datetime` and `java.time` objects used for the same purpose. + * Similarly, on the Darwin platforms, there is a `YearMonth.toNSDateComponents()` extension function. + * + * ### Construction, serialization, and deserialization + * + * [YearMonth] can be constructed directly from its components using the constructor. + * See sample 1. + * + * [parse] and [toString] methods can be used to obtain a [YearMonth] from and convert it to a string in the + * ISO 8601 extended format. + * See sample 2. + * + * [parse] and [YearMonth.format] both support custom formats created with [Format] or defined in [Formats]. + * See sample 3. + * + * Additionally, there are several `kotlinx-serialization` serializers for [YearMonth]: + * - [YearMonthIso8601Serializer] for the ISO 8601 extended format. + * - [YearMonthComponentSerializer] for an object with components. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.constructorFunctionMonthNumber + * @sample kotlinx.datetime.test.samples.YearMonthSamples.simpleParsingAndFormatting + * @sample kotlinx.datetime.test.samples.YearMonthSamples.customFormat + */ +@Serializable(with = YearMonthIso8601Serializer::class) +public expect class YearMonth +/** + * Constructs a [YearMonth] instance from the given year-month components. + * + * The [month] component is 1-based. + * + * The supported ranges of components: + * - [year] the range is platform-dependent, but at least is enough to represent year-months of all instants between + * [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] in any time zone. + * - [month] `1..12` + * + * @throws IllegalArgumentException if any parameter is out of range. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.constructorFunctionMonthNumber + */ +public constructor(year: Int, month: Int) : Comparable { + /** + * Returns the year component of the year-month. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.year + */ + public val year: Int + + /** + * Returns the month ([Month]) component of the year-month. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.month + */ + public val month: Month + + /** + * Returns the first day of the year-month. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.firstAndLastDay + */ + public val firstDay: LocalDate + + /** + * Returns the last day of the year-month. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.firstAndLastDay + */ + public val lastDay: LocalDate + + /** + * Returns the number of days in the year-month. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.numberOfDays + */ + public val numberOfDays: Int + + internal val monthNumber: Int + + /** + * Returns the range of days in the year-month. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.days + */ + public val days: LocalDateRange + + /** + * Constructs a [YearMonth] instance from the given year-month components. + * + * The range for [year] is platform-dependent, but at least is enough to represent year-months of all instants + * between [Instant.DISTANT_PAST] and [Instant.DISTANT_FUTURE] in any time zone. + * + * @throws IllegalArgumentException if [year] is out of range. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.constructorFunction + */ + public constructor(year: Int, month: Month) + + public companion object { + /** + * A shortcut for calling [DateTimeFormat.parse]. + * + * Parses a string that represents a date and returns the parsed [YearMonth] value. + * + * If [format] is not specified, [Formats.ISO] is used. + * `2023-01` is an example of a string in this format. + * + * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [YearMonth] are exceeded. + * + * @see YearMonth.toString for formatting using the default format. + * @see YearMonth.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.parsing + */ + public fun parse(input: CharSequence, format: DateTimeFormat = Formats.ISO): YearMonth + + /** + * Creates a new format for parsing and formatting [YearMonth] values. + * + * There is a collection of predefined formats in [YearMonth.Formats]. + * + * @throws IllegalArgumentException if parsing using this format is ambiguous. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.customFormat + */ + @Suppress("FunctionName") + public fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat + } + + /** + * A collection of predefined formats for parsing and formatting [YearMonth] values. + * + * [YearMonth.Formats.ISO] is a popular predefined format. + * [YearMonth.parse] and [YearMonth.toString] can be used as convenient shortcuts for it. + * + * Use [YearMonth.Format] to create a custom [kotlinx.datetime.format.DateTimeFormat] for [YearMonth] values. + */ + public object Formats { + /** + * ISO 8601 extended format, which is the format used by [YearMonth.toString] and [YearMonth.parse]. + * + * Examples of year-months in ISO 8601 format: + * - `2020-08` + * - `+12020-08` + * - `0000-08` + * - `-0001-08` + * + * See ISO-8601-1:2019, 5.2.2.2a), using the "expanded calendar year" extension from 5.2.2.3b), generalized + * to any number of digits in the year for years that fit in an [Int]. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.Formats.iso + */ + public val ISO: DateTimeFormat + } + + /** + * Creates a [YearMonthRange] from `this` to [that], inclusive. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation + */ + public operator fun rangeTo(that: YearMonth): YearMonthRange + + /** + * Creates a [YearMonthRange] from `this` to [that], exclusive, i.e., from this to (that - 1 month) + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation + */ + public operator fun rangeUntil(that: YearMonth): YearMonthRange + + /** + * Compares `this` date with the [other] year-month. + * Returns zero if this year-month represents the same month as the other (meaning they are equal to one other), + * a negative number if this year-month is earlier than the other, + * and a positive number if this year-month is later than the other. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.compareToSample + */ + override fun compareTo(other: YearMonth): Int + + /** + * Converts this year-month to the extended ISO 8601 string representation. + * + * @see Formats.ISO for the format details. + * @see parse for the dual operation: obtaining [YearMonth] from a string. + * @see YearMonth.format for formatting using a custom format. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.toStringSample + */ + override fun toString(): String +} + +/** + * Formats this value using the given [format]. + * Equivalent to calling [DateTimeFormat.format] on [format] with `this`. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.formatting + */ +public fun YearMonth.format(format: DateTimeFormat): String = format.format(this) + +/** + * Returns the year-month part of this date. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.yearMonth + */ +public val LocalDate.yearMonth: YearMonth get() = YearMonth(year, month) + +/** + * Combines this year-month with the specified day-of-month into a [LocalDate] value. + * + * @sample kotlinx.datetime.test.samples.YearMonthSamples.onDay + */ +public fun YearMonth.onDay(day: Int): LocalDate = LocalDate(year, month, day) + +/** + * Returns the number of whole years between two year-months. + * + * The value is rounded toward zero. + * + * @see YearMonth.until + * @sample kotlinx.datetime.test.samples.YearMonthSamples.yearsUntil + */ +public fun YearMonth.yearsUntil(other: YearMonth): Int = + ((other.prolepticMonth - prolepticMonth) / 12L).toInt() + +/** + * Returns the number of whole months between two year-months. + * + * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a positive result + * or [Int.MIN_VALUE] for a negative result. + * + * @see YearMonth.until + * @sample kotlinx.datetime.test.samples.YearMonthSamples.monthsUntil + */ +public fun YearMonth.monthsUntil(other: YearMonth): Int = + (other.prolepticMonth - prolepticMonth).clampToInt() + +/** + * Returns the whole number of the specified month-based [units][unit] between `this` and [other] year-months. + * + * The value returned is: + * - Positive or zero if this year-month is earlier than the other. + * - Negative or zero if this year-month is later than the other. + * - Zero if this date is equal to the other. + * + * The value is rounded toward zero. + * + * @see YearMonth.monthsUntil + * @see YearMonth.yearsUntil + * @sample kotlinx.datetime.test.samples.YearMonthSamples.until + */ +public fun YearMonth.until(other: YearMonth, unit: DateTimeUnit.MonthBased): Long = + (other.prolepticMonth - prolepticMonth) / unit.months + +/** + * The [YearMonth] 12 months later. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.plusYear + */ +public fun YearMonth.plusYear(): YearMonth = plus(1, DateTimeUnit.YEAR) + +/** + * The [YearMonth] 12 months earlier. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.minusYear + */ +public fun YearMonth.minusYear(): YearMonth = minus(1, DateTimeUnit.YEAR) + +/** + * The [YearMonth] one month later. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.plusMonth + */ +public fun YearMonth.plusMonth(): YearMonth = plus(1, DateTimeUnit.MONTH) + +/** + * The [YearMonth] one month earlier. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.minusMonth + */ +public fun YearMonth.minusMonth(): YearMonth = minus(1, DateTimeUnit.MONTH) + +/** + * Returns a [YearMonth] that results from adding the [value] number of the specified [unit] to this year-month. + * + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.plus + */ +public fun YearMonth.plus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth = + plus(value.toLong(), unit) + +/** + * Returns a [YearMonth] that results from subtracting the [value] number of the specified [unit] from this year-month. + * + * If the [value] is positive, the returned year-month is earlier than this year-month. + * If the [value] is negative, the returned year-month is later than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.minus + */ +public fun YearMonth.minus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth = + minus(value.toLong(), unit) + +/** + * Returns a [YearMonth] that results from adding the [value] number of the specified [unit] to this year-month. + * + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.plus + */ +public fun YearMonth.plus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth = try { + safeMultiply(value, unit.months.toLong()).let { monthsToAdd -> + if (monthsToAdd == 0L) { + this + } else { + YearMonth.fromProlepticMonth(safeAdd(prolepticMonth, monthsToAdd)) + } + } +} catch (e: ArithmeticException) { + throw DateTimeArithmeticException("Arithmetic overflow when adding $value of $unit to $this", e) +} catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException("Boundaries of YearMonth exceeded when adding $value of $unit to $this", e) +} + +/** + * Returns a [YearMonth] that results from subtracting the [value] number of the specified [unit] from this year-month. + * + * If the [value] is positive, the returned year-month is earlier than this year-month. + * If the [value] is negative, the returned year-month is later than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [YearMonth]. + * @sample kotlinx.datetime.test.samples.YearMonthSamples.minus + */ +public fun YearMonth.minus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth = + if (value != Long.MIN_VALUE) plus(-value, unit) else plus(Long.MAX_VALUE, unit).plus(1, unit) + +internal val YearMonth.prolepticMonth: Long get() = year * 12L + (monthNumber - 1) + +internal fun YearMonth.Companion.fromProlepticMonth(prolepticMonth: Long): YearMonth { + val year = prolepticMonth.floorDiv(12) + require(year in LocalDate.MIN.year..LocalDate.MAX.year) { + "Year $year is out of range: ${LocalDate.MIN.year}..${LocalDate.MAX.year}" + } + val month = prolepticMonth.mod(12) + 1 + println("proleptic month: ${prolepticMonth}, year: $year, month: $month") + return YearMonth(year.toInt(), month) +} + +internal val YearMonth.Companion.MAX get() = LocalDate.MAX.yearMonth +internal val YearMonth.Companion.MIN get() = LocalDate.MIN.yearMonth \ No newline at end of file diff --git a/core/common/src/YearMonthRange.kt b/core/common/src/YearMonthRange.kt new file mode 100644 index 00000000..4332473a --- /dev/null +++ b/core/common/src/YearMonthRange.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2019-2022 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime + +import kotlinx.datetime.internal.* +import kotlin.random.Random + +private class YearMonthProgressionIterator(private val iterator: LongIterator) : Iterator { + override fun hasNext(): Boolean = iterator.hasNext() + override fun next(): YearMonth = YearMonth.fromProlepticMonth(iterator.next()) +} + +/** + * A progression of values of type [YearMonth]. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.progressionWithStep + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.reversedProgression + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast + */ +public open class YearMonthProgression +internal constructor(internal val longProgression: LongProgression) : Collection { + + internal constructor( + start: YearMonth, + endInclusive: YearMonth, + step: Long + ) : this(LongProgression.fromClosedRange(start.prolepticMonth, endInclusive.prolepticMonth, step)) + + /** + * Returns the first [YearMonth] of the progression + */ + public val first: YearMonth = YearMonth.fromProlepticMonth(longProgression.first) + + /** + * Returns the last [YearMonth] of the progression + */ + public val last: YearMonth = YearMonth.fromProlepticMonth(longProgression.last) + + /** + * Returns an [Iterator] that traverses the progression from [first] to [last] + */ + override fun iterator(): Iterator = YearMonthProgressionIterator(longProgression.iterator()) + + /** + * Returns true iff the progression contains no values. + * i.e. [first] < [last] if step is positive, or [first] > [last] if step is negative. + */ + public override fun isEmpty(): Boolean = longProgression.isEmpty() + + /** + * Returns a string representation of the progression. + * Uses the range operator notation if the progression is increasing, and `downTo` if it is decreasing. + * The step is referenced in days. + */ + override fun toString(): String = + if (longProgression.step > 0) "$first..$last step ${longProgression.step}M" + else "$first downTo $last step ${longProgression.step}M" + + /** + * Returns the number of months in the progression. + * Returns [Int.MAX_VALUE] if the number of months overflows [Int] + */ + override val size: Int + get() = longProgression.sizeUnsafe + + /** + * Returns true iff every element in [elements] is a member of the progression. + */ + override fun containsAll(elements: Collection): Boolean = + (elements as Collection<*>).all { it is YearMonth && contains(it) } + + /** + * Returns true iff [value] is a member of the progression. + */ + override fun contains(value: YearMonth): Boolean { + @Suppress("USELESS_CAST") + if ((value as Any?) !is YearMonth) return false + + return longProgression.containsUnsafe(value.prolepticMonth) + } + + override fun equals(other: Any?): Boolean = + other is YearMonthProgression && longProgression == other.longProgression + + override fun hashCode(): Int = longProgression.hashCode() + + public companion object { + internal fun fromClosedRange( + rangeStart: YearMonth, + rangeEnd: YearMonth, + stepValue: Long, + stepUnit: DateTimeUnit.MonthBased + ): YearMonthProgression = + YearMonthProgression(rangeStart, rangeEnd, safeMultiplyOrClamp(stepValue, stepUnit.months.toLong())) + } +} + +/** + * A range of values of type [YearMonth]. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation + */ +public class YearMonthRange(start: YearMonth, endInclusive: YearMonth) : YearMonthProgression(start, endInclusive, 1), + ClosedRange, OpenEndRange { + /** + * Returns the lower bound of the range, inclusive. + */ + override val start: YearMonth get() = first + + /** + * Returns the upper bound of the range, inclusive. + */ + override val endInclusive: YearMonth get() = last + + /** + * Returns the upper bound of the range, exclusive. + */ + @Deprecated( + "This throws an exception if the exclusive end if not inside " + + "the platform-specific boundaries for YearMonth. " + + "The 'endInclusive' property does not throw and should be preferred.", + level = DeprecationLevel.WARNING + ) + override val endExclusive: YearMonth + get() { + if (last == YearMonth.MAX) + error("Cannot return the exclusive upper bound of a range that includes YearMonth.MAX.") + return endInclusive.plus(1, DateTimeUnit.MONTH) + } + + /** + * Returns true iff [value] is contained within the range. + * i.e. [value] is between [start] and [endInclusive]. + */ + @Suppress("ConvertTwoComparisonsToRangeCheck") + override fun contains(value: YearMonth): Boolean { + @Suppress("USELESS_CAST") + if ((value as Any?) !is YearMonth) return false + + return first <= value && value <= last + } + + /** + * Returns true iff there are no months contained within the range. + */ + override fun isEmpty(): Boolean = first > last + + /** + * Returns a string representation of the range using the range operator notation. + */ + override fun toString(): String = "$first..$last" + + public companion object { + /** An empty range of values of type YearMonth. */ + public val EMPTY: YearMonthRange = YearMonthRange(YearMonth(0, 2), YearMonth(0, 1)) + + internal fun fromRangeUntil(start: YearMonth, endExclusive: YearMonth): YearMonthRange { + return if (endExclusive == YearMonth.MIN) EMPTY else fromRangeTo( + start, + endExclusive.minus(1, DateTimeUnit.MONTH) + ) + } + + internal fun fromRangeTo(start: YearMonth, endInclusive: YearMonth): YearMonthRange { + return YearMonthRange(start, endInclusive) + } + } +} + +/** + * Returns the first [YearMonth] of the [YearMonthProgression]. + * + * @throws NoSuchElementException if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast + */ +public fun YearMonthProgression.first(): YearMonth { + if (isEmpty()) + throw NoSuchElementException("Progression $this is empty.") + return this.first +} + +/** + * Returns the last [YearMonth] of the [YearMonthProgression]. + * + * @throws NoSuchElementException if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast + */ +public fun YearMonthProgression.last(): YearMonth { + if (isEmpty()) + throw NoSuchElementException("Progression $this is empty.") + return this.last +} + +/** + * Returns the first [YearMonth] of the [YearMonthProgression], or null if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast + */ +public fun YearMonthProgression.firstOrNull(): YearMonth? = if (isEmpty()) null else this.first + +/** + * Returns the last [YearMonth] of the [YearMonthProgression], or null if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast + */ +public fun YearMonthProgression.lastOrNull(): YearMonth? = if (isEmpty()) null else this.last + +/** + * Returns a reversed [YearMonthProgression], i.e. one that goes from [last] to [first]. + * The sign of the step is switched, in order to reverse the direction of the progression. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.reversedProgression + */ +public fun YearMonthProgression.reversed(): YearMonthProgression = YearMonthProgression(longProgression.reversed()) + +/** + * Returns a [YearMonthProgression] with the same start and end, but a changed step value. + * + * **Pitfall**: the value parameter represents the magnitude of the step, + * not the direction, and therefore must be positive. + * Its sign will be matched to the sign of the existing step, in order to maintain the direction of the progression. + * If you wish to switch the direction of the progression, use [YearMonthProgression.reversed] + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.progressionWithStep + */ +public fun YearMonthProgression.step(value: Int, unit: DateTimeUnit.MonthBased): YearMonthProgression = + step(value.toLong(), unit) + +/** + * Returns a [YearMonthProgression] with the same start and end, but a changed step value. + * + * **Pitfall**: the value parameter represents the magnitude of the step, + * not the direction, and therefore must be positive. + * Its sign will be matched to the sign of the existing step, in order to maintain the direction of the progression. + * If you wish to switch the direction of the progression, use [YearMonthProgression.reversed] + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.progressionWithStep + */ +public fun YearMonthProgression.step(value: Long, unit: DateTimeUnit.MonthBased): YearMonthProgression = + YearMonthProgression(longProgression.step(safeMultiplyOrClamp(value, unit.months.toLong()))) + +/** + * Creates a [YearMonthProgression] from `this` down to [that], inclusive. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation + */ +public infix fun YearMonth.downTo(that: YearMonth): YearMonthProgression = + YearMonthProgression.fromClosedRange(this, that, -1, DateTimeUnit.MONTH) + +/** + * Returns a random [YearMonth] within the bounds of the [YearMonthProgression]. + * + * Takes the step into account; + * will not return any value within the range that would be skipped over by the progression. + * + * @throws NoSuchElementException if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.random + */ +public fun YearMonthProgression.random(random: Random = Random): YearMonth = + if (isEmpty()) throw NoSuchElementException("Cannot get random in empty range: $this") + else longProgression.randomUnsafe(random).let(YearMonth.Companion::fromProlepticMonth) + +/** + * Returns a random [YearMonth] within the bounds of the [YearMonthProgression] or null if the progression is empty. + * + * Takes the step into account; + * will not return any value within the range that would be skipped over by the progression. + * + * @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.random + */ +public fun YearMonthProgression.randomOrNull(random: Random = Random): YearMonth? = longProgression.randomUnsafeOrNull(random) + ?.let(YearMonth.Companion::fromProlepticMonth) \ No newline at end of file diff --git a/core/common/src/format/DateTimeComponents.kt b/core/common/src/format/DateTimeComponents.kt index 67e43067..86648b70 100644 --- a/core/common/src/format/DateTimeComponents.kt +++ b/core/common/src/format/DateTimeComponents.kt @@ -181,9 +181,21 @@ public class DateTimeComponents internal constructor(internal val contents: Date contents.time.populateFrom(localTime) } + /** + * Writes the contents of the specified [yearMonth] to this [DateTimeComponents]. + * The [yearMonth] is written to the [year] and [monthNumber] fields. + * + * If any of the fields are already set, they will be overwritten. + * + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.yearMonth + */ + public fun setYearMonth(yearMonth: YearMonth) { + contents.date.yearMonth.populateFrom(yearMonth) + } + /** * Writes the contents of the specified [localDate] to this [DateTimeComponents]. - * The [localDate] is written to the [year], [monthNumber], [day], and [dayOfWeek] fields. + * The [localDate] is written to the [year], [monthNumber], [day], [dayOfWeek], and [dayOfYear] fields. * * If any of the fields are already set, they will be overwritten. * @@ -416,6 +428,18 @@ public class DateTimeComponents internal constructor(internal val contents: Date */ public fun toUtcOffset(): UtcOffset = contents.offset.toUtcOffset() + /** + * Builds a [YearMonth] from the fields in this [DateTimeComponents]. + * + * This method uses the following fields: + * * [year] + * * [monthNumber] + * + * @throws IllegalArgumentException if any of the fields is missing or invalid. + * @sample kotlinx.datetime.test.samples.format.DateTimeComponentsSamples.toYearMonth + */ + public fun toYearMonth(): YearMonth = contents.date.yearMonth.toYearMonth() + /** * Builds a [LocalDate] from the fields in this [DateTimeComponents]. * @@ -424,6 +448,11 @@ public class DateTimeComponents internal constructor(internal val contents: Date * * [monthNumber] * * [day] * + * Alternatively, the following fields can be used: + * * [year] + * * [dayOfYear] + * + * If both sets of fields are specified, they are checked for consistency. * Also, [dayOfWeek] is checked for consistency with the other fields. * * @throws IllegalArgumentException if any of the fields is missing or invalid. diff --git a/core/common/src/format/DateTimeFormat.kt b/core/common/src/format/DateTimeFormat.kt index 375c4732..501c0416 100644 --- a/core/common/src/format/DateTimeFormat.kt +++ b/core/common/src/format/DateTimeFormat.kt @@ -166,5 +166,6 @@ private val allFormatConstants: List>> by "${DateTimeFormatBuilder.WithUtcOffset::offset.name}(UtcOffset.Formats.ISO)" to unwrap(UtcOffset.Formats.ISO), "${DateTimeFormatBuilder.WithUtcOffset::offset.name}(UtcOffset.Formats.ISO_BASIC)" to unwrap(UtcOffset.Formats.ISO_BASIC), "${DateTimeFormatBuilder.WithUtcOffset::offset.name}(UtcOffset.Formats.FOUR_DIGITS)" to unwrap(UtcOffset.Formats.FOUR_DIGITS), + "${DateTimeFormatBuilder.WithYearMonth::yearMonth.name}(YearMonth.Formats.ISO)" to unwrap(YearMonth.Formats.ISO), ) } diff --git a/core/common/src/format/DateTimeFormatBuilder.kt b/core/common/src/format/DateTimeFormatBuilder.kt index ff651335..4021da62 100644 --- a/core/common/src/format/DateTimeFormatBuilder.kt +++ b/core/common/src/format/DateTimeFormatBuilder.kt @@ -24,9 +24,9 @@ public sealed interface DateTimeFormatBuilder { public fun chars(value: String) /** - * Functions specific to the datetime format builders containing the local-date fields. + * Functions specific to the datetime format builders containing the year and month fields. */ - public sealed interface WithDate : DateTimeFormatBuilder { + public sealed interface WithYearMonth : DateTimeFormatBuilder { /** * A year number. * @@ -35,7 +35,7 @@ public sealed interface DateTimeFormatBuilder { * For years outside this range, it's formatted as a decimal number with a leading sign, so the year 12345 * is formatted as "+12345". * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.year + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.year */ public fun year(padding: Padding = Padding.ZERO) @@ -54,7 +54,7 @@ public sealed interface DateTimeFormatBuilder { * and when given a full year number with a leading sign, it parses the full year number, * so "+1850" becomes 1850. * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.yearTwoDigits + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.yearTwoDigits */ public fun yearTwoDigits(baseYear: Int) @@ -63,21 +63,29 @@ public sealed interface DateTimeFormatBuilder { * * By default, it's padded with zeros to two digits. This can be changed by passing [padding]. * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.monthNumber + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.monthNumber */ public fun monthNumber(padding: Padding = Padding.ZERO) /** * A month name (for example, "January"). * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.monthName + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.monthName */ public fun monthName(names: MonthNames) - /** @suppress */ - @Deprecated("Use 'day' instead", ReplaceWith("day(padding = padding)")) - public fun dayOfMonth(padding: Padding = Padding.ZERO) { day(padding) } + /** + * An existing [DateTimeFormat] for the date part. + * + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.yearMonth + */ + public fun yearMonth(format: DateTimeFormat) + } + /** + * Functions specific to the datetime format builders containing the local-date fields. + */ + public sealed interface WithDate : WithYearMonth { /** * A day-of-month number, from 1 to 31. * @@ -87,6 +95,10 @@ public sealed interface DateTimeFormatBuilder { */ public fun day(padding: Padding = Padding.ZERO) + /** @suppress */ + @Deprecated("Use 'day' instead", ReplaceWith("day(padding = padding)")) + public fun dayOfMonth(padding: Padding = Padding.ZERO) { day(padding) } + /** * A day-of-week name (for example, "Thursday"). * diff --git a/core/common/src/format/LocalDateFormat.kt b/core/common/src/format/LocalDateFormat.kt index f34e3dc8..c472ea19 100644 --- a/core/common/src/format/LocalDateFormat.kt +++ b/core/common/src/format/LocalDateFormat.kt @@ -6,98 +6,11 @@ package kotlinx.datetime.format import kotlinx.datetime.* -import kotlinx.datetime.format.MonthNames.Companion.ENGLISH_ABBREVIATED -import kotlinx.datetime.format.MonthNames.Companion.ENGLISH_FULL +import kotlinx.datetime.LocalDate import kotlinx.datetime.internal.* import kotlinx.datetime.internal.format.* import kotlinx.datetime.internal.format.parser.Copyable - -/** - * A description of how month names are formatted. - * - * Instances of this class are typically used as arguments to [DateTimeFormatBuilder.WithDate.monthName]. - * - * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED]. - * You can also create custom instances using the constructor. - * - * An [IllegalArgumentException] will be thrown if some month name is empty or there are duplicate names. - * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.usage - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.constructionFromList - */ -public class MonthNames( - /** - * A list of month names in order from January to December. - * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.names - */ - public val names: List -) { - init { - require(names.size == 12) { "Month names must contain exactly 12 elements" } - names.indices.forEach { ix -> - require(names[ix].isNotEmpty()) { "A month name can not be empty" } - for (ix2 in 0 until ix) { - require(names[ix] != names[ix2]) { - "Month names must be unique, but '${names[ix]}' was repeated" - } - } - } - } - - /** - * Create a [MonthNames] using the month names in order from January to December. - * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.constructionFromStrings - */ - public constructor( - january: String, february: String, march: String, april: String, may: String, june: String, - july: String, august: String, september: String, october: String, november: String, december: String - ) : - this(listOf(january, february, march, april, may, june, july, august, september, october, november, december)) - - public companion object { - /** - * English month names from 'January' to 'December'. - * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.englishFull - */ - public val ENGLISH_FULL: MonthNames = MonthNames( - listOf( - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ) - ) - - /** - * Shortened English month names from 'Jan' to 'Dec'. - * - * @sample kotlinx.datetime.test.samples.format.LocalDateFormatSamples.MonthNamesSamples.englishAbbreviated - */ - public val ENGLISH_ABBREVIATED: MonthNames = MonthNames( - listOf( - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - ) - ) - } - - /** @suppress */ - override fun toString(): String = - names.joinToString(", ", "MonthNames(", ")", transform = String::toString) - - /** @suppress */ - override fun equals(other: Any?): Boolean = other is MonthNames && names == other.names - - /** @suppress */ - override fun hashCode(): Int = names.hashCode() -} - -private fun MonthNames.toKotlinCode(): String = when (this.names) { - MonthNames.ENGLISH_FULL.names -> "MonthNames.${MonthNames.Companion::ENGLISH_FULL.name}" - MonthNames.ENGLISH_ABBREVIATED.names -> "MonthNames.${MonthNames.Companion::ENGLISH_ABBREVIATED.name}" - else -> names.joinToString(", ", "MonthNames(", ")", transform = String::toKotlinCode) -} +import kotlinx.datetime.number /** * A description of how the names of weekdays are formatted. @@ -146,7 +59,7 @@ public class DayOfWeekNames( saturday: String, sunday: String ) : - this(listOf(monday, tuesday, wednesday, thursday, friday, saturday, sunday)) + this(listOf(monday, tuesday, wednesday, thursday, friday, saturday, sunday)) public companion object { /** @@ -189,24 +102,13 @@ private fun DayOfWeekNames.toKotlinCode(): String = when (this.names) { else -> names.joinToString(", ", "DayOfWeekNames(", ")", transform = String::toKotlinCode) } -internal fun requireParsedField(field: T?, name: String): T { - if (field == null) { - throw DateTimeFormatException("Can not create a $name from the given input: the field $name is missing") - } - return field -} - -internal interface DateFieldContainer { - var year: Int? - var monthNumber: Int? +internal interface DateFieldContainer: YearMonthFieldContainer { var day: Int? var dayOfWeek: Int? var dayOfYear: Int? } private object DateFields { - val year = GenericFieldSpec(PropertyAccessor(DateFieldContainer::year)) - val month = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::monthNumber), minValue = 1, maxValue = 12) val day = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::day), minValue = 1, maxValue = 31) val isoDayOfWeek = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::dayOfWeek), minValue = 1, maxValue = 7) val dayOfYear = UnsignedFieldSpec(PropertyAccessor(DateFieldContainer::dayOfYear), minValue = 1, maxValue = 366) @@ -216,12 +118,11 @@ private object DateFields { * A [kotlinx.datetime.LocalDate], but potentially incomplete and inconsistent. */ internal class IncompleteLocalDate( - override var year: Int? = null, - override var monthNumber: Int? = null, + val yearMonth: IncompleteYearMonth = IncompleteYearMonth(), override var day: Int? = null, override var dayOfWeek: Int? = null, override var dayOfYear: Int? = null, -) : DateFieldContainer, Copyable { +) : YearMonthFieldContainer by yearMonth, DateFieldContainer, Copyable { fun toLocalDate(): LocalDate { val year = requireParsedField(year, "year") val date = when (val dayOfYear = dayOfYear) { @@ -234,13 +135,13 @@ internal class IncompleteLocalDate( if (it.year != year) { throw DateTimeFormatException( "Can not create a LocalDate from the given input: " + - "the day of year is $dayOfYear, which is not a valid day of year for the year $year" + "the day of year is $dayOfYear, which is not a valid day of year for the year $year" ) } - if (monthNumber != null && it.monthNumber != monthNumber) { + if (monthNumber != null && it.month.number != monthNumber) { throw DateTimeFormatException( "Can not create a LocalDate from the given input: " + - "the day of year is $dayOfYear, which is ${it.month}, " + + "the day of year is $dayOfYear, which is ${it.month}, " + "but $monthNumber was specified as the month number" ) } @@ -257,7 +158,7 @@ internal class IncompleteLocalDate( if (it != date.dayOfWeek.isoDayNumber) { throw DateTimeFormatException( "Can not create a LocalDate from the given input: " + - "the day of week is ${DayOfWeek(it)} but the date is $date, which is a ${date.dayOfWeek}" + "the day of week is ${DayOfWeek(it)} but the date is $date, which is a ${date.dayOfWeek}" ) } } @@ -273,125 +174,26 @@ internal class IncompleteLocalDate( } override fun copy(): IncompleteLocalDate = - IncompleteLocalDate(year, monthNumber, day, dayOfWeek, dayOfYear) + IncompleteLocalDate(yearMonth.copy(), day, dayOfWeek, dayOfYear) override fun equals(other: Any?): Boolean = - other is IncompleteLocalDate && year == other.year && monthNumber == other.monthNumber && + other is IncompleteLocalDate && yearMonth == other.yearMonth && day == other.day && dayOfWeek == other.dayOfWeek && dayOfYear == other.dayOfYear - override fun hashCode(): Int = year.hashCode() * 923521 + - monthNumber.hashCode() * 29791 + + override fun hashCode(): Int = yearMonth.hashCode() * 29791 + day.hashCode() * 961 + dayOfWeek.hashCode() * 31 + dayOfYear.hashCode() - override fun toString(): String = - "${year ?: "??"}-${monthNumber ?: "??"}-${day ?: "??"} (day of week is ${dayOfWeek ?: "??"})" -} - -private class YearDirective(private val padding: Padding, private val isYearOfEra: Boolean = false) : - SignedIntFieldFormatDirective( - DateFields.year, - minDigits = padding.minDigits(4), - maxDigits = null, - spacePadding = padding.spaces(4), - outputPlusOnExceededWidth = 4, - ) { - override val builderRepresentation: String - get() = when (padding) { - Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::year.name}()" - else -> "${DateTimeFormatBuilder.WithDate::year.name}(${padding.toKotlinCode()})" - }.let { - if (isYearOfEra) { - it + YEAR_OF_ERA_COMMENT - } else it - } - - override fun equals(other: Any?): Boolean = - other is YearDirective && padding == other.padding && isYearOfEra == other.isYearOfEra - - override fun hashCode(): Int = padding.hashCode() * 31 + isYearOfEra.hashCode() -} - -private class ReducedYearDirective(val base: Int, private val isYearOfEra: Boolean = false) : - ReducedIntFieldDirective( - DateFields.year, - digits = 2, - base = base, - ) { - override val builderRepresentation: String - get() = - "${DateTimeFormatBuilder.WithDate::yearTwoDigits.name}($base)".let { - if (isYearOfEra) { - it + YEAR_OF_ERA_COMMENT - } else it - } - - override fun equals(other: Any?): Boolean = - other is ReducedYearDirective && base == other.base && isYearOfEra == other.isYearOfEra - - override fun hashCode(): Int = base.hashCode() * 31 + isYearOfEra.hashCode() -} - -private const val YEAR_OF_ERA_COMMENT = - " /** TODO: the original format had an `y` directive, so the behavior is different on years earlier than 1 AD. See the [kotlinx.datetime.format.byUnicodePattern] documentation for details. */" - -/** - * A special directive for year-of-era that behaves equivalently to [DateTimeFormatBuilder.WithDate.year]. - * This is the result of calling [byUnicodePattern] on a pattern that uses the ubiquitous "y" symbol. - * We need a separate directive so that, when using [DateTimeFormat.formatAsKotlinBuilderDsl], we can print an - * additional comment and explain that the behavior was not preserved exactly. - */ -internal fun DateTimeFormatBuilder.WithDate.yearOfEra(padding: Padding) { - @Suppress("NO_ELSE_IN_WHEN") - when (this) { - is AbstractWithDateBuilder -> addFormatStructureForDate( - BasicFormatStructure(YearDirective(padding, isYearOfEra = true)) - ) - } -} - -/** - * A special directive for year-of-era that behaves equivalently to [DateTimeFormatBuilder.WithDate.year]. - * This is the result of calling [byUnicodePattern] on a pattern that uses the ubiquitous "y" symbol. - * We need a separate directive so that, when using [DateTimeFormat.formatAsKotlinBuilderDsl], we can print an - * additional comment and explain that the behavior was not preserved exactly. - */ -internal fun DateTimeFormatBuilder.WithDate.yearOfEraTwoDigits(baseYear: Int) { - @Suppress("NO_ELSE_IN_WHEN") - when (this) { - is AbstractWithDateBuilder -> addFormatStructureForDate( - BasicFormatStructure(ReducedYearDirective(baseYear, isYearOfEra = true)) - ) + override fun toString(): String = when { + dayOfYear == null -> + "$yearMonth-${day ?: "??"} (day of week is ${dayOfWeek ?: "??"})" + day == null && monthNumber == null -> + "(${yearMonth.year ?: "??"})-$dayOfYear (day of week is ${dayOfWeek ?: "??"})" + else -> "$yearMonth-${day ?: "??"} (day of week is ${dayOfWeek ?: "??"}, day of year is $dayOfYear)" } } -private class MonthDirective(private val padding: Padding) : - UnsignedIntFieldFormatDirective( - DateFields.month, - minDigits = padding.minDigits(2), - spacePadding = padding.spaces(2), - ) { - override val builderRepresentation: String - get() = when (padding) { - Padding.ZERO -> "${DateTimeFormatBuilder.WithDate::monthNumber.name}()" - else -> "${DateTimeFormatBuilder.WithDate::monthNumber.name}(${padding.toKotlinCode()})" - } - - override fun equals(other: Any?): Boolean = other is MonthDirective && padding == other.padding - override fun hashCode(): Int = padding.hashCode() -} - -private class MonthNameDirective(private val names: MonthNames) : - NamedUnsignedIntFieldFormatDirective(DateFields.month, names.names, "monthName") { - override val builderRepresentation: String - get() = - "${DateTimeFormatBuilder.WithDate::monthName.name}(${names.toKotlinCode()})" - - override fun equals(other: Any?): Boolean = other is MonthNameDirective && names.names == other.names.names - override fun hashCode(): Int = names.names.hashCode() -} - private class DayDirective(private val padding: Padding) : UnsignedIntFieldFormatDirective( DateFields.day, @@ -462,20 +264,12 @@ internal class LocalDateFormat(override val actualFormat: CachedFormatStructure< } } -internal interface AbstractWithDateBuilder : DateTimeFormatBuilder.WithDate { +internal interface AbstractWithDateBuilder : AbstractWithYearMonthBuilder, DateTimeFormatBuilder.WithDate { fun addFormatStructureForDate(structure: FormatStructure) - override fun year(padding: Padding) = - addFormatStructureForDate(BasicFormatStructure(YearDirective(padding))) - - override fun yearTwoDigits(baseYear: Int) = - addFormatStructureForDate(BasicFormatStructure(ReducedYearDirective(baseYear))) - - override fun monthNumber(padding: Padding) = - addFormatStructureForDate(BasicFormatStructure(MonthDirective(padding))) - - override fun monthName(names: MonthNames) = - addFormatStructureForDate(BasicFormatStructure(MonthNameDirective(names))) + override fun addFormatStructureForYearMonth(structure: FormatStructure) { + addFormatStructureForDate(structure) + } override fun day(padding: Padding) = addFormatStructureForDate(BasicFormatStructure(DayDirective(padding))) diff --git a/core/common/src/format/Unicode.kt b/core/common/src/format/Unicode.kt index 13655bce..97404a77 100644 --- a/core/common/src/format/Unicode.kt +++ b/core/common/src/format/Unicode.kt @@ -123,6 +123,13 @@ public fun DateTimeFormatBuilder.byUnicodePattern(pattern: String) { format.addToFormat(builder) } + is UnicodeFormat.Directive.YearMonthBased -> { + require(builder is DateTimeFormatBuilder.WithYearMonth) { + "A year-month-based directive $format was used in a format builder that doesn't support year-month components" + } + format.addToFormat(builder) + } + is UnicodeFormat.Directive.DateBased -> { require(builder is DateTimeFormatBuilder.WithDate) { "A date-based directive $format was used in a format builder that doesn't support date components" @@ -247,17 +254,21 @@ internal sealed interface UnicodeFormat { override fun hashCode(): Int = formatLetter.hashCode() * 31 + formatLength - sealed class DateBased : Directive() { - abstract fun addToFormat(builder: DateTimeFormatBuilder.WithDate) + sealed class YearMonthBased : DateBased() { + abstract fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) + override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + val downcastedBuilder: DateTimeFormatBuilder.WithYearMonth = builder + addToFormat(downcastedBuilder) + } - class Era(override val formatLength: Int) : DateBased() { + class Era(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'G' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = localizedDirective() + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) = localizedDirective() } - class Year(override val formatLength: Int) : DateBased() { + class Year(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'u' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) { when (formatLength) { 1 -> builder.year(padding = Padding.NONE) 2 -> builder.yearTwoDigits(baseYear = 2000) @@ -268,9 +279,9 @@ internal sealed interface UnicodeFormat { } } - class YearOfEra(override val formatLength: Int) : DateBased() { + class YearOfEra(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'y' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = when (formatLength) { + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) = when (formatLength) { 1 -> builder.yearOfEra(padding = Padding.NONE) 2 -> builder.yearOfEraTwoDigits(baseYear = 2000) 3 -> unsupportedPadding(formatLength) @@ -279,32 +290,21 @@ internal sealed interface UnicodeFormat { } } - class CyclicYearName(override val formatLength: Int) : DateBased() { + class CyclicYearName(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'U' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = unsupportedDirective("cyclic-year") + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) = unsupportedDirective("cyclic-year") } // https://cldr.unicode.org/development/development-process/design-proposals/pattern-character-for-related-year - class RelatedGregorianYear(override val formatLength: Int) : DateBased() { + class RelatedGregorianYear(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'r' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) = unsupportedDirective("related-gregorian-year") } - class DayOfYear(override val formatLength: Int) : DateBased() { - override val formatLetter = 'D' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { - when (formatLength) { - 1 -> builder.dayOfYear(Padding.NONE) - 3 -> builder.dayOfYear(Padding.ZERO) - else -> unknownLength() - } - } - } - - class MonthOfYear(override val formatLength: Int) : DateBased() { + class MonthOfYear(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'M' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) { when (formatLength) { 1 -> builder.monthNumber(Padding.NONE) 2 -> builder.monthNumber(Padding.ZERO) @@ -314,9 +314,9 @@ internal sealed interface UnicodeFormat { } } - class StandaloneMonthOfYear(override val formatLength: Int) : DateBased() { + class StandaloneMonthOfYear(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'L' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) { when (formatLength) { 1 -> builder.monthNumber(Padding.NONE) 2 -> builder.monthNumber(Padding.ZERO) @@ -326,24 +326,9 @@ internal sealed interface UnicodeFormat { } } - class DayOfMonth(override val formatLength: Int) : DateBased() { - override val formatLetter = 'd' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = when (formatLength) { - 1 -> builder.day(Padding.NONE) - 2 -> builder.day(Padding.ZERO) - else -> unknownLength() - } - } - - class ModifiedJulianDay(override val formatLength: Int) : DateBased() { - override val formatLetter = 'g' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = - unsupportedDirective("modified-julian-day") - } - - class QuarterOfYear(override val formatLength: Int) : DateBased() { + class QuarterOfYear(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'Q' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) { when (formatLength) { 1, 2 -> unsupportedDirective("quarter-of-year") 3, 4, 5 -> localizedDirective() @@ -352,9 +337,9 @@ internal sealed interface UnicodeFormat { } } - class StandaloneQuarterOfYear(override val formatLength: Int) : DateBased() { + class StandaloneQuarterOfYear(override val formatLength: Int) : YearMonthBased() { override val formatLetter = 'q' - override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + override fun addToFormat(builder: DateTimeFormatBuilder.WithYearMonth) { when (formatLength) { 1, 2 -> unsupportedDirective("standalone-quarter-of-year") 3, 4, 5 -> localizedDirective() @@ -363,6 +348,38 @@ internal sealed interface UnicodeFormat { } } + } + + sealed class DateBased : Directive() { + abstract fun addToFormat(builder: DateTimeFormatBuilder.WithDate) + + class DayOfYear(override val formatLength: Int) : DateBased() { + override val formatLetter = 'D' + override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) { + when (formatLength) { + 1 -> builder.dayOfYear(Padding.NONE) + 3 -> builder.dayOfYear(Padding.ZERO) + else -> unknownLength() + } + } + } + + class DayOfMonth(override val formatLength: Int) : DateBased() { + override val formatLetter = 'd' + override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = when (formatLength) { + 1 -> builder.day(Padding.NONE) + 2 -> builder.day(Padding.ZERO) + else -> unknownLength() + } + } + + class ModifiedJulianDay(override val formatLength: Int) : DateBased() { + override val formatLetter = 'g' + override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = + unsupportedDirective("modified-julian-day") + } + + class WeekBasedYear(override val formatLength: Int) : DateBased() { override val formatLetter = 'Y' override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = @@ -401,7 +418,6 @@ internal sealed interface UnicodeFormat { override fun addToFormat(builder: DateTimeFormatBuilder.WithDate) = unsupportedDirective("day-of-week-in-month") } - } sealed class TimeBased : Directive() { @@ -582,16 +598,16 @@ internal sealed interface UnicodeFormat { private class UnknownUnicodeDirective(override val formatLetter: Char, override val formatLength: Int) : UnicodeFormat.Directive() private fun unicodeDirective(char: Char, formatLength: Int): UnicodeFormat = when (char) { - 'G' -> UnicodeFormat.Directive.DateBased.Era(formatLength) - 'y' -> UnicodeFormat.Directive.DateBased.YearOfEra(formatLength) + 'G' -> UnicodeFormat.Directive.YearMonthBased.Era(formatLength) + 'y' -> UnicodeFormat.Directive.YearMonthBased.YearOfEra(formatLength) + 'u' -> UnicodeFormat.Directive.YearMonthBased.Year(formatLength) + 'U' -> UnicodeFormat.Directive.YearMonthBased.CyclicYearName(formatLength) + 'r' -> UnicodeFormat.Directive.YearMonthBased.RelatedGregorianYear(formatLength) + 'Q' -> UnicodeFormat.Directive.YearMonthBased.QuarterOfYear(formatLength) + 'q' -> UnicodeFormat.Directive.YearMonthBased.StandaloneQuarterOfYear(formatLength) + 'M' -> UnicodeFormat.Directive.YearMonthBased.MonthOfYear(formatLength) + 'L' -> UnicodeFormat.Directive.YearMonthBased.StandaloneMonthOfYear(formatLength) 'Y' -> UnicodeFormat.Directive.DateBased.WeekBasedYear(formatLength) - 'u' -> UnicodeFormat.Directive.DateBased.Year(formatLength) - 'U' -> UnicodeFormat.Directive.DateBased.CyclicYearName(formatLength) - 'r' -> UnicodeFormat.Directive.DateBased.RelatedGregorianYear(formatLength) - 'Q' -> UnicodeFormat.Directive.DateBased.QuarterOfYear(formatLength) - 'q' -> UnicodeFormat.Directive.DateBased.StandaloneQuarterOfYear(formatLength) - 'M' -> UnicodeFormat.Directive.DateBased.MonthOfYear(formatLength) - 'L' -> UnicodeFormat.Directive.DateBased.StandaloneMonthOfYear(formatLength) 'w' -> UnicodeFormat.Directive.DateBased.WeekOfWeekBasedYear(formatLength) 'W' -> UnicodeFormat.Directive.DateBased.WeekOfMonth(formatLength) 'd' -> UnicodeFormat.Directive.DateBased.DayOfMonth(formatLength) diff --git a/core/common/src/format/YearMonthFormat.kt b/core/common/src/format/YearMonthFormat.kt new file mode 100644 index 00000000..a8a0a7d6 --- /dev/null +++ b/core/common/src/format/YearMonthFormat.kt @@ -0,0 +1,300 @@ +/* + * Copyright 2019-2023 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.format + +import kotlinx.datetime.* +import kotlinx.datetime.internal.* +import kotlinx.datetime.internal.format.* +import kotlinx.datetime.internal.format.parser.Copyable + +/** + * A description of how month names are formatted. + * + * Instances of this class are typically used as arguments to [DateTimeFormatBuilder.WithYearMonth.monthName]. + * + * Predefined instances are available as [ENGLISH_FULL] and [ENGLISH_ABBREVIATED]. + * You can also create custom instances using the constructor. + * + * An [IllegalArgumentException] will be thrown if some month name is empty or there are duplicate names. + * + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.MonthNamesSamples.usage + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.MonthNamesSamples.constructionFromList + */ +public class MonthNames( + /** + * A list of month names in order from January to December. + * + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.MonthNamesSamples.names + */ + public val names: List +) { + init { + require(names.size == 12) { "Month names must contain exactly 12 elements" } + names.indices.forEach { ix -> + require(names[ix].isNotEmpty()) { "A month name can not be empty" } + for (ix2 in 0 until ix) { + require(names[ix] != names[ix2]) { + "Month names must be unique, but '${names[ix]}' was repeated" + } + } + } + } + + /** + * Create a [MonthNames] using the month names in order from January to December. + * + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.MonthNamesSamples.constructionFromStrings + */ + public constructor( + january: String, february: String, march: String, april: String, may: String, june: String, + july: String, august: String, september: String, october: String, november: String, december: String + ) : + this(listOf(january, february, march, april, may, june, july, august, september, october, november, december)) + + public companion object { + /** + * English month names from 'January' to 'December'. + * + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.MonthNamesSamples.englishFull + */ + public val ENGLISH_FULL: MonthNames = MonthNames( + listOf( + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ) + ) + + /** + * Shortened English month names from 'Jan' to 'Dec'. + * + * @sample kotlinx.datetime.test.samples.format.YearMonthFormatSamples.MonthNamesSamples.englishAbbreviated + */ + public val ENGLISH_ABBREVIATED: MonthNames = MonthNames( + listOf( + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ) + ) + } + + /** @suppress */ + override fun toString(): String = + names.joinToString(", ", "MonthNames(", ")", transform = String::toString) + + /** @suppress */ + override fun equals(other: Any?): Boolean = other is MonthNames && names == other.names + + /** @suppress */ + override fun hashCode(): Int = names.hashCode() +} + +private fun MonthNames.toKotlinCode(): String = when (this.names) { + MonthNames.ENGLISH_FULL.names -> "MonthNames.${MonthNames::ENGLISH_FULL.name}" + MonthNames.ENGLISH_ABBREVIATED.names -> "MonthNames.${MonthNames::ENGLISH_ABBREVIATED.name}" + else -> names.joinToString(", ", "MonthNames(", ")", transform = String::toKotlinCode) +} + +internal fun requireParsedField(field: T?, name: String): T { + if (field == null) { + throw DateTimeFormatException("Can not create a $name from the given input: the field $name is missing") + } + return field +} + +internal interface YearMonthFieldContainer { + var year: Int? + var monthNumber: Int? +} + +private object YearMonthFields { + val year = GenericFieldSpec(PropertyAccessor(YearMonthFieldContainer::year)) + val month = UnsignedFieldSpec(PropertyAccessor(YearMonthFieldContainer::monthNumber), minValue = 1, maxValue = 12) +} + +internal class IncompleteYearMonth( + override var year: Int? = null, + override var monthNumber: Int? = null, +) : YearMonthFieldContainer, Copyable { + fun toYearMonth(): YearMonth { + val year = requireParsedField(year, "year") + val month = requireParsedField(monthNumber, "monthNumber") + return YearMonth(year, month) + } + + fun populateFrom(yearMonth: YearMonth) { + year = yearMonth.year + monthNumber = yearMonth.month.number + } + + override fun copy(): IncompleteYearMonth = IncompleteYearMonth(year, monthNumber) + + override fun equals(other: Any?): Boolean = + other is IncompleteYearMonth && year == other.year && monthNumber == other.monthNumber + + override fun hashCode(): Int = year.hashCode() * 31 + monthNumber.hashCode() + + override fun toString(): String = "${year ?: "??"}-${monthNumber ?: "??"}" +} + +private class YearDirective(private val padding: Padding, private val isYearOfEra: Boolean = false) : + SignedIntFieldFormatDirective( + YearMonthFields.year, + minDigits = padding.minDigits(4), + maxDigits = null, + spacePadding = padding.spaces(4), + outputPlusOnExceededWidth = 4, + ) { + override val builderRepresentation: String + get() = when (padding) { + Padding.ZERO -> "${DateTimeFormatBuilder.WithYearMonth::year.name}()" + else -> "${DateTimeFormatBuilder.WithYearMonth::year.name}(${padding.toKotlinCode()})" + }.let { + if (isYearOfEra) { + it + YEAR_OF_ERA_COMMENT + } else it + } + + override fun equals(other: Any?): Boolean = + other is YearDirective && padding == other.padding && isYearOfEra == other.isYearOfEra + + override fun hashCode(): Int = padding.hashCode() * 31 + isYearOfEra.hashCode() +} + +private class ReducedYearDirective(val base: Int, private val isYearOfEra: Boolean = false) : + ReducedIntFieldDirective( + YearMonthFields.year, + digits = 2, + base = base, + ) { + override val builderRepresentation: String + get() = + "${DateTimeFormatBuilder.WithYearMonth::yearTwoDigits.name}($base)".let { + if (isYearOfEra) { + it + YEAR_OF_ERA_COMMENT + } else it + } + + override fun equals(other: Any?): Boolean = + other is ReducedYearDirective && base == other.base && isYearOfEra == other.isYearOfEra + + override fun hashCode(): Int = base.hashCode() * 31 + isYearOfEra.hashCode() +} + +private const val YEAR_OF_ERA_COMMENT = + " /** TODO: the original format had an `y` directive, so the behavior is different on years earlier than 1 AD. See the [kotlinx.datetime.format.byUnicodePattern] documentation for details. */" + +/** + * A special directive for year-of-era that behaves equivalently to [DateTimeFormatBuilder.WithYearMonth.year]. + * This is the result of calling [byUnicodePattern] on a pattern that uses the ubiquitous "y" symbol. + * We need a separate directive so that, when using [DateTimeFormat.formatAsKotlinBuilderDsl], we can print an + * additional comment and explain that the behavior was not preserved exactly. + */ +internal fun DateTimeFormatBuilder.WithYearMonth.yearOfEra(padding: Padding) { + @Suppress("NO_ELSE_IN_WHEN") + when (this) { + is AbstractWithYearMonthBuilder -> addFormatStructureForYearMonth( + BasicFormatStructure(YearDirective(padding, isYearOfEra = true)) + ) + } +} + +/** + * A special directive for year-of-era that behaves equivalently to [DateTimeFormatBuilder.WithYearMonth.year]. + * This is the result of calling [byUnicodePattern] on a pattern that uses the ubiquitous "y" symbol. + * We need a separate directive so that, when using [DateTimeFormat.formatAsKotlinBuilderDsl], we can print an + * additional comment and explain that the behavior was not preserved exactly. + */ +internal fun DateTimeFormatBuilder.WithYearMonth.yearOfEraTwoDigits(baseYear: Int) { + @Suppress("NO_ELSE_IN_WHEN") + when (this) { + is AbstractWithYearMonthBuilder -> addFormatStructureForYearMonth( + BasicFormatStructure(ReducedYearDirective(baseYear, isYearOfEra = true)) + ) + } +} + +private class MonthDirective(private val padding: Padding) : + UnsignedIntFieldFormatDirective( + YearMonthFields.month, + minDigits = padding.minDigits(2), + spacePadding = padding.spaces(2), + ) { + override val builderRepresentation: String + get() = when (padding) { + Padding.ZERO -> "${DateTimeFormatBuilder.WithYearMonth::monthNumber.name}()" + else -> "${DateTimeFormatBuilder.WithYearMonth::monthNumber.name}(${padding.toKotlinCode()})" + } + + override fun equals(other: Any?): Boolean = other is MonthDirective && padding == other.padding + override fun hashCode(): Int = padding.hashCode() +} + +private class MonthNameDirective(private val names: MonthNames) : + NamedUnsignedIntFieldFormatDirective(YearMonthFields.month, names.names, "monthName") { + override val builderRepresentation: String + get() = + "${DateTimeFormatBuilder.WithYearMonth::monthName.name}(${names.toKotlinCode()})" + + override fun equals(other: Any?): Boolean = other is MonthNameDirective && names.names == other.names.names + override fun hashCode(): Int = names.names.hashCode() +} + +internal class YearMonthFormat(override val actualFormat: CachedFormatStructure) : + AbstractDateTimeFormat() { + override fun intermediateFromValue(value: YearMonth): IncompleteYearMonth = + IncompleteYearMonth().apply { populateFrom(value) } + + override fun valueFromIntermediate(intermediate: IncompleteYearMonth): YearMonth = intermediate.toYearMonth() + + override val emptyIntermediate get() = emptyIncompleteYearMonth + + companion object { + fun build(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat { + val builder = Builder(AppendableFormatStructure()) + builder.block() + return YearMonthFormat(builder.build()) + } + } + + internal class Builder(override val actualBuilder: AppendableFormatStructure) : + AbstractDateTimeFormatBuilder, AbstractWithYearMonthBuilder { + + override fun addFormatStructureForYearMonth(structure: FormatStructure) = + actualBuilder.add(structure) + + override fun createEmpty(): Builder = Builder(AppendableFormatStructure()) + } +} + +internal interface AbstractWithYearMonthBuilder : DateTimeFormatBuilder.WithYearMonth { + fun addFormatStructureForYearMonth(structure: FormatStructure) + + override fun year(padding: Padding) = + addFormatStructureForYearMonth(BasicFormatStructure(YearDirective(padding))) + + override fun yearTwoDigits(baseYear: Int) = + addFormatStructureForYearMonth(BasicFormatStructure(ReducedYearDirective(baseYear))) + + override fun monthNumber(padding: Padding) = + addFormatStructureForYearMonth(BasicFormatStructure(MonthDirective(padding))) + + override fun monthName(names: MonthNames) = + addFormatStructureForYearMonth(BasicFormatStructure(MonthNameDirective(names))) + + @Suppress("NO_ELSE_IN_WHEN") + override fun yearMonth(format: DateTimeFormat) = when (format) { + is YearMonthFormat -> addFormatStructureForYearMonth(format.actualFormat) + } +} + +private val emptyIncompleteYearMonth = IncompleteYearMonth() + +// these are constants so that the formats are not recreated every time they are used +internal val ISO_YEAR_MONTH by lazy { + YearMonthFormat.build { + year(); char('-'); monthNumber() + } +} diff --git a/core/common/src/internal/math.kt b/core/common/src/internal/math.kt index 6564698d..142cc278 100644 --- a/core/common/src/internal/math.kt +++ b/core/common/src/internal/math.kt @@ -5,6 +5,9 @@ package kotlinx.datetime.internal +import kotlin.random.Random +import kotlin.random.nextLong + internal fun Long.clampToInt(): Int = when { this > Int.MAX_VALUE -> Int.MAX_VALUE @@ -257,3 +260,28 @@ internal class DecimalFraction( throw UnsupportedOperationException("DecimalFraction is not supposed to be used as a hash key") } } + +// this implementation is incorrect in general +// (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).random()` throws an exception), +// but for the range of epoch days in LocalDate it's good enough +internal fun LongProgression.randomUnsafe(random: Random = Random): Long = + random.nextLong(0L..(last - first) / step) * step + first + +// incorrect in general; see `randomUnsafe` just above +internal fun LongProgression.randomUnsafeOrNull(random: Random = Random): Long? = + if (isEmpty()) null else randomUnsafe(random) + +// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)` +// returns `false` incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5), +// but for the range of epoch days in LocalDate it's good enough +internal fun LongProgression.containsUnsafe(value: Long): Boolean = + value in (if (step > 0) first..last else last..first) && (value - first) % step == 0L + +// this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0), +// but for the range of epoch days in LocalDate it's good enough +internal val LongProgression.sizeUnsafe: Int + get() = if (isEmpty()) 0 else try { + (safeAdd(last, -first) / step + 1).clampToInt() + } catch (e: ArithmeticException) { + Int.MAX_VALUE + } diff --git a/core/common/src/internal/util.kt b/core/common/src/internal/util.kt index 6cb52990..1c92f34a 100644 --- a/core/common/src/internal/util.kt +++ b/core/common/src/internal/util.kt @@ -38,3 +38,6 @@ internal fun removeLeadingZerosFromLongYearFormLocalDate(input: String) = internal fun removeLeadingZerosFromLongYearFormLocalDateTime(input: String) = removeLeadingZerosFromLongYearForm(input.toString(), 12) // 12 = "-01-02T23:59".length + +internal fun removeLeadingZerosFromLongYearFormYearMonth(input: String) = + removeLeadingZerosFromLongYearForm(input.toString(), 3) // 3 = "-01".length diff --git a/core/common/src/serializers/YearMonthSerializers.kt b/core/common/src/serializers/YearMonthSerializers.kt new file mode 100644 index 00000000..6adf74b1 --- /dev/null +++ b/core/common/src/serializers/YearMonthSerializers.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.serializers + +import kotlinx.datetime.YearMonth +import kotlinx.datetime.number +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +/** + * A serializer for [YearMonth] that uses the ISO 8601 representation. + * + * JSON example: `"2020-01"` + * + * @see YearMonth.parse + * @see YearMonth.toString + */ +public object YearMonthIso8601Serializer: KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.YearMonth", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): YearMonth = + YearMonth.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: YearMonth) { + encoder.encodeString(value.toString()) + } + +} + +/** + * A serializer for [YearMonth] that represents a value as its components. + * + * JSON example: `{"year":2020,"month":12}` + */ +public object YearMonthComponentSerializer: KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("kotlinx.datetime.LocalDate") { + element("year") + element("month") + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): YearMonth = + decoder.decodeStructure(descriptor) { + var year: Int? = null + var month: Short? = null + loop@while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> year = decodeIntElement(descriptor, 0) + 1 -> month = decodeShortElement(descriptor, 1) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throwUnknownIndexException(index) + } + } + if (year == null) throw MissingFieldException(missingField = "year", serialName = descriptor.serialName) + if (month == null) throw MissingFieldException(missingField = "month", serialName = descriptor.serialName) + YearMonth(year, month.toInt()) + } + + override fun serialize(encoder: Encoder, value: YearMonth) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.year) + encodeShortElement(descriptor, 1, value.month.number.toShort()) + } + } + +} diff --git a/core/common/test/ReadmeTest.kt b/core/common/test/ReadmeTest.kt index 2c84fa63..00bbf61b 100644 --- a/core/common/test/ReadmeTest.kt +++ b/core/common/test/ReadmeTest.kt @@ -117,10 +117,10 @@ class ReadmeTest { @Test fun testParsingAndFormattingPartialCompoundOrOutOfBoundsData() { - val yearMonth = DateTimeComponents.Format { year(); char('-'); monthNumber() } - .parse("2024-01") - assertEquals(2024, yearMonth.year) - assertEquals(1, yearMonth.monthNumber) + val monthDay = DateTimeComponents.Format { monthNumber(); char('/'); dayOfMonth() } + .parse("12/25") + assertEquals(25, monthDay.dayOfMonth) + assertEquals(12, monthDay.monthNumber) val dateTimeOffset = DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET .parse("2023-01-07T23:16:15.53+02:00") diff --git a/core/common/test/YearMonthRangeTest.kt b/core/common/test/YearMonthRangeTest.kt new file mode 100644 index 00000000..99ba41ab --- /dev/null +++ b/core/common/test/YearMonthRangeTest.kt @@ -0,0 +1,313 @@ +/* + * Copyright 2019-2022 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test + +import kotlinx.datetime.* +import kotlinx.datetime.internal.clampToInt +import kotlin.random.Random +import kotlin.random.nextLong +import kotlin.test.* + +class YearMonthRangeTest { + val Dec_1999 = YearMonth(1999, 12) + val Jan_2000 = YearMonth(2000, 1) + val Feb_2000 = YearMonth(2000, 2) + val May_2000 = YearMonth(2000, 5) + val Jun_2000 = YearMonth(2000, 6) + val Dec_2000 = YearMonth(2000, 12) + + @Test + fun emptyRange() { + assertTrue { (May_2000..Jan_2000).isEmpty() } + assertTrue { (Jan_2000 downTo May_2000).isEmpty() } + assertTrue { YearMonthRange.EMPTY.isEmpty() } + } + + @Test + fun forwardRange() { + assertContentEquals( + (1..5).map { YearMonth(2000, it) }, + Jan_2000..May_2000 + ) + assertContentEquals( + (1..<5).map { YearMonth(2000, it) }, + Jan_2000.. { + (Feb_2000..Jan_2000).random() + } + + assertNull((Feb_2000..Jan_2000).randomOrNull()) + + val seed = 123456 + val expectedRand = Random(seed) + val actualRand = Random(seed) + + repeat(20) { + assertEquals( + expectedRand.nextLong(0L..11L).let { Jan_2000.plus(it, DateTimeUnit.MONTH) }, + (Jan_2000..Dec_2000).random(actualRand) + ) + } + + repeat(20) { + assertEquals( + expectedRand.nextLong(0L..11L).let { Dec_2000.minus(it, DateTimeUnit.MONTH) }, + (Dec_2000 downTo Jan_2000).random(actualRand) + ) + } + + listOf(1L, 2L, 5L, 30L).forEach { step -> + repeat(20) { + val range = (0L..11L step step) + assertEquals( + expectedRand.nextLong(0L..range.last / step).let { Jan_2000.plus(it * step, DateTimeUnit.MONTH) }, + (Jan_2000..Dec_2000).step(step, DateTimeUnit.MONTH).random(actualRand) + ) + } + + repeat(20) { + val range = (0L..11L step step) + assertEquals( + expectedRand.nextLong(0..range.last / step).let { Dec_2000.minus(it * step, DateTimeUnit.MONTH) }, + (Dec_2000 downTo Jan_2000).step(step, DateTimeUnit.MONTH).random(actualRand) + ) + } + } + repeat(20) { + (Jan_2000..Dec_2000).step(5, DateTimeUnit.MONTH).let { assertContains(it, it.random()) } + } + } + + @Test + fun first() { + assertEquals((Jan_2000..Jan_2000).first(), Jan_2000) + assertEquals((Jan_2000 downTo Jan_2000).first(), Jan_2000) + assertEquals((Jan_2000..May_2000).first(), Jan_2000) + assertEquals((May_2000 downTo Jan_2000).first(), May_2000) + assertFailsWith { (Feb_2000..Jan_2000).first() } + assertFailsWith { (Jan_2000 downTo Feb_2000).first() } + } + + @Test + fun last() { + assertEquals((Jan_2000..Jan_2000).last(), Jan_2000) + assertEquals((Jan_2000 downTo Jan_2000).last(), Jan_2000) + assertEquals((Jan_2000..May_2000).last(), May_2000) + assertEquals((May_2000 downTo Jan_2000).last(), Jan_2000) + assertEquals((Jan_2000..Jun_2000).step(2, DateTimeUnit.MONTH).last(), May_2000) + assertEquals((Jun_2000 downTo Jan_2000).step(2, DateTimeUnit.MONTH).last(), Feb_2000) + assertFailsWith { (Feb_2000..Jan_2000).last() } + assertFailsWith { (Jan_2000 downTo Feb_2000).last() } + } + + @Test + fun firstOrNull() { + assertEquals((Jan_2000..Jan_2000).firstOrNull(), Jan_2000) + assertEquals((Jan_2000 downTo Jan_2000).firstOrNull(), Jan_2000) + assertEquals((Jan_2000..May_2000).firstOrNull(), Jan_2000) + assertEquals((May_2000 downTo Jan_2000).firstOrNull(), May_2000) + assertNull( (Feb_2000..Jan_2000).firstOrNull() ) + assertNull( (Jan_2000 downTo Feb_2000).firstOrNull() ) + } + + @Test + fun lastOrNull() { + assertEquals((Jan_2000..Jan_2000).lastOrNull(), Jan_2000) + assertEquals((Jan_2000 downTo Jan_2000).lastOrNull(), Jan_2000) + assertEquals((Jan_2000..May_2000).lastOrNull(), May_2000) + assertEquals((May_2000 downTo Jan_2000).lastOrNull(), Jan_2000) + assertNull( (Feb_2000..Jan_2000).lastOrNull() ) + assertNull( (Jan_2000 downTo Feb_2000).lastOrNull() ) + } + + @Test + fun reversed() { + assertEquals( + May_2000 downTo Jan_2000, + (Jan_2000..May_2000).reversed() + ) + assertEquals( + Jan_2000..May_2000, + (May_2000 downTo Jan_2000).reversed() + ) + assertEquals( + Jan_2000 downTo Jan_2000, + (Jan_2000..Jan_2000).reversed() + ) + } + + @Test + fun contains() { + assertTrue { Jan_2000 in Jan_2000..Jan_2000 } + assertTrue { Feb_2000 in Jan_2000..May_2000 } + assertTrue { Jan_2000 in Jan_2000 downTo Jan_2000 } + assertTrue { Feb_2000 in May_2000 downTo Jan_2000 } + + assertFalse { Jan_2000 in Feb_2000..Feb_2000 } + assertFalse { May_2000 in Feb_2000..Feb_2000 } + assertFalse { Jan_2000 in Feb_2000..May_2000 } + assertFalse { Dec_2000 in Feb_2000..Feb_2000 } + assertFalse { Jan_2000 in Feb_2000 downTo Feb_2000 } + assertFalse { May_2000 in Feb_2000 downTo Feb_2000 } + assertFalse { Jan_2000 in Feb_2000 downTo May_2000 } + assertFalse { Dec_2000 in May_2000 downTo Feb_2000 } + + assertFalse { (Jan_2000..May_2000).contains(Any()) } + + assertTrue { (Jan_2000..Jan_2000).containsAll(listOf(Jan_2000)) } + assertTrue { (Jan_2000..May_2000).containsAll(listOf(Jan_2000, Feb_2000, May_2000)) } + + assertFalse { (Jan_2000..Jan_2000).containsAll(listOf(Jan_2000, Feb_2000)) } + assertFalse { (Jan_2000..May_2000).containsAll(listOf(Jan_2000, Feb_2000, May_2000, Dec_2000)) } + + assertFalse { ((Jan_2000..May_2000) as Collection<*>).containsAll(listOf(Any())) } + + } + + @Test + fun getSize() { + assertEquals(1, (Jan_2000..Jan_2000).size) + assertEquals(1, (Jan_2000 downTo Jan_2000).size) + assertEquals(2, (Jan_2000..Feb_2000).size) + assertEquals(2, (Feb_2000 downTo Jan_2000).size) + assertEquals(5, (Jan_2000..May_2000).size) + assertEquals(5, (May_2000 downTo Jan_2000).size) + assertEquals(4, (Jan_2000.. { YearMonth(2016, Month.FEBRUARY).onDay(30) } + assertFailsWith { YearMonth(2016, Month.FEBRUARY).onDay(0) } + assertFailsWith { YearMonth(2016, Month.FEBRUARY).onDay(-1) } + assertFailsWith { YearMonth(2015, Month.FEBRUARY).onDay(29) } + } + + @Test + fun addComponents() { + val start = YearMonth(2016, 2) + checkComponents(start.plus(1, DateTimeUnit.MONTH), 2016, 3) + checkComponents(start.plus(1, DateTimeUnit.YEAR), 2017, 2) + assertEquals(start, start.plus(1, DateTimeUnit.MONTH).minus(1, DateTimeUnit.MONTH)) + assertEquals(start, start.plus(3, DateTimeUnit.MONTH).minus(3, DateTimeUnit.MONTH)) + } + + @Test + fun unitsUntil() { + val data = listOf, Long, Int>>( + Triple(Pair("2012-06", "2012-06"), 0, 0), + Triple(Pair("2012-06", "2012-07"), 1, 0), + Triple(Pair("2012-06", "2013-07"), 13, 1), + Triple(Pair("-0001-01", "0001-01"), 24, 2), + Triple(Pair("-10000-01", "+10000-01"), 240000, 20000), + ) + for ((values, months, years) in data) { + val (v1, v2) = values + val start = YearMonth.parse(v1) + val end = YearMonth.parse(v2) + assertEquals(months, start.until(end, DateTimeUnit.MONTH)) + assertEquals(-months, end.until(start, DateTimeUnit.MONTH)) + assertEquals(years.toLong(), start.until(end, DateTimeUnit.YEAR)) + assertEquals(-years.toLong(), end.until(start, DateTimeUnit.YEAR)) + if (months <= Int.MAX_VALUE) { + assertEquals(months.toInt(), start.monthsUntil(end)) + assertEquals(-months.toInt(), end.monthsUntil(start)) + } + assertEquals(years, start.yearsUntil(end)) + assertEquals(-years, end.yearsUntil(start)) + } + + } + + @Test + fun unitMultiplesUntil() { + val start = YearMonth(2000, 1) + val end = YearMonth(2030, 3) + val yearsBetween = start.until(end, DateTimeUnit.MONTH * 12) + assertEquals(30, yearsBetween) + assertEquals(15, start.until(end, DateTimeUnit.MONTH * 24)) + assertEquals(10, start.until(end, DateTimeUnit.MONTH * 36)) + assertEquals(5, start.until(end, DateTimeUnit.MONTH * 72)) + assertEquals(2, start.until(end, DateTimeUnit.MONTH * 180)) + assertEquals(1, start.until(end, DateTimeUnit.MONTH * 360)) + val monthsBetween = start.until(end, DateTimeUnit.MONTH) + assertEquals(yearsBetween * 12 + 2, monthsBetween) // 362 + assertEquals(181, start.until(end, DateTimeUnit.MONTH * 2)) + assertEquals(120, start.until(end, DateTimeUnit.MONTH * 3)) + assertEquals(90, start.until(end, DateTimeUnit.MONTH * 4)) + assertEquals(72, start.until(end, DateTimeUnit.MONTH * 5)) + assertEquals(60, start.until(end, DateTimeUnit.MONTH * 6)) + } + + @Test + fun constructInvalidYearMonth() { + assertFailsWith { YearMonth(Int.MIN_VALUE, 1) } + assertFailsWith { YearMonth(2007, 0) } + assertFailsWith { YearMonth(2007, 13) } + } + + @Test + fun unitArithmeticOutOfRange() { + maxYearMonth.plus(-1, DateTimeUnit.MONTH) + minYearMonth.plus(1, DateTimeUnit.MONTH) + // Arithmetic overflow + assertArithmeticFails { maxYearMonth.plus(Long.MAX_VALUE, DateTimeUnit.YEAR) } + assertArithmeticFails { maxYearMonth.plus(Long.MAX_VALUE - 2, DateTimeUnit.YEAR) } + assertArithmeticFails { minYearMonth.plus(Long.MIN_VALUE, DateTimeUnit.YEAR) } + assertArithmeticFails { minYearMonth.plus(Long.MIN_VALUE + 2, DateTimeUnit.YEAR) } + assertArithmeticFails { minYearMonth.plus(Long.MAX_VALUE, DateTimeUnit.MONTH) } + assertArithmeticFails { maxYearMonth.plus(Long.MIN_VALUE, DateTimeUnit.MONTH) } + // Exceeding the boundaries of LocalDate + assertArithmeticFails { maxYearMonth.plus(1, DateTimeUnit.YEAR) } + assertArithmeticFails { minYearMonth.plus(-1, DateTimeUnit.YEAR) } + } + + @Test + fun monthsUntilClamping() { + val preciseDifference = minYearMonth.until(maxYearMonth, DateTimeUnit.MONTH) + // TODO: remove the condition after https://github.com/Kotlin/kotlinx-datetime/pull/453 + if (preciseDifference > Int.MAX_VALUE) { + assertEquals(Int.MAX_VALUE, minYearMonth.monthsUntil(maxYearMonth)) + assertEquals(Int.MIN_VALUE, maxYearMonth.monthsUntil(minYearMonth)) + } + } + + @Test + fun daysRange() { + @OptIn(ExperimentalStdlibApi::class) + fun test(year: Int, month: Int) { + val yearMonth = YearMonth(year, month) + assertEquals(LocalDate(year, month, 1), yearMonth.firstDay) + assertEquals(LocalDate(year, month, yearMonth.numberOfDays), yearMonth.lastDay) + assertEquals(yearMonth.plusMonth().firstDay, yearMonth.lastDay.plus(1, DateTimeUnit.DAY)) + assertEquals(LocalDateRange(yearMonth.firstDay, yearMonth.lastDay), yearMonth.days) + assertContains(yearMonth.days as OpenEndRange, yearMonth.firstDay) + assertContains(yearMonth.days as OpenEndRange, yearMonth.lastDay) + assertFalse(yearMonth.days.contains(yearMonth.firstDay.minus(1, DateTimeUnit.DAY))) + assertFalse(yearMonth.days.contains(yearMonth.lastDay.plus(1, DateTimeUnit.DAY))) + } + for (month in 1..12) { + for (year in 2000..2005) { + test(year, month) + } + for (year in -2005..-2000) { + test(year, month) + } + } + } + + private val minYearMonth = LocalDate.MIN.yearMonth + private val maxYearMonth = LocalDate.MAX.yearMonth +} diff --git a/core/common/test/format/LocalDateFormatTest.kt b/core/common/test/format/LocalDateFormatTest.kt index 3be5b765..3666cb50 100644 --- a/core/common/test/format/LocalDateFormatTest.kt +++ b/core/common/test/format/LocalDateFormatTest.kt @@ -242,15 +242,6 @@ class LocalDateFormatTest { assertEquals("2020 Jan 05", format.format(LocalDate(2020, 1, 5))) } - @Test - fun testEmptyMonthNames() { - val names = MonthNames.ENGLISH_FULL.names - for (i in 0 until 12) { - val newNames = (0 until 12).map { if (it == i) "" else names[it] } - assertFailsWith { MonthNames(newNames) } - } - } - @Test fun testEmptyDayOfWeekNames() { val names = DayOfWeekNames.ENGLISH_FULL.names @@ -260,13 +251,6 @@ class LocalDateFormatTest { } } - @Test - fun testIdenticalMonthNames() { - assertFailsWith { - MonthNames("Jan", "Jan", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") - } - } - @Test fun testIdenticalDayOfWeekNames() { assertFailsWith { diff --git a/core/common/test/format/YearMonthFormatTest.kt b/core/common/test/format/YearMonthFormatTest.kt new file mode 100644 index 00000000..dc8b1df2 --- /dev/null +++ b/core/common/test/format/YearMonthFormatTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.format + +import kotlinx.datetime.* +import kotlinx.datetime.format.* +import kotlin.test.* + +class YearMonthFormatTest { + @Test + fun testIso() { + val yearMonths = buildMap>> { + put(YearMonth(2008, 7), ("2008-07" to setOf())) + put(YearMonth(2007, 12), ("2007-12" to setOf())) + put(YearMonth(999, 11), ("0999-11" to setOf())) + put(YearMonth(-1, 1), ("-0001-01" to setOf())) + put(YearMonth(9999, 10), ("9999-10" to setOf())) + put(YearMonth(-9999, 9), ("-9999-09" to setOf())) + put(YearMonth(10000, 8), ("+10000-08" to setOf())) + put(YearMonth(-10000, 7), ("-10000-07" to setOf())) + put(YearMonth(123456, 6), ("+123456-06" to setOf())) + put(YearMonth(-123456, 5), ("-123456-05" to setOf())) + } + test(yearMonths, YearMonth.Formats.ISO) + test(yearMonths, YearMonth.Format { byUnicodePattern("yyyy-MM") }) + } + + @Test + fun testIsoWithoutSeparators() { + val yearMonths = buildMap>> { + put(YearMonth(2008, 7), ("200807" to setOf())) + put(YearMonth(2007, 12), ("200712" to setOf())) + put(YearMonth(999, 11), ("099911" to setOf())) + put(YearMonth(-1, 1), ("-000101" to setOf())) + put(YearMonth(9999, 10), ("999910" to setOf())) + put(YearMonth(-9999, 9), ("-999909" to setOf())) + put(YearMonth(10000, 8), ("+1000008" to setOf())) + put(YearMonth(-10000, 7), ("-1000007" to setOf())) + put(YearMonth(123456, 6), ("+12345606" to setOf())) + put(YearMonth(-123456, 5), ("-12345605" to setOf())) + } + test(yearMonths, YearMonth.Format { year(); monthNumber() }) + test(yearMonths, YearMonth.Format { byUnicodePattern("yyyyMM") }) + } + + @Test + fun testErrorHandling() { + YearMonth.Formats.ISO.apply { + assertEquals(YearMonth(2023, 2), parse("2023-02")) + assertCanNotParse("2023-XX") + assertCanNotParse("2023-40") + } + } + + @Test + fun testEmptyMonthNames() { + val names = MonthNames.ENGLISH_FULL.names + for (i in 0 until 12) { + val newNames = (0 until 12).map { if (it == i) "" else names[it] } + assertFailsWith { MonthNames(newNames) } + } + } + + @Test + fun testIdenticalMonthNames() { + assertFailsWith { + MonthNames("Jan", "Jan", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + } + } + + private fun test(strings: Map>>, format: DateTimeFormat) { + for ((yearMonth, stringsForYearMonth) in strings) { + val (canonicalString, otherStrings) = stringsForYearMonth + assertEquals(canonicalString, format.format(yearMonth), "formatting $yearMonth with $format") + assertEquals(yearMonth, format.parse(canonicalString), "parsing '$canonicalString' with $format") + for (otherString in otherStrings) { + assertEquals(yearMonth, format.parse(otherString), "parsing '$otherString' with $format") + } + } + } +} diff --git a/core/common/test/samples/YearMonthRangeSamples.kt b/core/common/test/samples/YearMonthRangeSamples.kt new file mode 100644 index 00000000..af5cd055 --- /dev/null +++ b/core/common/test/samples/YearMonthRangeSamples.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test.samples + +import kotlinx.datetime.* +import kotlin.random.Random +import kotlin.test.Test + +class YearMonthRangeSamples { + + @Test + fun simpleRangeCreation() { + // Creating YearMonthRange from YearMonth + check((YearMonth(2000, 1)..YearMonth(2000, 5)).toList() == listOf( + YearMonth(2000, 1), + YearMonth(2000, 2), + YearMonth(2000, 3), + YearMonth(2000, 4), + YearMonth(2000, 5) + )) + check( + (YearMonth(2000, 1).. { - val packed1 = prolepticMonth * 32 + dayOfMonth - val packed2 = other.prolepticMonth * 32 + other.dayOfMonth + val packed1 = prolepticMonth * 32 + day + val packed2 = other.prolepticMonth * 32 + other.day val result = (packed2 - packed1) / 32 result / unit.months } diff --git a/core/commonKotlin/src/YearMonth.kt b/core/commonKotlin/src/YearMonth.kt new file mode 100644 index 00000000..1f9c1a95 --- /dev/null +++ b/core/commonKotlin/src/YearMonth.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime + +import kotlinx.datetime.format.* +import kotlinx.datetime.internal.* +import kotlinx.datetime.serializers.YearMonthIso8601Serializer +import kotlinx.serialization.Serializable + +@Serializable(with = YearMonthIso8601Serializer::class) +public actual class YearMonth +public actual constructor(public actual val year: Int, month: Int) : Comparable { + internal actual val monthNumber: Int = month + + init { + require(month in 1..12) { "Month must be in 1..12, but was $month" } + require(year in LocalDate.MIN.year..LocalDate.MAX.year) { + "Year $year is out of range: ${LocalDate.MIN.year}..${LocalDate.MAX.year}" + } + } + + public actual val month: Month get() = Month(monthNumber) + + public actual val firstDay: LocalDate get() = onDay(1) + + public actual val lastDay: LocalDate get() = onDay(numberOfDays) + + public actual val numberOfDays: Int get() = monthNumber.monthLength(isLeapYear(year)) + + public actual val days: LocalDateRange get() = firstDay..lastDay // no ranges yet + + public actual constructor(year: Int, month: Month): this(year, month.number) + + public actual companion object { + public actual fun parse(input: CharSequence, format: DateTimeFormat): YearMonth = + format.parse(input) + + @Suppress("FunctionName") + public actual fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat = + YearMonthFormat.build(block) + } + + public actual object Formats { + public actual val ISO: DateTimeFormat get() = ISO_YEAR_MONTH + } + + public actual operator fun rangeTo(that: YearMonth): YearMonthRange = YearMonthRange.fromRangeTo(this, that) + + public actual operator fun rangeUntil(that: YearMonth): YearMonthRange = YearMonthRange.fromRangeUntil(this, that) + + actual override fun compareTo(other: YearMonth): Int = + compareValuesBy(this, other, YearMonth::year, YearMonth::month) + + actual override fun toString(): String = Formats.ISO.format(this) + + override fun equals(other: Any?): Boolean = other is YearMonth && year == other.year && month == other.month + + override fun hashCode(): Int = year * 31 + month.hashCode() +} diff --git a/core/darwin/src/Converters.kt b/core/darwin/src/Converters.kt index cf785dea..4d2e946f 100644 --- a/core/darwin/src/Converters.kt +++ b/core/darwin/src/Converters.kt @@ -80,7 +80,7 @@ public fun LocalDate.toNSDateComponents(): NSDateComponents { } /** - * Converts the given [LocalDate] to [NSDateComponents]. + * Converts the given [LocalDateTime] to [NSDateComponents]. * * Of all the fields, only the bare minimum required for uniquely identifying the date and time are set. */ @@ -92,3 +92,15 @@ public fun LocalDateTime.toNSDateComponents(): NSDateComponents { components.nanosecond = nanosecond.convert() return components } + +/** + * Converts the given [YearMonth] to [NSDateComponents]. + * + * Of all the fields, only the bare minimum required for uniquely identifying the year and month are set. + */ +public fun YearMonth.toNSDateComponents(): NSDateComponents { + val components = NSDateComponents() + components.year = year.convert() + components.month = month.number.convert() + return components +} diff --git a/core/darwin/test/ConvertersTest.kt b/core/darwin/test/ConvertersTest.kt index 9efe0775..c4ce58d2 100644 --- a/core/darwin/test/ConvertersTest.kt +++ b/core/darwin/test/ConvertersTest.kt @@ -103,6 +103,18 @@ class ConvertersTest { assertEqualUpToHalfMicrosecond(dateTime.toInstant(TimeZone.UTC), nsDate.toKotlinInstant()) } + @Test + fun yearMonthToNSDateComponentsTest() { + val yearMonth = YearMonth(2019, 2) + val components = yearMonth.toNSDateComponents().apply { timeZone = utc } + val nsDate = isoCalendar.dateFromComponents(components)!! + val formatter = NSDateFormatter().apply { + timeZone = utc + dateFormat = "yyyy-MM" + } + assertEquals("2019-02", formatter.stringFromDate(nsDate)) + } + @OptIn(ExperimentalForeignApi::class, UnsafeNumber::class) private fun zoneOffsetCheck(timeZone: FixedOffsetTimeZone, hours: Int, minutes: Int) { val nsTimeZone = timeZone.toNSTimeZone() diff --git a/core/jvm/src/Converters.kt b/core/jvm/src/Converters.kt index 688b0f84..e1f41dd8 100644 --- a/core/jvm/src/Converters.kt +++ b/core/jvm/src/Converters.kt @@ -112,3 +112,13 @@ public fun DayOfWeek.toJavaDayOfWeek(): java.time.DayOfWeek = java.time.DayOfWee * Converts this [java.time.DayOfWeek][java.time.DayOfWeek] value to a [kotlinx.datetime.DayOfWeek][DayOfWeek] value. */ public fun java.time.DayOfWeek.toKotlinDayOfWeek(): DayOfWeek = DayOfWeek.entries[this.value - 1] + +/* + * Converts this [kotlinx.datetime.YearMonth][YearMonth] value to a [java.time.YearMonth][java.time.YearMonth] value. + */ +public fun YearMonth.toJavaYearMonth(): java.time.YearMonth = this.value + +/** + * Converts this [java.time.YearMonth][java.time.YearMonth] value to a [kotlinx.datetime.YearMonth][YearMonth] value. + */ +public fun java.time.YearMonth.toKotlinYearMonth(): YearMonth = YearMonth(this) diff --git a/core/jvm/src/YearMonthJvm.kt b/core/jvm/src/YearMonthJvm.kt new file mode 100644 index 00000000..7309dc26 --- /dev/null +++ b/core/jvm/src/YearMonthJvm.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime + +import kotlinx.datetime.format.* +import kotlinx.datetime.internal.* +import kotlinx.datetime.serializers.YearMonthIso8601Serializer +import kotlinx.serialization.Serializable +import java.time.DateTimeException +import java.time.format.DateTimeFormatterBuilder +import java.time.format.DateTimeParseException +import java.time.format.SignStyle +import java.time.YearMonth as jtYearMonth + +@Serializable(with = YearMonthIso8601Serializer::class) +public actual class YearMonth internal constructor( + internal val value: jtYearMonth +) : Comparable, java.io.Serializable { + public actual val year: Int get() = value.year + internal actual val monthNumber: Int get() = value.monthValue + + public actual val month: Month get() = value.month.toKotlinMonth() + public actual val firstDay: LocalDate get() = LocalDate(value.atDay(1)) + public actual val lastDay: LocalDate get() = LocalDate(value.atEndOfMonth()) + public actual val numberOfDays: Int get() = value.lengthOfMonth() + public actual val days: LocalDateRange get() = firstDay..lastDay // no ranges yet + + public actual constructor(year: Int, month: Int): this(try { + jtYearMonth.of(year, month) + } catch (e: DateTimeException) { + throw IllegalArgumentException(e) + }) + public actual constructor(year: Int, month: Month): this(try { + jtYearMonth.of(year, month.toJavaMonth()) + } catch (e: DateTimeException) { + throw IllegalArgumentException(e) + }) + + public actual companion object { + public actual fun parse(input: CharSequence, format: DateTimeFormat): YearMonth = + if (format === Formats.ISO) { + try { + val sanitizedInput = removeLeadingZerosFromLongYearFormYearMonth(input.toString()) + jtYearMonth.parse(sanitizedInput).let(::YearMonth) + } catch (e: DateTimeParseException) { + throw DateTimeFormatException(e) + } + } else { + format.parse(input) + } + + @Suppress("FunctionName") + public actual fun Format(block: DateTimeFormatBuilder.WithYearMonth.() -> Unit): DateTimeFormat = + YearMonthFormat.build(block) + } + + public actual object Formats { + public actual val ISO: DateTimeFormat get() = ISO_YEAR_MONTH + } + + public actual operator fun rangeTo(that: YearMonth): YearMonthRange = YearMonthRange.fromRangeTo(this, that) + + public actual operator fun rangeUntil(that: YearMonth): YearMonthRange = YearMonthRange.fromRangeUntil(this, that) + + actual override fun compareTo(other: YearMonth): Int = value.compareTo(other.value) + + actual override fun toString(): String = isoFormat.format(value) + + override fun equals(other: Any?): Boolean = this === other || other is YearMonth && value == other.value + + override fun hashCode(): Int = value.hashCode() + + private fun writeReplace(): Any = Ser(Ser.YEAR_MONTH_TAG, this) +} + +internal fun YearMonth.toEpochMonths(): Long = (year - 1970L) * 12 + monthNumber - 1 + +internal fun YearMonth.Companion.fromEpochMonths(months: Long): YearMonth { + val year = months.floorDiv(12) + 1970 + val month = months.mod(12) + 1 + return YearMonth(year.toInt(), month) +} + +private val isoFormat by lazy { + DateTimeFormatterBuilder().parseCaseInsensitive() + .appendValue(java.time.temporal.ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(java.time.temporal.ChronoField.MONTH_OF_YEAR, 2) + .toFormatter() +} diff --git a/core/jvm/src/internal/Ser.kt b/core/jvm/src/internal/Ser.kt index 61309ee6..9c831292 100644 --- a/core/jvm/src/internal/Ser.kt +++ b/core/jvm/src/internal/Ser.kt @@ -33,6 +33,10 @@ internal class Ser(private var typeTag: Int, private var value: Any?) : External value as UtcOffset out.writeInt(value.totalSeconds) } + YEAR_MONTH_TAG -> { + value as YearMonth + out.writeLong(value.toEpochMonths()) + } else -> throw IllegalStateException("Unknown type tag: $typeTag for value: $value") } } @@ -51,6 +55,8 @@ internal class Ser(private var typeTag: Int, private var value: Any?) : External ) UTC_OFFSET_TAG -> UtcOffset(seconds = `in`.readInt()) + YEAR_MONTH_TAG -> + YearMonth.fromEpochMonths(`in`.readLong()) else -> throw IOException("Unknown type tag: $typeTag") } } @@ -63,5 +69,6 @@ internal class Ser(private var typeTag: Int, private var value: Any?) : External const val TIME_TAG = 3 const val DATE_TIME_TAG = 4 const val UTC_OFFSET_TAG = 10 + const val YEAR_MONTH_TAG = 11 } } diff --git a/core/jvm/test/ConvertersTest.kt b/core/jvm/test/ConvertersTest.kt index 06b7e223..9e83f4e1 100644 --- a/core/jvm/test/ConvertersTest.kt +++ b/core/jvm/test/ConvertersTest.kt @@ -212,4 +212,22 @@ class ConvertersTest { DayOfWeek.entries.forEach(::test) assertEquals(DayOfWeek.MONDAY, java.time.DayOfWeek.MONDAY.toKotlinDayOfWeek()) } + + @Test + fun yearMonth() { + fun test(year: Int, month: Int) { + val ktYearMonth = YearMonth(year, month) + val jtYearMonth = java.time.YearMonth.of(year, month) + + assertEquals(ktYearMonth, jtYearMonth.toKotlinYearMonth()) + assertEquals(jtYearMonth, ktYearMonth.toJavaYearMonth()) + + assertEquals(ktYearMonth, jtYearMonth.toString().let(YearMonth::parse)) + assertEquals(jtYearMonth, ktYearMonth.toString().let(java.time.YearMonth::parse)) + } + + repeat(STRESS_TEST_ITERATIONS) { + test(Random.nextInt(-10000, 10000), (1..12).random()) + } + } } diff --git a/core/jvm/test/JvmSerializationTest.kt b/core/jvm/test/JvmSerializationTest.kt index 4a72e6d3..d5aae44b 100644 --- a/core/jvm/test/JvmSerializationTest.kt +++ b/core/jvm/test/JvmSerializationTest.kt @@ -42,6 +42,16 @@ class JvmSerializationTest { expectedDeserialization(UtcOffset.parse("-04:15:30"), "050affffc41e") } + @Test + fun serializeYearMonth() { + roundTripSerialization(YearMonth(2022, 1)) + roundTripSerialization(YearMonth(1969, 7)) + roundTripSerialization(YearMonth(-999999999, 1)) + roundTripSerialization(YearMonth(999999999, 12)) + expectedDeserialization(YearMonth(2024, 8), "090b000000000000028f") + expectedDeserialization(YearMonth(1970, 1), "090b0000000000000000") + } + @Test fun serializeTimeZone() { assertFailsWith { diff --git a/core/jvm/test/UnicodeFormatTest.kt b/core/jvm/test/UnicodeFormatTest.kt index 67982bda..865d0b25 100644 --- a/core/jvm/test/UnicodeFormatTest.kt +++ b/core/jvm/test/UnicodeFormatTest.kt @@ -85,12 +85,12 @@ class UnicodeFormatTest { val directives = directivesInFormat(unicodeFormat) val dates = when { directives.any { - it is UnicodeFormat.Directive.DateBased.Year && it.formatLength == 2 - || it is UnicodeFormat.Directive.DateBased.YearOfEra && it.formatLength == 2 + it is UnicodeFormat.Directive.YearMonthBased.Year && it.formatLength == 2 + || it is UnicodeFormat.Directive.YearMonthBased.YearOfEra && it.formatLength == 2 } -> interestingDates21stCentury - directives.any { it is UnicodeFormat.Directive.DateBased.YearOfEra } -> interestingDatesPositive - directives.any { it is UnicodeFormat.Directive.DateBased } -> interestingDates + directives.any { it is UnicodeFormat.Directive.YearMonthBased.YearOfEra } -> interestingDatesPositive + directives.any { it is UnicodeFormat.Directive.YearMonthBased } -> interestingDates else -> listOf(LocalDate(1970, 1, 1)) } val times = when { diff --git a/serialization/common/test/YearMonthSerializationTest.kt b/serialization/common/test/YearMonthSerializationTest.kt new file mode 100644 index 00000000..31a6eca4 --- /dev/null +++ b/serialization/common/test/YearMonthSerializationTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlin.test.* + +class YearMonthSerializationTest { + private fun iso8601Serialization(serializer: KSerializer) { + for ((yearMonth, json) in listOf( + Pair(YearMonth(2020, 12), "\"2020-12\""), + Pair(YearMonth(-2020, 1), "\"-2020-01\""), + Pair(YearMonth(2019, 10), "\"2019-10\""), + )) { + assertEquals(json, Json.encodeToString(serializer, yearMonth)) + assertEquals(yearMonth, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((yearMonth, json) in listOf( + Pair(YearMonth(2020, 12), "{\"year\":2020,\"month\":12}"), + Pair(YearMonth(-2020, 1), "{\"year\":-2020,\"month\":1}"), + Pair(YearMonth(2019, 10), "{\"year\":2019,\"month\":10}"), + )) { + assertEquals(json, Json.encodeToString(serializer, yearMonth)) + assertEquals(yearMonth, Json.decodeFromString(serializer, json)) + } + // all components must be present + assertFailsWith { + Json.decodeFromString(serializer, "{}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":3}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"month\":3}") + } + // invalid values must fail to construct + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":1000000000000,\"month\":3}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":2020,\"month\":30}") + } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(YearMonthIso8601Serializer) + } + + @Test + fun testComponentSerialization() { + componentSerialization(YearMonthComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO 8601 + iso8601Serialization(Json.serializersModule.serializer()) + } + +}