[go: nahoru, domu]

Merge "Adds a model to be used internally by date pickers" into androidx-main
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index bc57514..f96b55a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -103,6 +103,9 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -681,6 +684,9 @@
 
 package androidx.compose.material3.internal {
 
+  public final class AndroidDatePickerModel_androidKt {
+  }
+
   public final class ExposedDropdownMenuPopupKt {
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 906e393..c815b0b 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -132,6 +132,9 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -1013,6 +1016,9 @@
 
 package androidx.compose.material3.internal {
 
+  public final class AndroidDatePickerModel_androidKt {
+  }
+
   public final class ExposedDropdownMenuPopupKt {
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index bc57514..f96b55a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -103,6 +103,9 @@
     method @androidx.compose.runtime.Composable public static void TextButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ButtonColors colors, optional androidx.compose.material3.ButtonElevation? elevation, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
+  public final class CalendarModelKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class CardColors {
   }
 
@@ -681,6 +684,9 @@
 
 package androidx.compose.material3.internal {
 
+  public final class AndroidDatePickerModel_androidKt {
+  }
+
   public final class ExposedDropdownMenuPopupKt {
   }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
new file mode 100644
index 0000000..9b5997e
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/CalendarModelTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2022 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 android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.material3.internal.CalendarModelImpl
+import androidx.compose.material3.internal.LegacyCalendarModelImpl
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import java.util.Locale
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+@OptIn(ExperimentalMaterial3Api::class)
+@RequiresApi(Build.VERSION_CODES.O)
+internal class CalendarModelTest(private val model: CalendarModel) {
+
+    @Test
+    fun dateCreation() {
+        val date = model.getDate(January2022Millis) // 1/1/2022
+        assertThat(date.year).isEqualTo(2022)
+        assertThat(date.month).isEqualTo(1)
+        assertThat(date.dayOfMonth).isEqualTo(1)
+        assertThat(date.utcTimeMillis).isEqualTo(January2022Millis)
+    }
+
+    @Test
+    fun dateRestore() {
+        val date =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        assertThat(model.getDate(date.utcTimeMillis)).isEqualTo(date)
+    }
+
+    @Test
+    fun monthCreation() {
+        val date =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        val monthFromDate = model.getMonth(date)
+        val monthFromMilli = model.getMonth(January2022Millis)
+        val monthFromYearMonth = model.getMonth(year = 2022, month = 1)
+        assertThat(monthFromDate).isEqualTo(monthFromMilli)
+        assertThat(monthFromDate).isEqualTo(monthFromYearMonth)
+    }
+
+    @Test
+    fun monthRestore() {
+        val month = model.getMonth(year = 1999, month = 12)
+        assertThat(model.getMonth(month.startUtcTimeMillis)).isEqualTo(month)
+    }
+
+    @Test
+    fun plusMinusMonth() {
+        val month = model.getMonth(January2022Millis) // 1/1/2022
+        val expectedNextMonth = model.getMonth(month.endUtcTimeMillis + 1) // 2/1/2022
+        val plusMonth = model.plusMonths(from = month, addedMonthsCount = 1)
+        assertThat(plusMonth).isEqualTo(expectedNextMonth)
+        assertThat(model.minusMonths(from = plusMonth, subtractedMonthsCount = 1)).isEqualTo(month)
+    }
+
+    @Test
+    fun parseDate() {
+        val expectedDate =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        val parsedDate = model.parse("1/1/2022", "M/d/yyyy")
+        assertThat(parsedDate).isEqualTo(expectedDate)
+    }
+
+    @Test
+    fun formatDate() {
+        val date =
+            CalendarDate(year = 2022, month = 1, dayOfMonth = 1, utcTimeMillis = January2022Millis)
+        val month = model.plusMonths(model.getMonth(date), 2)
+        assertThat(model.format(date, "MM/dd/yyyy")).isEqualTo("01/01/2022")
+        assertThat(model.format(month, "MM/dd/yyyy")).isEqualTo("03/01/2022")
+    }
+
+    @Test
+    fun weekdayNames() {
+        // Ensure we are running on a US locale for this test.
+        Locale.setDefault(Locale.US)
+        val weekDays = model.weekdayNames
+        assertThat(weekDays).hasSize(DaysInWeek)
+        // Check that the first day is always "Monday", per ISO-8601 standard.
+        assertThat(weekDays.first().first).ignoringCase().contains("Monday")
+        weekDays.forEach {
+            assertThat(it.second.first().lowercaseChar()).isEqualTo(
+                it.first.first().lowercaseChar()
+            )
+        }
+    }
+
+    @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 = CalendarModelImpl()
+        val legacyModel = LegacyCalendarModelImpl()
+
+        val date = newModel.getDate(January2022Millis) // 1/1/2022
+        val legacyDate = legacyModel.getDate(January2022Millis)
+        val month = newModel.getMonth(date)
+        val legacyMonth = legacyModel.getMonth(date)
+
+        assertThat(newModel.today).isEqualTo(legacyModel.today)
+        assertThat(month).isEqualTo(legacyMonth)
+        assertThat(newModel.plusMonths(month, 3)).isEqualTo(legacyModel.plusMonths(month, 3))
+        assertThat(date).isEqualTo(legacyDate)
+        assertThat(newModel.getDayOfWeek(date)).isEqualTo(legacyModel.getDayOfWeek(date))
+        assertThat(newModel.format(date, "MMM d, yyyy")).isEqualTo(
+            legacyModel.format(
+                date,
+                "MMM d, yyyy"
+            )
+        )
+        assertThat(newModel.format(month, "MMM yyyy")).isEqualTo(
+            legacyModel.format(
+                month,
+                "MMM yyyy"
+            )
+        )
+    }
+
+    internal companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            CalendarModelImpl(),
+            LegacyCalendarModelImpl()
+        )
+    }
+}
+
+private const val January2022Millis = 1640995200000
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AndroidDatePickerModel.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AndroidDatePickerModel.android.kt
new file mode 100644
index 0000000..089aeb0
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/internal/AndroidDatePickerModel.android.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2022 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.internal
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.compose.material3.CalendarDate
+import androidx.compose.material3.CalendarModel
+import androidx.compose.material3.CalendarMonth
+import androidx.compose.material3.DaysInWeek
+import androidx.compose.material3.ExperimentalMaterial3Api
+import java.text.DateFormatSymbols
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.time.DayOfWeek
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
+import java.time.format.TextStyle
+import java.time.temporal.WeekFields
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * Creates a [CalendarModel] to be used by the date picker.
+ */
+@ExperimentalMaterial3Api
+internal fun createDefaultCalendarModel(): CalendarModel {
+    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        CalendarModelImpl()
+    } else {
+        LegacyCalendarModelImpl()
+    }
+}
+
+/**
+ * A [CalendarModel] implementation for API < 26.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+internal class LegacyCalendarModelImpl : CalendarModel {
+
+    override val today
+        get(): CalendarDate {
+            val systemCalendar = Calendar.getInstance()
+            systemCalendar[Calendar.HOUR_OF_DAY] = 0
+            systemCalendar[Calendar.MINUTE] = 0
+            systemCalendar[Calendar.SECOND] = 0
+            systemCalendar[Calendar.MILLISECOND] = 0
+            val utcOffset =
+                systemCalendar.get(Calendar.ZONE_OFFSET) + systemCalendar.get(Calendar.DST_OFFSET)
+            return CalendarDate(
+                year = systemCalendar[Calendar.YEAR],
+                month = systemCalendar[Calendar.MONTH] + 1,
+                dayOfMonth = systemCalendar[Calendar.DAY_OF_MONTH],
+                utcTimeMillis = systemCalendar.timeInMillis + utcOffset
+            )
+        }
+
+    override val firstDayOfWeek: Int = dayInISO8601(Calendar.getInstance().firstDayOfWeek)
+
+    override val weekdayNames: List<Pair<String, String>> = buildList {
+        val weekdays = DateFormatSymbols(Locale.getDefault()).weekdays
+        val shortWeekdays = DateFormatSymbols(Locale.getDefault()).shortWeekdays
+        // Skip the first item, as it's empty, and the second item, as it represents Sunday while it
+        // should be last according to ISO-8601.
+        weekdays.drop(2).forEachIndexed { index, day ->
+            add(Pair(day, shortWeekdays[index + 2]))
+        }
+        // Add Sunday to the end.
+        add(Pair(weekdays[1], shortWeekdays[1]))
+    }
+
+    override fun getDate(timeInMillis: Long): CalendarDate {
+        val calendar = Calendar.getInstance(utcTimeZone)
+        calendar.timeInMillis = timeInMillis
+        return CalendarDate(
+            year = calendar[Calendar.YEAR],
+            month = calendar[Calendar.MONTH] + 1,
+            dayOfMonth = calendar[Calendar.DAY_OF_MONTH],
+            utcTimeMillis = timeInMillis
+        )
+    }
+
+    override fun getMonth(timeInMillis: Long): CalendarMonth {
+        val firstDayCalendar = Calendar.getInstance(utcTimeZone)
+        firstDayCalendar.timeInMillis = timeInMillis
+        firstDayCalendar[Calendar.DAY_OF_MONTH] = 1
+        return getMonth(firstDayCalendar)
+    }
+
+    override fun getMonth(date: CalendarDate): CalendarMonth {
+        return getMonth(date.year, date.month)
+    }
+
+    override fun getMonth(year: Int, month: Int): CalendarMonth {
+        val firstDayCalendar = Calendar.getInstance(utcTimeZone)
+        firstDayCalendar.clear()
+        firstDayCalendar[Calendar.YEAR] = year
+        firstDayCalendar[Calendar.MONTH] = month - 1
+        firstDayCalendar[Calendar.DAY_OF_MONTH] = 1
+        return getMonth(firstDayCalendar)
+    }
+
+    override fun getDayOfWeek(date: CalendarDate): Int {
+        return dayInISO8601(date.toCalendar(TimeZone.getDefault())[Calendar.DAY_OF_WEEK])
+    }
+
+    override fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth {
+        if (addedMonthsCount <= 0) return from
+
+        val laterMonth = from.toCalendar()
+        laterMonth.add(Calendar.MONTH, addedMonthsCount)
+        return getMonth(laterMonth)
+    }
+
+    override fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth {
+        if (subtractedMonthsCount <= 0) return from
+
+        val earlierMonth = from.toCalendar()
+        earlierMonth.add(Calendar.MONTH, -subtractedMonthsCount)
+        return getMonth(earlierMonth)
+    }
+
+    override fun format(month: CalendarMonth, pattern: String): String {
+        val dateFormat = SimpleDateFormat(pattern, Locale.getDefault())
+        dateFormat.timeZone = utcTimeZone
+        dateFormat.isLenient = false
+        return dateFormat.format(month.toCalendar().timeInMillis)
+    }
+
+    override fun format(date: CalendarDate, pattern: String): String {
+        val dateFormat = SimpleDateFormat(pattern, Locale.getDefault())
+        dateFormat.timeZone = utcTimeZone
+        dateFormat.isLenient = false
+        return dateFormat.format(date.toCalendar(utcTimeZone).timeInMillis)
+    }
+
+    override fun parse(date: String, pattern: String): CalendarDate? {
+        val dateFormat = SimpleDateFormat(pattern)
+        dateFormat.timeZone = utcTimeZone
+        dateFormat.isLenient = false
+        return try {
+            val parsedDate = dateFormat.parse(date) ?: return null
+            val calendar = Calendar.getInstance(utcTimeZone)
+            calendar.time = parsedDate
+            CalendarDate(
+                year = calendar[Calendar.YEAR],
+                month = calendar[Calendar.MONTH] + 1,
+                dayOfMonth = calendar[Calendar.DAY_OF_MONTH],
+                utcTimeMillis = calendar.timeInMillis
+            )
+        } catch (pe: ParseException) {
+            null
+        }
+    }
+
+    /**
+     * Returns a given [Calendar] day number as a day representation under ISO-8601, where the first
+     * day is defined as Monday.
+     */
+    private fun dayInISO8601(day: Int): Int {
+        val shiftedDay = (day + 6) % 7
+        return if (shiftedDay == 0) return /* Sunday */ 7 else shiftedDay
+    }
+
+    private fun getMonth(firstDayCalendar: Calendar): CalendarMonth {
+        val difference = dayInISO8601(firstDayCalendar[Calendar.DAY_OF_WEEK]) - firstDayOfWeek
+        val daysFromStartOfWeekToFirstOfMonth = if (difference < 0) {
+            difference + DaysInWeek
+        } else {
+            difference
+        }
+        return CalendarMonth(
+            year = firstDayCalendar[Calendar.YEAR],
+            month = firstDayCalendar[Calendar.MONTH] + 1,
+            numberOfDays = firstDayCalendar.getActualMaximum(Calendar.DAY_OF_MONTH),
+            daysFromStartOfWeekToFirstOfMonth = daysFromStartOfWeekToFirstOfMonth,
+            startUtcTimeMillis = firstDayCalendar.timeInMillis
+        )
+    }
+
+    private fun CalendarMonth.toCalendar(): Calendar {
+        val calendar = Calendar.getInstance(utcTimeZone)
+        calendar.timeInMillis = this.startUtcTimeMillis
+        return calendar
+    }
+
+    private fun CalendarDate.toCalendar(timeZone: TimeZone): Calendar {
+        val calendar = Calendar.getInstance(timeZone)
+        calendar.clear()
+        calendar[Calendar.YEAR] = this.year
+        calendar[Calendar.MONTH] = this.month - 1
+        calendar[Calendar.DAY_OF_MONTH] = this.dayOfMonth
+        return calendar
+    }
+
+    private var utcTimeZone = TimeZone.getTimeZone("UTC")
+}
+
+/**
+ * A [CalendarModel] implementation for API >= 26.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@RequiresApi(Build.VERSION_CODES.O)
+internal class CalendarModelImpl : CalendarModel {
+
+    override val today
+        get(): CalendarDate {
+            val systemLocalDate = LocalDate.now()
+            return CalendarDate(
+                year = systemLocalDate.year,
+                month = systemLocalDate.monthValue,
+                dayOfMonth = systemLocalDate.dayOfMonth,
+                utcTimeMillis = systemLocalDate.atTime(LocalTime.MIDNIGHT)
+                    .atZone(utcTimeZoneId).toInstant().toEpochMilli()
+            )
+        }
+
+    override val firstDayOfWeek: Int = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value
+
+    override val weekdayNames: List<Pair<String, String>> =
+        // This will start with Monday as the first day, according to ISO-8601.
+        with(Locale.getDefault()) {
+            DayOfWeek.values().map {
+                it.getDisplayName(
+                    TextStyle.FULL,
+                    /* locale = */ this
+                ) to it.getDisplayName(
+                    TextStyle.NARROW,
+                    /* locale = */ this
+                )
+            }
+        }
+
+    override fun getDate(timeInMillis: Long): CalendarDate {
+        val localDate =
+            Instant.ofEpochMilli(timeInMillis).atZone(utcTimeZoneId).toLocalDate()
+        return CalendarDate(
+            year = localDate.year,
+            month = localDate.monthValue,
+            dayOfMonth = localDate.dayOfMonth,
+            utcTimeMillis = timeInMillis
+        )
+    }
+
+    override fun getMonth(timeInMillis: Long): CalendarMonth {
+        return getMonth(
+            Instant.ofEpochMilli(timeInMillis).atZone(utcTimeZoneId).toLocalDate()
+        )
+    }
+
+    override fun getMonth(date: CalendarDate): CalendarMonth {
+        return getMonth(LocalDate.of(date.year, date.month, 1))
+    }
+
+    override fun getMonth(year: Int, month: Int): CalendarMonth {
+        return getMonth(LocalDate.of(year, month, 1))
+    }
+
+    override fun getDayOfWeek(date: CalendarDate): Int {
+        return date.toLocalDate().dayOfWeek.value
+    }
+
+    override fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth {
+        if (addedMonthsCount <= 0) return from
+
+        val firstDayLocalDate = from.toLocalDate()
+        val laterMonth = firstDayLocalDate.plusMonths(addedMonthsCount.toLong())
+        return getMonth(laterMonth)
+    }
+
+    override fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth {
+        if (subtractedMonthsCount <= 0) return from
+
+        val firstDayLocalDate = from.toLocalDate()
+        val earlierMonth = firstDayLocalDate.minusMonths(subtractedMonthsCount.toLong())
+        return getMonth(earlierMonth)
+    }
+
+    override fun format(month: CalendarMonth, pattern: String): String {
+        val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(pattern)
+        return month.toLocalDate().format(formatter)
+    }
+
+    override fun format(date: CalendarDate, pattern: String): String {
+        val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(pattern)
+        return date.toLocalDate().format(formatter)
+    }
+
+    override fun parse(date: String, pattern: String): CalendarDate? {
+        // TODO: A DateTimeFormatter can be reused.
+        val formatter = DateTimeFormatter.ofPattern(pattern)
+        return try {
+            val localDate = LocalDate.parse(date, formatter)
+            CalendarDate(
+                year = localDate.year,
+                month = localDate.month.value,
+                dayOfMonth = localDate.dayOfMonth,
+                utcTimeMillis = localDate.atTime(LocalTime.MIDNIGHT)
+                    .atZone(utcTimeZoneId).toInstant().toEpochMilli()
+            )
+        } catch (pe: DateTimeParseException) {
+            null
+        }
+    }
+
+    private fun getMonth(firstDayLocalDate: LocalDate): CalendarMonth {
+        val difference = firstDayLocalDate.dayOfWeek.value - firstDayOfWeek
+        val daysFromStartOfWeekToFirstOfMonth = if (difference < 0) {
+            difference + DaysInWeek
+        } else {
+            difference
+        }
+        val firstDayEpochMillis =
+            firstDayLocalDate.atTime(LocalTime.MIDNIGHT).atZone(utcTimeZoneId).toInstant()
+                .toEpochMilli()
+        return CalendarMonth(
+            year = firstDayLocalDate.year,
+            month = firstDayLocalDate.monthValue,
+            numberOfDays = firstDayLocalDate.lengthOfMonth(),
+            daysFromStartOfWeekToFirstOfMonth = daysFromStartOfWeekToFirstOfMonth,
+            startUtcTimeMillis = firstDayEpochMillis
+        )
+    }
+
+    private fun CalendarMonth.toLocalDate(): LocalDate {
+        return Instant.ofEpochMilli(startUtcTimeMillis).atZone(utcTimeZoneId).toLocalDate()
+    }
+
+    private fun CalendarDate.toLocalDate(): LocalDate {
+        return LocalDate.of(
+            this.year,
+            this.month,
+            this.dayOfMonth
+        )
+    }
+
+    private val utcTimeZoneId = ZoneId.of("UTC")
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
new file mode 100644
index 0000000..54cbd7b
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/CalendarModel.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2022 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
+
+@ExperimentalMaterial3Api
+internal interface CalendarModel {
+
+    /**
+     * A [CalendarDate] representing the current day.
+     */
+    val today: CalendarDate
+
+    /**
+     * Hold the first day of the week at the current `Locale` as an integer. The integer value
+     * follows the ISO-8601 standard and refer to Monday as 1, and Sunday as 7.
+     */
+    val firstDayOfWeek: Int
+
+    /**
+     * Holds a list of weekday names, starting from Monday as the first day in the list.
+     *
+     * Each item in this list is a [Pair] that holds the full name of the day, and its short
+     * abbreviation letter(s).
+     *
+     * Newer APIs (i.e. API 26+), a [Pair] will hold a full name and the first letter of the
+     * day.
+     * Older APIs that predate API 26 will hold a full name and the first three letters of the day.
+     */
+    val weekdayNames: List<Pair<String, String>>
+
+    /**
+     * Returns a [CalendarDate] from a given _UTC_ time in milliseconds.
+     *
+     * @param timeInMillis UTC milliseconds from the epoch
+     */
+    fun getDate(timeInMillis: Long): CalendarDate
+
+    /**
+     * Returns a [CalendarMonth] from a given _UTC_ time in milliseconds.
+     *
+     * @param timeInMillis UTC milliseconds from the epoch for the first day the month
+     */
+    fun getMonth(timeInMillis: Long): CalendarMonth
+
+    /**
+     * Returns a [CalendarMonth] from a given [CalendarDate].
+     *
+     * Note: This function ignores the [CalendarDate.dayOfMonth] value and just uses the date's
+     * year and month to resolve a [CalendarMonth].
+     *
+     * @param date a [CalendarDate] to resolve into a month
+     */
+    fun getMonth(date: CalendarDate): CalendarMonth
+
+    /**
+     * Returns a [CalendarMonth] from a given [year] and [month].
+     *
+     * @param year the month's year
+     * @param month an integer representing a month (e.g. JANUARY as 1, December as 12)
+     */
+    fun getMonth(year: Int, /* @IntRange(from = 1, to = 12) */ month: Int): CalendarMonth
+
+    /**
+     * Returns a day of week from a given [CalendarDate].
+     *
+     * @param date a [CalendarDate] to resolve
+     */
+    fun getDayOfWeek(date: CalendarDate): Int
+
+    /**
+     * Returns a [CalendarMonth] that is computed by adding a number of months, given as
+     * [addedMonthsCount], to a given month.
+     *
+     * @param from the [CalendarMonth] to add to
+     * @param addedMonthsCount the number of months to add
+     */
+    fun plusMonths(from: CalendarMonth, addedMonthsCount: Int): CalendarMonth
+
+    /**
+     * Returns a [CalendarMonth] that is computed by subtracting a number of months, given as
+     * [subtractedMonthsCount], from a given month.
+     *
+     * @param from the [CalendarMonth] to subtract from
+     * @param subtractedMonthsCount the number of months to subtract
+     */
+    fun minusMonths(from: CalendarMonth, subtractedMonthsCount: Int): CalendarMonth
+
+    /**
+     * Formats a [CalendarMonth] into a string with a given date format pattern.
+     *
+     * @param month a [CalendarMonth] to format
+     * @param pattern a date format pattern
+     */
+    fun format(month: CalendarMonth, pattern: String): String
+
+    /**
+     * Formats a [CalendarDate] into a string with a given date format pattern.
+     *
+     * @param date a [CalendarDate] to format
+     * @param pattern a date format pattern
+     */
+    fun format(date: CalendarDate, pattern: String): String
+
+    /**
+     * Parses a date string into a [CalendarDate].
+     *
+     * @param date a date string
+     * @param pattern the expected date pattern to be used for parsing the date string
+     * @return a [CalendarDate], or a `null` in case the parsing failed
+     */
+    fun parse(date: String, pattern: String): CalendarDate?
+}
+
+/**
+ * Represents a calendar date.
+ *
+ * @param year the date's year
+ * @param month the date's month
+ * @param dayOfMonth the date's day of month
+ * @param utcTimeMillis the date representation in _UTC_ milliseconds from the epoch
+ */
+@ExperimentalMaterial3Api
+internal data class CalendarDate(
+    val year: Int,
+    val month: Int,
+    val dayOfMonth: Int,
+    val utcTimeMillis: Long
+) : Comparable<CalendarDate> {
+    override operator fun compareTo(other: CalendarDate): Int =
+        this.utcTimeMillis.compareTo(other.utcTimeMillis)
+}
+
+/**
+ * Represents a calendar month.
+ *
+ * @param year the month's year
+ * @param month the calendar month as an integer (e.g. JANUARY as 1, December as 12)
+ * @param numberOfDays the number of days in the month
+ * @param daysFromStartOfWeekToFirstOfMonth the number of days from the start of the week to the
+ * first day of the month
+ * @param startUtcTimeMillis the first day of the month in _UTC_ milliseconds from the epoch
+ */
+@ExperimentalMaterial3Api
+internal data class CalendarMonth(
+    val year: Int,
+    val month: Int,
+    val numberOfDays: Int,
+    val daysFromStartOfWeekToFirstOfMonth: Int,
+    val startUtcTimeMillis: Long
+) {
+
+    /**
+     * The last _UTC_ milliseconds from the epoch of the month (i.e. the last millisecond of the
+     * last day of the month)
+     */
+    val endUtcTimeMillis: Long = startUtcTimeMillis + (numberOfDays * MillisecondsIn24Hours) - 1
+
+    /**
+     * Returns the position of a [CalendarMonth] within given years range.
+     */
+    internal fun indexIn(years: IntRange): Int {
+        return (year - years.first) * 12 + month - 1
+    }
+}
+
+internal const val DaysInWeek: Int = 7
+internal const val MillisecondsIn24Hours = 86400000L