Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DatePicker. Fix today circle #782

Merged
merged 12 commits into from
Sep 5, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,17 @@ internal actual object PlatformDateFormat {
setDateStyle(NSDateFormatterShortStyle)
}.dateFormat

val delimiter = pattern.first { !it.isLetter() }

return DateInputFormat(pattern, delimiter)
return datePatternAsInputFormat(pattern)
}

@Suppress("UNCHECKED_CAST")
actual fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>>? {
actual fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>> {
val formatter = NSDateFormatter().apply {
setLocale(locale)
}

val fromSundayToSaturday = formatter.standaloneWeekdaySymbols
.zip(formatter.shortStandaloneWeekdaySymbols) as List<Pair<String, String>>
.zip(formatter.veryShortStandaloneWeekdaySymbols) as List<Pair<String, String>>

return fromSundayToSaturday.drop(1) + fromSundayToSaturday.first()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package androidx.compose.material3

import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.DayOfWeek
import java.time.format.TextStyle
import java.util.Locale


internal actual object PlatformDateFormat {
Expand Down Expand Up @@ -62,17 +65,26 @@ internal actual object PlatformDateFormat {
date: String,
pattern: String
): CalendarDate? {


return delegate.parse(date, pattern)
}

actual fun getDateInputFormat(locale: CalendarLocale): DateInputFormat {
return delegate.getDateInputFormat(locale)
}

actual fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>>? {
return delegate.weekdayNames(locale)
// From CalendarModelImpl.android.kt weekdayNames.
//
// Legacy model returns short ('Mon') format while newer version returns narrow ('M') format
actual fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>> {
return DayOfWeek.values().map {
it.getDisplayName(
TextStyle.FULL,
locale
) to it.getDisplayName(
TextStyle.NARROW,
locale
)
}
}

// https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/text/format/DateFormat.java
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.material3

import com.google.common.truth.Truth
import org.junit.Test

class EqualityOfCalendarModelsTest {

// Copy of Android CalendarModelTest.equalModelsOutput
// with KotlinxDatetimeCalendarModel and different time zones.
// Ensures that models have the same implementation
@Test
fun equalModelsOutput() {
// Note: This test ignores the parameters and just runs a few equality tests for the output.
// It will execute twice, but that should to tolerable :)
val newModel = KotlinxDatetimeCalendarModel()
val legacyModel = LegacyCalendarModelImpl()

val defaultTZ = getTimeZone()

listOf("GMT-12", "GMT-5", "GMT+5", "GMT+12").forEach {

setTimeZone(it)

val date = newModel.getCanonicalDate(January2022Millis) // 1/1/2022
val legacyDate = legacyModel.getCanonicalDate(January2022Millis)
val month = newModel.getMonth(date)
val legacyMonth = legacyModel.getMonth(date)

Truth.assertThat(newModel.today).isEqualTo(legacyModel.today)
Truth.assertThat(month).isEqualTo(legacyMonth)
Truth.assertThat(newModel.getDateInputFormat())
.isEqualTo(legacyModel.getDateInputFormat())
Truth.assertThat(newModel.plusMonths(month, 3))
.isEqualTo(legacyModel.plusMonths(month, 3))
Truth.assertThat(date).isEqualTo(legacyDate)
Truth.assertThat(newModel.getDayOfWeek(date)).isEqualTo(legacyModel.getDayOfWeek(date))
if (supportsDateSkeleton) {
Truth.assertThat(newModel.formatWithSkeleton(date, "MMM d, yyyy")).isEqualTo(
legacyModel.formatWithSkeleton(
date,
"MMM d, yyyy"
)
)
Truth.assertThat(newModel.formatWithSkeleton(month, "MMM yyyy")).isEqualTo(
legacyModel.formatWithSkeleton(
month,
"MMM yyyy"
)
)
}
}

setTimeZone(defaultTZ)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import kotlin.js.Date
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime

internal actual object PlatformDateFormat {
Expand Down Expand Up @@ -151,7 +152,8 @@ internal actual object PlatformDateFormat {

return LocalDate(
year, month, day
).atStartOfDayIn(TimeZone.UTC)
).atTime(hour = 0, minute = 0)
.toInstant(TimeZone.UTC)
.toCalendarDate(TimeZone.UTC)
}

Expand All @@ -170,17 +172,15 @@ internal actual object PlatformDateFormat {

val shortDate = date.toLocaleDateString(locale.toLanguageTag())

val delimiter = shortDate.first { !it.isDigit() }

val pattern = shortDate
.replace("2000", "yyyy")
.replace("11", "MM") //10 -> 11 not an error. month is index
.replace("23", "dd")

return DateInputFormat(pattern, delimiter)
return datePatternAsInputFormat(pattern)
}

actual fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>>? {
actual fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>> {
val now = Date.now()

val week = List(DaysInWeek) {
Expand All @@ -189,7 +189,7 @@ internal actual object PlatformDateFormat {

val mondayToSunday = week.drop(1) + week.first()

val longAndShortWeekDays = listOf(LONG, SHORT).map { format ->
val longAndShortWeekDays = listOf(LONG, NARROW).map { format ->
mondayToSunday.map {
it.toLocaleDateString(
locales = locale.toLanguageTag(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,33 @@ import kotlinx.datetime.Clock
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.isoDayNumber
import kotlinx.datetime.plus
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime

internal class KotlinxDatetimeCalendarModel : CalendarModel {

private val midnight = LocalTime(0,0)
alexzhirkevich marked this conversation as resolved.
Show resolved Hide resolved

override val today: CalendarDate
get() = Clock.System.now().toCalendarDate(systemTZ)
get() {
val localDate = Clock.System.now().toLocalDateTime(systemTZ)
return CalendarDate(
year = localDate.year,
month = localDate.monthNumber,
dayOfMonth = localDate.dayOfMonth,
utcTimeMillis = localDate.date
.atTime(midnight)
.toInstant(TimeZone.UTC)
.toEpochMilliseconds()
)
}

override val firstDayOfWeek: Int
get() = PlatformDateFormat.firstDayOfWeek
Expand All @@ -42,13 +58,12 @@ internal class KotlinxDatetimeCalendarModel : CalendarModel {
get() = TimeZone.currentSystemDefault()

fun weekdayNames(locale: CalendarLocale): List<Pair<String, String>> {
return PlatformDateFormat.weekdayNames(locale) ?: EnglishWeekdaysNames
return PlatformDateFormat.weekdayNames(locale)
}

override fun getDateInputFormat(locale: CalendarLocale): DateInputFormat {
return PlatformDateFormat
.getDateInputFormat(locale)
.applyCalendarModelStyle()
}

override fun getCanonicalDate(timeInMillis: Long): CalendarDate {
Expand All @@ -75,16 +90,18 @@ internal class KotlinxDatetimeCalendarModel : CalendarModel {
year = year,
monthNumber = month,
dayOfMonth = 1,
).atStartOfDayIn(TimeZone.UTC)
).atTime(midnight)
.toInstant(TimeZone.UTC)

return getMonth(instant.toEpochMilliseconds())
}

override fun getDayOfWeek(date: CalendarDate): Int {
return Instant
.fromEpochMilliseconds(date.utcTimeMillis)
.toLocalDateTime(systemTZ)
.dayOfWeek.isoDayNumber
return LocalDate(
year = date.year,
monthNumber = date.month,
dayOfMonth = date.dayOfMonth
).dayOfWeek.isoDayNumber
}

override fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth {
Expand All @@ -93,7 +110,8 @@ internal class KotlinxDatetimeCalendarModel : CalendarModel {
.toLocalDateTime(TimeZone.UTC)
.date
.plus(DatePeriod(months = addedMonthsCount))
.atStartOfDayIn(TimeZone.UTC)
.atTime(midnight)
.toInstant(TimeZone.UTC)
.toCalendarMonth(TimeZone.UTC)
}

Expand Down Expand Up @@ -133,64 +151,16 @@ internal class KotlinxDatetimeCalendarModel : CalendarModel {
daysFromStartOfWeekToFirstOfMonth = monthStart
.daysFromStartOfWeekToFirstOfMonth(),
startUtcTimeMillis = monthStart
.atStartOfDayIn(TimeZone.UTC)
.atTime(midnight)
.toInstant(TimeZone.UTC)
.toEpochMilliseconds()
)
}

/**
* Applies some specific rules to fit the Android one
* */
private fun DateInputFormat.applyCalendarModelStyle() : DateInputFormat {

var pattern = patternWithDelimiters
// the following checks are the result of testing

// most of time dateFormat returns dd.MM.y -> we need dd.MM.yyyy
if (!pattern.contains("yyyy", true)) {

// it can also return dd.MM.yy because such formats exist so check for it
while (pattern.contains("yy", true)) {
pattern = pattern.replace("yy", "y",true)
}

pattern = pattern.replace("y", "yyyy",true)
}

// it can return M.d -> we need MM.dd
if ("MM" !in pattern){
pattern = pattern.replace("M","MM")
}
if ("dd" !in pattern){
pattern = pattern.replace("d", "dd")
}

// it can return "yyyy. MM. dd."
pattern = pattern
.dropWhile { !it.isLetter() } // remove prefix non-letters
.dropLastWhile { !it.isLetter() } // remove suffix non-letters
.filter { it != ' ' } // remove whitespaces

val delimiter = pattern.first { !it.isLetter() }

return DateInputFormat(pattern, delimiter)
}


private fun LocalDate.daysFromStartOfWeekToFirstOfMonth() =
(dayOfWeek.isoDayNumber - firstDayOfWeek).let { if (it > 0) it else 7 + it }
}

private val EnglishWeekdaysNames = listOf(
"Monday" to "Mon",
"Tuesday" to "Tue",
"Wednesday" to "Wed",
"Thursday" to "Thu",
"Friday" to "Fri",
"Saturday" to "Sat",
"Sunday" to "Sun",
)

internal fun Instant.toCalendarDate(
timeZone : TimeZone
) : CalendarDate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ internal expect object PlatformDateFormat{

/**
* Localized by platform weekdays
* or null if platform does not support weekdays localization
* */
fun weekdayNames(locale: CalendarLocale) : List<Pair<String, String>>?
fun weekdayNames(locale: CalendarLocale) : List<Pair<String, String>>

fun formatWithPattern(
utcTimeMillis: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,4 @@ internal class KotlinxDatetimeCalendarModelTest {
}
}

private const val January2022Millis = 1640995200000
internal const val January2022Millis = 1640995200000