From 0fa33e2632abffa421359dca9edccdcf8f6c7e45 Mon Sep 17 00:00:00 2001 From: adrcotfas Date: Fri, 7 Nov 2025 23:03:17 +0200 Subject: [PATCH] Add support for platform specific localized display names for DayOfWeek and Month --- core/api/kotlinx-datetime.api | 37 +++++++ core/api/kotlinx-datetime.klib.api | 27 +++++ core/common/src/DayOfWeekExt.kt | 34 ++++++ core/common/src/Locale.kt | 25 +++++ core/common/src/MonthExt.kt | 33 ++++++ core/common/src/TextStyle.kt | 53 +++++++++ core/common/src/format/DayOfWeekNamesExt.kt | 13 +++ core/common/src/format/MonthNamesExt.kt | 16 +++ core/common/test/LocalizationTest.kt | 102 ++++++++++++++++++ core/common/test/samples/DayOfWeekSamples.kt | 18 ++++ core/common/test/samples/LocaleSamples.kt | 23 ++++ core/common/test/samples/MonthSamples.kt | 18 ++++ .../samples/format/LocalDateFormatSamples.kt | 12 +++ .../samples/format/YearMonthFormatSamples.kt | 15 +++ core/commonKotlin/src/DayOfWeekExt.kt | 24 +++++ core/commonKotlin/src/Locale.kt | 19 ++++ core/commonKotlin/src/MonthExt.kt | 24 +++++ core/darwin/src/DayOfWeekExtDarwin.kt | 59 ++++++++++ core/darwin/src/Locale.kt | 32 ++++++ core/darwin/src/MonthExtDarwin.kt | 53 +++++++++ core/jvm/src/DayOfWeekExtJvm.kt | 26 +++++ core/jvm/src/Locale.kt | 29 +++++ core/jvm/src/MonthExtJvm.kt | 26 +++++ 23 files changed, 718 insertions(+) create mode 100644 core/common/src/DayOfWeekExt.kt create mode 100644 core/common/src/Locale.kt create mode 100644 core/common/src/MonthExt.kt create mode 100644 core/common/src/TextStyle.kt create mode 100644 core/common/src/format/DayOfWeekNamesExt.kt create mode 100644 core/common/src/format/MonthNamesExt.kt create mode 100644 core/common/test/LocalizationTest.kt create mode 100644 core/common/test/samples/LocaleSamples.kt create mode 100644 core/commonKotlin/src/DayOfWeekExt.kt create mode 100644 core/commonKotlin/src/Locale.kt create mode 100644 core/commonKotlin/src/MonthExt.kt create mode 100644 core/darwin/src/DayOfWeekExtDarwin.kt create mode 100644 core/darwin/src/Locale.kt create mode 100644 core/darwin/src/MonthExtDarwin.kt create mode 100644 core/jvm/src/DayOfWeekExtJvm.kt create mode 100644 core/jvm/src/Locale.kt create mode 100644 core/jvm/src/MonthExtJvm.kt diff --git a/core/api/kotlinx-datetime.api b/core/api/kotlinx-datetime.api index cacd8d702..000009559 100644 --- a/core/api/kotlinx-datetime.api +++ b/core/api/kotlinx-datetime.api @@ -195,6 +195,8 @@ public final class kotlinx/datetime/DayOfWeek : java/lang/Enum { public final class kotlinx/datetime/DayOfWeekKt { public static final synthetic fun DayOfWeek (I)Ljava/time/DayOfWeek; public static final fun DayOfWeek (I)Lkotlinx/datetime/DayOfWeek; + public static final fun displayName (Lkotlinx/datetime/DayOfWeek;Lkotlinx/datetime/TextStyle;Lkotlinx/datetime/Locale;)Ljava/lang/String; + public static synthetic fun displayName$default (Lkotlinx/datetime/DayOfWeek;Lkotlinx/datetime/TextStyle;Lkotlinx/datetime/Locale;ILjava/lang/Object;)Ljava/lang/String; public static final fun getIsoDayNumber (Ljava/time/DayOfWeek;)I public static final fun getIsoDayNumber (Lkotlinx/datetime/DayOfWeek;)I } @@ -548,6 +550,19 @@ public final class kotlinx/datetime/LocalTimeKt { public static final fun toLocalTime (Ljava/lang/String;)Lkotlinx/datetime/LocalTime; } +public final class kotlinx/datetime/Locale { + public static final field Companion Lkotlinx/datetime/Locale$Companion; +} + +public final class kotlinx/datetime/Locale$Companion { + public final fun getDefault ()Lkotlinx/datetime/Locale; +} + +public final class kotlinx/datetime/LocaleKt { + public static final fun toJavaLocale (Lkotlinx/datetime/Locale;)Ljava/util/Locale; + public static final fun toKotlinLocale (Ljava/util/Locale;)Lkotlinx/datetime/Locale; +} + public final class kotlinx/datetime/Month : java/lang/Enum { public static final field APRIL Lkotlinx/datetime/Month; public static final field AUGUST Lkotlinx/datetime/Month; @@ -569,6 +584,8 @@ public final class kotlinx/datetime/Month : java/lang/Enum { public final class kotlinx/datetime/MonthKt { public static final synthetic fun Month (I)Ljava/time/Month; public static final fun Month (I)Lkotlinx/datetime/Month; + public static final fun displayName (Lkotlinx/datetime/Month;Lkotlinx/datetime/TextStyle;Lkotlinx/datetime/Locale;)Ljava/lang/String; + public static synthetic fun displayName$default (Lkotlinx/datetime/Month;Lkotlinx/datetime/TextStyle;Lkotlinx/datetime/Locale;ILjava/lang/Object;)Ljava/lang/String; public static final fun getNumber (Ljava/time/Month;)I public static final fun getNumber (Lkotlinx/datetime/Month;)I } @@ -592,6 +609,18 @@ public final class kotlinx/datetime/Ser : java/io/Externalizable { public final class kotlinx/datetime/Ser$Companion { } +public final class kotlinx/datetime/TextStyle : java/lang/Enum { + public static final field FULL Lkotlinx/datetime/TextStyle; + public static final field FULL_STANDALONE Lkotlinx/datetime/TextStyle; + public static final field NARROW Lkotlinx/datetime/TextStyle; + public static final field NARROW_STANDALONE Lkotlinx/datetime/TextStyle; + public static final field SHORT Lkotlinx/datetime/TextStyle; + public static final field SHORT_STANDALONE Lkotlinx/datetime/TextStyle; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/datetime/TextStyle; + public static fun values ()[Lkotlinx/datetime/TextStyle; +} + public class kotlinx/datetime/TimeZone { public static final field Companion Lkotlinx/datetime/TimeZone$Companion; public fun equals (Ljava/lang/Object;)Z @@ -1003,6 +1032,10 @@ public final class kotlinx/datetime/format/DayOfWeekNames$Companion { public final fun getENGLISH_FULL ()Lkotlinx/datetime/format/DayOfWeekNames; } +public final class kotlinx/datetime/format/DayOfWeekNamesExtKt { + public static final fun getENGLISH_NARROW (Lkotlinx/datetime/format/DayOfWeekNames$Companion;)Ljava/util/List; +} + public abstract interface annotation class kotlinx/datetime/format/FormatStringsInDatetimeFormats : java/lang/annotation/Annotation { } @@ -1021,6 +1054,10 @@ public final class kotlinx/datetime/format/MonthNames$Companion { public final fun getENGLISH_FULL ()Lkotlinx/datetime/format/MonthNames; } +public final class kotlinx/datetime/format/MonthNamesExtKt { + public static final fun getENGLISH_NARROW (Lkotlinx/datetime/format/MonthNames$Companion;)Ljava/util/List; +} + public final class kotlinx/datetime/format/Padding : java/lang/Enum { public static final field NONE Lkotlinx/datetime/format/Padding; public static final field SPACE Lkotlinx/datetime/format/Padding; diff --git a/core/api/kotlinx-datetime.klib.api b/core/api/kotlinx-datetime.klib.api index 4a20aa640..2a2f80cf6 100644 --- a/core/api/kotlinx-datetime.klib.api +++ b/core/api/kotlinx-datetime.klib.api @@ -71,6 +71,21 @@ final enum class kotlinx.datetime/Month : kotlin/Enum { final fun values(): kotlin/Array // kotlinx.datetime/Month.values|values#static(){}[0] } +final enum class kotlinx.datetime/TextStyle : kotlin/Enum { // kotlinx.datetime/TextStyle|null[0] + enum entry FULL // kotlinx.datetime/TextStyle.FULL|null[0] + enum entry FULL_STANDALONE // kotlinx.datetime/TextStyle.FULL_STANDALONE|null[0] + enum entry NARROW // kotlinx.datetime/TextStyle.NARROW|null[0] + enum entry NARROW_STANDALONE // kotlinx.datetime/TextStyle.NARROW_STANDALONE|null[0] + enum entry SHORT // kotlinx.datetime/TextStyle.SHORT|null[0] + enum entry SHORT_STANDALONE // kotlinx.datetime/TextStyle.SHORT_STANDALONE|null[0] + + final val entries // kotlinx.datetime/TextStyle.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // kotlinx.datetime/TextStyle.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): kotlinx.datetime/TextStyle // kotlinx.datetime/TextStyle.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // kotlinx.datetime/TextStyle.values|values#static(){}[0] +} + abstract interface kotlinx.datetime/Clock { // kotlinx.datetime/Clock|null[0] abstract fun now(): kotlinx.datetime/Instant // kotlinx.datetime/Clock.now|now(){}[0] @@ -552,6 +567,12 @@ final class kotlinx.datetime/LocalTime : kotlin/Comparable(): kotlin.collections/List // kotlinx.datetime.format/ENGLISH_NARROW.|@kotlinx.datetime.format.DayOfWeekNames.Companion(){}[0] +final val kotlinx.datetime.format/ENGLISH_NARROW // kotlinx.datetime.format/ENGLISH_NARROW|@kotlinx.datetime.format.MonthNames.Companion{}ENGLISH_NARROW[0] + final fun (kotlinx.datetime.format/MonthNames.Companion).(): kotlin.collections/List // kotlinx.datetime.format/ENGLISH_NARROW.|@kotlinx.datetime.format.MonthNames.Companion(){}[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] @@ -1139,6 +1164,7 @@ final fun (kotlinx.datetime/Clock).kotlinx.datetime/todayAt(kotlinx.datetime/Tim final fun (kotlinx.datetime/Clock).kotlinx.datetime/todayIn(kotlinx.datetime/TimeZone): kotlinx.datetime/LocalDate // kotlinx.datetime/todayIn|todayIn@kotlinx.datetime.Clock(kotlinx.datetime.TimeZone){}[0] final fun (kotlinx.datetime/DatePeriod).kotlinx.datetime/plus(kotlinx.datetime/DatePeriod): kotlinx.datetime/DatePeriod // kotlinx.datetime/plus|plus@kotlinx.datetime.DatePeriod(kotlinx.datetime.DatePeriod){}[0] final fun (kotlinx.datetime/DateTimePeriod).kotlinx.datetime/plus(kotlinx.datetime/DateTimePeriod): kotlinx.datetime/DateTimePeriod // kotlinx.datetime/plus|plus@kotlinx.datetime.DateTimePeriod(kotlinx.datetime.DateTimePeriod){}[0] +final fun (kotlinx.datetime/DayOfWeek).kotlinx.datetime/displayName(kotlinx.datetime/TextStyle = ..., kotlinx.datetime/Locale = ...): kotlin/String // kotlinx.datetime/displayName|displayName@kotlinx.datetime.DayOfWeek(kotlinx.datetime.TextStyle;kotlinx.datetime.Locale){}[0] final fun (kotlinx.datetime/Instant).kotlinx.datetime/daysUntil(kotlinx.datetime/Instant, kotlinx.datetime/TimeZone): kotlin/Int // kotlinx.datetime/daysUntil|daysUntil@kotlinx.datetime.Instant(kotlinx.datetime.Instant;kotlinx.datetime.TimeZone){}[0] final fun (kotlinx.datetime/Instant).kotlinx.datetime/format(kotlinx.datetime.format/DateTimeFormat, kotlinx.datetime/UtcOffset = ...): kotlin/String // kotlinx.datetime/format|format@kotlinx.datetime.Instant(kotlinx.datetime.format.DateTimeFormat;kotlinx.datetime.UtcOffset){}[0] final fun (kotlinx.datetime/Instant).kotlinx.datetime/minus(kotlin/Int, kotlinx.datetime/DateTimeUnit, kotlinx.datetime/TimeZone): kotlinx.datetime/Instant // kotlinx.datetime/minus|minus@kotlinx.datetime.Instant(kotlin.Int;kotlinx.datetime.DateTimeUnit;kotlinx.datetime.TimeZone){}[0] @@ -1207,6 +1233,7 @@ final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlin/Int, kotli final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlin/Int, kotlinx.datetime/Month, kotlin/Int, kotlin/Unit = ...): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlin.Int;kotlinx.datetime.Month;kotlin.Int;kotlin.Unit){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/atDate(kotlinx.datetime/LocalDate): kotlinx.datetime/LocalDateTime // kotlinx.datetime/atDate|atDate@kotlinx.datetime.LocalTime(kotlinx.datetime.LocalDate){}[0] final fun (kotlinx.datetime/LocalTime).kotlinx.datetime/format(kotlinx.datetime.format/DateTimeFormat): kotlin/String // kotlinx.datetime/format|format@kotlinx.datetime.LocalTime(kotlinx.datetime.format.DateTimeFormat){}[0] +final fun (kotlinx.datetime/Month).kotlinx.datetime/displayName(kotlinx.datetime/TextStyle = ..., kotlinx.datetime/Locale = ...): kotlin/String // kotlinx.datetime/displayName|displayName@kotlinx.datetime.Month(kotlinx.datetime.TextStyle;kotlinx.datetime.Locale){}[0] final fun (kotlinx.datetime/TimeZone).kotlinx.datetime/offsetAt(kotlin.time/Instant): kotlinx.datetime/UtcOffset // kotlinx.datetime/offsetAt|offsetAt@kotlinx.datetime.TimeZone(kotlin.time.Instant){}[0] final fun (kotlinx.datetime/TimeZone).kotlinx.datetime/offsetAt(kotlinx.datetime/Instant): kotlinx.datetime/UtcOffset // kotlinx.datetime/offsetAt|offsetAt@kotlinx.datetime.TimeZone(kotlinx.datetime.Instant){}[0] final fun (kotlinx.datetime/UtcOffset).kotlinx.datetime/asTimeZone(): kotlinx.datetime/FixedOffsetTimeZone // kotlinx.datetime/asTimeZone|asTimeZone@kotlinx.datetime.UtcOffset(){}[0] diff --git a/core/common/src/DayOfWeekExt.kt b/core/common/src/DayOfWeekExt.kt new file mode 100644 index 000000000..82630fc4b --- /dev/null +++ b/core/common/src/DayOfWeekExt.kt @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@file:JvmName("DayOfWeekKt") +@file:JvmMultifileClass + +package kotlinx.datetime + +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Returns the localized display name for this day of the week. + * + * The display name is formatted according to the specified [textStyle] and [locale]. + * For example, [DayOfWeek.MONDAY] with [TextStyle.FULL_STANDALONE] and English locale + * returns "Monday", while with [TextStyle.SHORT] it returns "Mon". + * + * The distinction between standalone and non-standalone styles matters in some languages + * where grammatical forms differ based on context. For example, in some Slavic languages, + * the standalone form is used when the day appears alone, while the non-standalone form + * is used when it's part of a date phrase. + * + * @param textStyle the text style to use for formatting (default: [TextStyle.FULL_STANDALONE]) + * @param locale the locale to use for formatting (default: system default locale) + * @return the localized display name for this day of the week + * @sample kotlinx.datetime.test.samples.DayOfWeekSamples.displayName + */ +public expect fun DayOfWeek.displayName( + textStyle: TextStyle = TextStyle.FULL_STANDALONE, + locale: Locale = Locale.getDefault() +): String diff --git a/core/common/src/Locale.kt b/core/common/src/Locale.kt new file mode 100644 index 000000000..77cb8babf --- /dev/null +++ b/core/common/src/Locale.kt @@ -0,0 +1,25 @@ +/* + * 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 + +/** + * Represents a locale, which is used to format date and time values according to cultural conventions. + * + * This is a platform-specific type: + * - On JVM: [java.util.Locale] + * - On iOS/macOS: NSLocale + * - On other platforms: platform-specific locale representation + * + * @sample kotlinx.datetime.test.samples.LocaleSamples.usage + */ +public expect class Locale { + public companion object { + /** + * Returns the default locale for the current system. + */ + public fun getDefault(): Locale + } +} diff --git a/core/common/src/MonthExt.kt b/core/common/src/MonthExt.kt new file mode 100644 index 000000000..c781ad46a --- /dev/null +++ b/core/common/src/MonthExt.kt @@ -0,0 +1,33 @@ +/* + * 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. + */ + +@file:JvmName("MonthKt") +@file:JvmMultifileClass + +package kotlinx.datetime + +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName + +/** + * Returns the localized display name for this month. + * + * The display name is formatted according to the specified [textStyle] and [locale]. + * For example, [Month.JANUARY] with [TextStyle.FULL_STANDALONE] and English locale + * returns "January", while with [TextStyle.SHORT] it returns "Jan". + * + * The distinction between standalone and non-standalone styles matters in some languages + * where grammatical forms differ based on context. For example, in Polish, the standalone + * form of January is "styczeń", while the genitive form used in dates is "stycznia". + * + * @param textStyle the text style to use for formatting (default: [TextStyle.FULL_STANDALONE]) + * @param locale the locale to use for formatting (default: system default locale) + * @return the localized display name for this month + * @sample kotlinx.datetime.test.samples.MonthSamples.displayName + */ +public expect fun Month.displayName( + textStyle: TextStyle = TextStyle.FULL_STANDALONE, + locale: Locale = Locale.getDefault() +): String diff --git a/core/common/src/TextStyle.kt b/core/common/src/TextStyle.kt new file mode 100644 index 000000000..437700518 --- /dev/null +++ b/core/common/src/TextStyle.kt @@ -0,0 +1,53 @@ +/* + * 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 + +/** + * Represents the style used for formatting date-time text, such as month or day-of-week names. + * + * Different styles provide different levels of detail and formatting: + * - [FULL] and [FULL_STANDALONE] provide complete names (e.g., "January", "Monday") + * - [SHORT] and [SHORT_STANDALONE] provide abbreviated names (e.g., "Jan", "Mon") + * - [NARROW] and [NARROW_STANDALONE] provide minimal names (e.g., "J", "M") + * + * The standalone versions are used when the name appears in isolation, + * while the non-standalone versions are used when the name appears as part of a larger phrase. + * This distinction is important in some languages where grammatical forms differ based on context. + * For example, in Polish, the standalone form of "January" is "styczeń", + * while the genitive form used in dates is "stycznia". + */ +public enum class TextStyle { + /** + * Full text style, typically used in context (e.g., "Monday", "January"). + */ + FULL, + + /** + * Full text style for standalone use (e.g., "Monday", "January"). + * This is the default style for most use cases. + */ + FULL_STANDALONE, + + /** + * Short or abbreviated text style, typically used in context (e.g., "Mon", "Jan"). + */ + SHORT, + + /** + * Short or abbreviated text style for standalone use (e.g., "Mon", "Jan"). + */ + SHORT_STANDALONE, + + /** + * Narrow text style, typically a single character, used in context (e.g., "M", "J"). + */ + NARROW, + + /** + * Narrow text style for standalone use, typically a single character (e.g., "M", "J"). + */ + NARROW_STANDALONE, +} diff --git a/core/common/src/format/DayOfWeekNamesExt.kt b/core/common/src/format/DayOfWeekNamesExt.kt new file mode 100644 index 000000000..710a14176 --- /dev/null +++ b/core/common/src/format/DayOfWeekNamesExt.kt @@ -0,0 +1,13 @@ +/* + * 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.format + +/** + * Narrow English names of weekdays from 'M' to 'S'. + * Used primarily as fallback for localization on platforms where narrow names are unavailable. + */ +public val DayOfWeekNames.Companion.ENGLISH_NARROW: List + get() = listOf("M", "T", "W", "T", "F", "S", "S") diff --git a/core/common/src/format/MonthNamesExt.kt b/core/common/src/format/MonthNamesExt.kt new file mode 100644 index 000000000..443e3bc2d --- /dev/null +++ b/core/common/src/format/MonthNamesExt.kt @@ -0,0 +1,16 @@ +/* + * 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.format + +/** + * Narrow English month names. + * Used primarily as fallback for localization on platforms where narrow names are unavailable. + */ +public val MonthNames.Companion.ENGLISH_NARROW: List + get() = listOf( + "J", "F", "M", "A", "M", "J", + "J", "A", "S", "O", "N", "D" + ) diff --git a/core/common/test/LocalizationTest.kt b/core/common/test/LocalizationTest.kt new file mode 100644 index 000000000..477fc2b9c --- /dev/null +++ b/core/common/test/LocalizationTest.kt @@ -0,0 +1,102 @@ +/* + * 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.test + +import kotlinx.datetime.* +import kotlin.test.* + +class LocalizationTest { + + @Test + fun testDayOfWeekDisplayNameReturnsNonEmptyString() { + for (day in DayOfWeek.entries) { + val displayName = day.displayName() + assertNotNull(displayName) + assertTrue(displayName.isNotEmpty(), "Display name for $day should not be empty") + } + } + + @Test + fun testMonthDisplayNameReturnsNonEmptyString() { + for (month in Month.entries) { + val displayName = month.displayName() + assertNotNull(displayName) + assertTrue(displayName.isNotEmpty(), "Display name for $month should not be empty") + } + } + + @Test + fun testAllTextStylesWork() { + val day = DayOfWeek.MONDAY + val month = Month.JANUARY + + for (style in TextStyle.entries) { + val dayName = day.displayName(textStyle = style) + val monthName = month.displayName(textStyle = style) + + assertNotNull(dayName, "Day name should not be null for style $style") + assertNotNull(monthName, "Month name should not be null for style $style") + assertTrue(dayName.isNotEmpty(), "Day name should not be empty for style $style") + assertTrue(monthName.isNotEmpty(), "Month name should not be empty for style $style") + } + } + + @Test + fun testNarrowShorterThanShort() { + val day = DayOfWeek.WEDNESDAY + val month = Month.SEPTEMBER + + val dayNarrow = day.displayName(textStyle = TextStyle.NARROW) + val dayShort = day.displayName(textStyle = TextStyle.SHORT) + val dayFull = day.displayName(textStyle = TextStyle.FULL) + + val monthNarrow = month.displayName(textStyle = TextStyle.NARROW) + val monthShort = month.displayName(textStyle = TextStyle.SHORT) + val monthFull = month.displayName(textStyle = TextStyle.FULL) + + // Narrow should generally be shorter or equal to short + assertTrue(dayNarrow.length <= dayShort.length, + "Day narrow ($dayNarrow) should not be longer than short ($dayShort)") + assertTrue(dayShort.length <= dayFull.length, + "Day short ($dayShort) should not be longer than full ($dayFull)") + + assertTrue(monthNarrow.length <= monthShort.length, + "Month narrow ($monthNarrow) should not be longer than short ($monthShort)") + assertTrue(monthShort.length <= monthFull.length, + "Month short ($monthShort) should not be longer than full ($monthFull)") + } + + @Test + fun testConsistentResults() { + val day = DayOfWeek.FRIDAY + val month = Month.MARCH + + // Multiple calls with same parameters should return same result + val dayName1 = day.displayName() + val dayName2 = day.displayName() + assertEquals(dayName1, dayName2, "Multiple calls should return consistent results") + + val monthName1 = month.displayName() + val monthName2 = month.displayName() + assertEquals(monthName1, monthName2, "Multiple calls should return consistent results") + } + + @Test + fun testAllDaysHaveUniqueFullNames() { + val dayNames = DayOfWeek.entries.map { it.displayName(textStyle = TextStyle.FULL) } + val uniqueNames = dayNames.toSet() + assertEquals(7, uniqueNames.size, + "All 7 days should have unique full names. Found: $dayNames") + } + + @Test + fun testAllMonthsHaveUniqueFullNames() { + val monthNames = Month.entries.map { it.displayName(textStyle = TextStyle.FULL) } + val uniqueNames = monthNames.toSet() + assertEquals(12, uniqueNames.size, + "All 12 months should have unique full names. Found: $monthNames") + } +} diff --git a/core/common/test/samples/DayOfWeekSamples.kt b/core/common/test/samples/DayOfWeekSamples.kt index 73320fbf5..26715694b 100644 --- a/core/common/test/samples/DayOfWeekSamples.kt +++ b/core/common/test/samples/DayOfWeekSamples.kt @@ -49,4 +49,22 @@ class DayOfWeekSamples { // Expected } } + + @Test + fun displayName() { + // Getting the localized display name for a day of the week + val monday = DayOfWeek.MONDAY + + // Full name (default) + val fullName = monday.displayName() + check(fullName.isNotEmpty()) + + // Short name + val shortName = monday.displayName(textStyle = TextStyle.SHORT) + check(shortName.isNotEmpty()) + + // Narrow name (typically one letter) + val narrowName = monday.displayName(textStyle = TextStyle.NARROW) + check(narrowName.isNotEmpty()) + } } diff --git a/core/common/test/samples/LocaleSamples.kt b/core/common/test/samples/LocaleSamples.kt new file mode 100644 index 000000000..6aabf5b71 --- /dev/null +++ b/core/common/test/samples/LocaleSamples.kt @@ -0,0 +1,23 @@ +/* + * 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.test.samples + +import kotlinx.datetime.* +import kotlin.test.* + +class LocaleSamples { + + @Test + fun usage() { + // Using the default system locale + val locale = Locale.getDefault() + val monday = DayOfWeek.MONDAY + + // Get localized name using default locale + val displayName = monday.displayName(locale = locale) + check(displayName.isNotEmpty()) + } +} diff --git a/core/common/test/samples/MonthSamples.kt b/core/common/test/samples/MonthSamples.kt index 4b2c9e0d3..cebd6314c 100644 --- a/core/common/test/samples/MonthSamples.kt +++ b/core/common/test/samples/MonthSamples.kt @@ -54,4 +54,22 @@ class MonthSamples { // Expected } } + + @Test + fun displayName() { + // Getting the localized display name for a month + val january = Month.JANUARY + + // Full name (default) + val fullName = january.displayName() + check(fullName.isNotEmpty()) + + // Short name + val shortName = january.displayName(textStyle = TextStyle.SHORT) + check(shortName.isNotEmpty()) + + // Narrow name (typically a few letters) + val narrowName = january.displayName(textStyle = TextStyle.NARROW) + check(narrowName.isNotEmpty()) + } } diff --git a/core/common/test/samples/format/LocalDateFormatSamples.kt b/core/common/test/samples/format/LocalDateFormatSamples.kt index 0a03c98c0..9fc0ea25d 100644 --- a/core/common/test/samples/format/LocalDateFormatSamples.kt +++ b/core/common/test/samples/format/LocalDateFormatSamples.kt @@ -117,5 +117,17 @@ class LocalDateFormatSamples { } check(format.format(LocalDate(2021, 1, 13)) == "2021-01-13, Wed") } + + @Test + fun englishNarrow() { + // Using the built-in English narrow day of week names in formatting + // Note: Narrow names contain duplicates (e.g., "T" for Tuesday/Thursday) and cannot be parsed unambiguously + check(DayOfWeekNames.ENGLISH_NARROW == listOf( + "M", "T", "W", "T", "F", "S", "S" + )) + // They are useful for compact display where context makes the meaning clear + val narrowName = DayOfWeekNames.ENGLISH_NARROW[DayOfWeek.WEDNESDAY.ordinal] + check(narrowName == "W") + } } } diff --git a/core/common/test/samples/format/YearMonthFormatSamples.kt b/core/common/test/samples/format/YearMonthFormatSamples.kt index 47e497381..43c7f4e39 100644 --- a/core/common/test/samples/format/YearMonthFormatSamples.kt +++ b/core/common/test/samples/format/YearMonthFormatSamples.kt @@ -6,8 +6,10 @@ package kotlinx.datetime.test.samples.format import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month import kotlinx.datetime.YearMonth import kotlinx.datetime.format.DayOfWeekNames +import kotlinx.datetime.format.ENGLISH_NARROW import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.Padding import kotlinx.datetime.format.char @@ -148,5 +150,18 @@ class YearMonthFormatSamples { } check(format.format(LocalDate(2021, 1, 13)) == "Jan 13, 2021") } + + @Test + fun englishNarrow() { + // Using the built-in English narrow month names in formatting + // Note: Narrow names contain duplicates (e.g., "J" for January/June/July) and cannot be parsed unambiguously + check(MonthNames.ENGLISH_NARROW == listOf( + "J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D" + )) + // They are useful for compact display where context makes the meaning clear + check(Month.JANUARY.toString() == "JANUARY") + val narrowName = MonthNames.ENGLISH_NARROW[Month.MARCH.ordinal] + check(narrowName == "M") + } } } diff --git a/core/commonKotlin/src/DayOfWeekExt.kt b/core/commonKotlin/src/DayOfWeekExt.kt new file mode 100644 index 000000000..e81850ac9 --- /dev/null +++ b/core/commonKotlin/src/DayOfWeekExt.kt @@ -0,0 +1,24 @@ +/* + * 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.DayOfWeekNames +import kotlinx.datetime.format.ENGLISH_NARROW + +/** + * Returns the display name for this day of the week. + * On platforms without native locale support, returns English names only. + */ +public actual fun DayOfWeek.displayName( + textStyle: TextStyle, + locale: Locale +): String { + return when (textStyle) { + TextStyle.FULL, TextStyle.FULL_STANDALONE -> DayOfWeekNames.ENGLISH_FULL.names[this.ordinal] + TextStyle.SHORT, TextStyle.SHORT_STANDALONE -> DayOfWeekNames.ENGLISH_ABBREVIATED.names[this.ordinal] + TextStyle.NARROW, TextStyle.NARROW_STANDALONE -> DayOfWeekNames.ENGLISH_NARROW[this.ordinal] + } +} diff --git a/core/commonKotlin/src/Locale.kt b/core/commonKotlin/src/Locale.kt new file mode 100644 index 000000000..053484609 --- /dev/null +++ b/core/commonKotlin/src/Locale.kt @@ -0,0 +1,19 @@ +/* + * 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 + +/** + * Platform-specific locale implementation for platforms without native locale support. + * Uses a simple string-based representation. + */ +public actual class Locale internal constructor(internal val localeString: String) { + public actual companion object { + /** + * Returns the default locale (English) for platforms without locale detection. + */ + public actual fun getDefault(): Locale = Locale("en") + } +} diff --git a/core/commonKotlin/src/MonthExt.kt b/core/commonKotlin/src/MonthExt.kt new file mode 100644 index 000000000..1c6d6fdf0 --- /dev/null +++ b/core/commonKotlin/src/MonthExt.kt @@ -0,0 +1,24 @@ +/* + * 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.ENGLISH_NARROW +import kotlinx.datetime.format.MonthNames + +/** + * Returns the display name for this month. + * On platforms without native locale support, returns English names only. + */ +public actual fun Month.displayName( + textStyle: TextStyle, + locale: Locale +): String { + return when (textStyle) { + TextStyle.FULL, TextStyle.FULL_STANDALONE -> MonthNames.ENGLISH_FULL.names[this.ordinal] + TextStyle.SHORT, TextStyle.SHORT_STANDALONE -> MonthNames.ENGLISH_ABBREVIATED.names[this.ordinal] + TextStyle.NARROW, TextStyle.NARROW_STANDALONE -> MonthNames.ENGLISH_NARROW[this.ordinal] + } +} diff --git a/core/darwin/src/DayOfWeekExtDarwin.kt b/core/darwin/src/DayOfWeekExtDarwin.kt new file mode 100644 index 000000000..ae0d55fc2 --- /dev/null +++ b/core/darwin/src/DayOfWeekExtDarwin.kt @@ -0,0 +1,59 @@ +/* + * 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.DayOfWeekNames +import platform.Foundation.NSCalendar + +// Cache for calendars per locale to avoid repeated creation +private val calendarCache = mutableMapOf() + +private fun getOrCreateCalendar(locale: Locale): NSCalendar { + return calendarCache.getOrPut(locale) { + NSCalendar.currentCalendar.apply { + this.locale = locale.value + } + } +} + +/** + * Returns the localized display name for this day of the week using NSCalendar. + */ +public actual fun DayOfWeek.displayName( + textStyle: TextStyle, + locale: Locale +): String { + val calendar = getOrCreateCalendar(locale) + + // Convert ISO day number (1=Monday, 7=Sunday) to NSCalendar weekday (1=Sunday, 7=Saturday) + // ISO: Mon=1, Tue=2, Wed=3, Thu=4, Fri=5, Sat=6, Sun=7 + // iOS: Sun=1, Mon=2, Tue=3, Wed=4, Thu=5, Fri=6, Sat=7 + val iosWeekday = when (this.isoDayNumber) { + 7 -> 1 // Sunday + else -> this.isoDayNumber + 1 // Monday-Saturday: add 1 + } + + val weekdaySymbols = when (textStyle) { + TextStyle.FULL -> calendar.weekdaySymbols + TextStyle.FULL_STANDALONE -> calendar.standaloneWeekdaySymbols + TextStyle.SHORT -> calendar.shortWeekdaySymbols + TextStyle.SHORT_STANDALONE -> calendar.shortStandaloneWeekdaySymbols + TextStyle.NARROW -> calendar.veryShortWeekdaySymbols + TextStyle.NARROW_STANDALONE -> calendar.veryShortStandaloneWeekdaySymbols + } + + // Safely convert NSArray to List + // In Kotlin/Native, NSArray is already mapped to List<*> + @Suppress("UNCHECKED_CAST") + val weekdayNames = weekdaySymbols as? List + + val result = weekdayNames?.getOrNull(iosWeekday - 1) + return result ?: when (textStyle) { + TextStyle.FULL, TextStyle.FULL_STANDALONE -> DayOfWeekNames.ENGLISH_FULL.names[this.ordinal] + TextStyle.SHORT, TextStyle.SHORT_STANDALONE -> DayOfWeekNames.ENGLISH_ABBREVIATED.names[this.ordinal] + TextStyle.NARROW, TextStyle.NARROW_STANDALONE -> listOf("M", "T", "W", "T", "F", "S", "S")[this.ordinal] + } +} diff --git a/core/darwin/src/Locale.kt b/core/darwin/src/Locale.kt new file mode 100644 index 000000000..0532182f6 --- /dev/null +++ b/core/darwin/src/Locale.kt @@ -0,0 +1,32 @@ +/* + * 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 platform.Foundation.NSLocale +import platform.Foundation.currentLocale + +/** + * Platform-specific locale implementation for Darwin platforms (iOS, macOS, watchOS, tvOS). + * Wraps [NSLocale]. + */ +public actual class Locale internal constructor(internal val value: NSLocale) { + public actual companion object { + /** + * Returns the default locale for the current system. + */ + public actual fun getDefault(): Locale = Locale(NSLocale.currentLocale) + } +} + +/** + * Converts this [NSLocale] to a [Locale]. + */ +public fun NSLocale.toKotlinLocale(): Locale = Locale(this) + +/** + * Converts this [Locale] to an [NSLocale]. + */ +public fun Locale.toNSLocale(): NSLocale = this.value diff --git a/core/darwin/src/MonthExtDarwin.kt b/core/darwin/src/MonthExtDarwin.kt new file mode 100644 index 000000000..c5b866708 --- /dev/null +++ b/core/darwin/src/MonthExtDarwin.kt @@ -0,0 +1,53 @@ +/* + * 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.MonthNames +import platform.Foundation.NSCalendar + +// Cache for calendars per locale to avoid repeated creation +private val monthCalendarCache = mutableMapOf() + +private fun getOrCreateMonthCalendar(locale: Locale): NSCalendar { + return monthCalendarCache.getOrPut(locale) { + NSCalendar.currentCalendar.apply { + this.locale = locale.value + } + } +} + +/** + * Returns the localized display name for this month using NSCalendar. + */ +public actual fun Month.displayName( + textStyle: TextStyle, + locale: Locale +): String { + val calendar = getOrCreateMonthCalendar(locale) + + // Month.ordinal is 0-based (JANUARY=0, DECEMBER=11) + // NSCalendar month symbols are 0-indexed arrays + val monthSymbols = when (textStyle) { + TextStyle.FULL -> calendar.monthSymbols + TextStyle.FULL_STANDALONE -> calendar.standaloneMonthSymbols + TextStyle.SHORT -> calendar.shortMonthSymbols + TextStyle.SHORT_STANDALONE -> calendar.shortStandaloneMonthSymbols + TextStyle.NARROW -> calendar.veryShortMonthSymbols + TextStyle.NARROW_STANDALONE -> calendar.veryShortStandaloneMonthSymbols + } + + // Safely convert NSArray to List + // In Kotlin/Native, NSArray is already mapped to List<*> + @Suppress("UNCHECKED_CAST") + val monthNames = monthSymbols as? List + + val result = monthNames?.getOrNull(this.ordinal) + return result ?: when (textStyle) { + TextStyle.FULL, TextStyle.FULL_STANDALONE -> MonthNames.ENGLISH_FULL.names[this.ordinal] + TextStyle.SHORT, TextStyle.SHORT_STANDALONE -> MonthNames.ENGLISH_ABBREVIATED.names[this.ordinal] + TextStyle.NARROW, TextStyle.NARROW_STANDALONE -> listOf("J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D")[this.ordinal] + } +} diff --git a/core/jvm/src/DayOfWeekExtJvm.kt b/core/jvm/src/DayOfWeekExtJvm.kt new file mode 100644 index 000000000..2ec376177 --- /dev/null +++ b/core/jvm/src/DayOfWeekExtJvm.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +@file:JvmName("DayOfWeekKt") +@file:JvmMultifileClass +package kotlinx.datetime + +/** + * Returns the localized display name for this day of the week using java.time.DayOfWeek. + */ +public actual fun DayOfWeek.displayName( + textStyle: TextStyle, + locale: Locale +): String { + val javaTextStyle = when (textStyle) { + TextStyle.FULL -> java.time.format.TextStyle.FULL + TextStyle.FULL_STANDALONE -> java.time.format.TextStyle.FULL_STANDALONE + TextStyle.SHORT -> java.time.format.TextStyle.SHORT + TextStyle.SHORT_STANDALONE -> java.time.format.TextStyle.SHORT_STANDALONE + TextStyle.NARROW -> java.time.format.TextStyle.NARROW + TextStyle.NARROW_STANDALONE -> java.time.format.TextStyle.NARROW_STANDALONE + } + return toJavaDayOfWeek().getDisplayName(javaTextStyle, locale.value) +} diff --git a/core/jvm/src/Locale.kt b/core/jvm/src/Locale.kt new file mode 100644 index 000000000..b5a3477c8 --- /dev/null +++ b/core/jvm/src/Locale.kt @@ -0,0 +1,29 @@ +/* + * 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 + +/** + * Platform-specific locale implementation for JVM. + * Wraps [java.util.Locale]. + */ +public actual class Locale internal constructor(internal val value: java.util.Locale) { + public actual companion object { + /** + * Returns the default locale for the current system. + */ + public actual fun getDefault(): Locale = Locale(java.util.Locale.getDefault()) + } +} + +/** + * Converts this [java.util.Locale] to a [Locale]. + */ +public fun java.util.Locale.toKotlinLocale(): Locale = Locale(this) + +/** + * Converts this [Locale] to a [java.util.Locale]. + */ +public fun Locale.toJavaLocale(): java.util.Locale = this.value diff --git a/core/jvm/src/MonthExtJvm.kt b/core/jvm/src/MonthExtJvm.kt new file mode 100644 index 000000000..7890aefc3 --- /dev/null +++ b/core/jvm/src/MonthExtJvm.kt @@ -0,0 +1,26 @@ +/* + * 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. + */ + +@file:JvmName("MonthKt") +@file:JvmMultifileClass +package kotlinx.datetime + +/** + * Returns the localized display name for this month using java.time.Month. + */ +public actual fun Month.displayName( + textStyle: TextStyle, + locale: Locale +): String { + val javaTextStyle = when (textStyle) { + TextStyle.FULL -> java.time.format.TextStyle.FULL + TextStyle.FULL_STANDALONE -> java.time.format.TextStyle.FULL_STANDALONE + TextStyle.SHORT -> java.time.format.TextStyle.SHORT + TextStyle.SHORT_STANDALONE -> java.time.format.TextStyle.SHORT_STANDALONE + TextStyle.NARROW -> java.time.format.TextStyle.NARROW + TextStyle.NARROW_STANDALONE -> java.time.format.TextStyle.NARROW_STANDALONE + } + return toJavaMonth().getDisplayName(javaTextStyle, locale.value) +}