LegacyCalendarModelImpl.kt

/*
 * 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 java.text.DateFormat
import java.text.DateFormatSymbols
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone

/**
 * 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 val dateInputFormat: DateInputFormat
        get() = datePatternAsInputFormat(
            (DateFormat.getDateInstance(
                DateFormat.SHORT,
                Locale.getDefault()
            ) as SimpleDateFormat).toPattern()
        )

    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")
}