Skip to content

Commit

Permalink
DatePicker. Fix today circle (#782)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzhirkevich authored Sep 5, 2023
1 parent 8d6b3f9 commit 8ee0520
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 78 deletions.
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(Midnight)
.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,31 @@ 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 {

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 +56,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 +88,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 +108,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 +149,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 All @@ -205,6 +173,8 @@ internal fun Instant.toCalendarDate(
)
}

internal val Midnight = LocalTime(0,0)

private fun Int.isLeapYear() = this % 4 == 0 && (this % 100 != 0 || this % 400 == 0)

private fun Month.numberOfDays(isLeap : Boolean) : Int {
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

0 comments on commit 8ee0520

Please sign in to comment.