/* * Copyright (C) 2020 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 com.android.deskclock.data import android.content.Context import androidx.annotation.VisibleForTesting import com.android.deskclock.R import java.text.DateFormatSymbols import java.util.Calendar /** * This class is responsible for encoding a weekly repeat cycle in a [bitset][.getBits]. It * also converts between those bits and the [Calendar.DAY_OF_WEEK] values for easier mutation * and querying. */ class Weekdays private constructor(bits: Int) { /** * The preferred starting day of the week can differ by locale. This enumerated value is used to * describe the preferred ordering. */ enum class Order(vararg calendarDays: Int) { SAT_TO_FRI(Calendar.SATURDAY, Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY), SUN_TO_SAT(Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY), MON_TO_SUN(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY); val calendarDays: List = calendarDays.asList() } companion object { /** All valid bits set. */ private const val ALL_DAYS = 0x7F /** An instance with all weekdays in the weekly repeat cycle. */ @JvmField val ALL = fromBits(ALL_DAYS) /** An instance with no weekdays in the weekly repeat cycle. */ @JvmField val NONE = fromBits(0) /** Maps calendar weekdays to the bit masks that represent them in this class. */ private val sCalendarDayToBit: Map init { val map: MutableMap = mutableMapOf() map[Calendar.MONDAY] = 0x01 map[Calendar.TUESDAY] = 0x02 map[Calendar.WEDNESDAY] = 0x04 map[Calendar.THURSDAY] = 0x08 map[Calendar.FRIDAY] = 0x10 map[Calendar.SATURDAY] = 0x20 map[Calendar.SUNDAY] = 0x40 sCalendarDayToBit = map } /** * @param bits [bits][.getBits] representing the encoded weekly repeat schedule * @return a Weekdays instance representing the same repeat schedule as the `bits` */ @JvmStatic fun fromBits(bits: Int): Weekdays { return Weekdays(bits) } /** * @param calendarDays an array containing any or all of the following values * * * [Calendar.SUNDAY] * * [Calendar.MONDAY] * * [Calendar.TUESDAY] * * [Calendar.WEDNESDAY] * * [Calendar.THURSDAY] * * [Calendar.FRIDAY] * * [Calendar.SATURDAY] * * @return a Weekdays instance representing the given `calendarDays` */ @JvmStatic fun fromCalendarDays(vararg calendarDays: Int): Weekdays { var bits = 0 for (calendarDay in calendarDays) { val bit = sCalendarDayToBit[calendarDay] if (bit != null) { bits = bits or bit } } return Weekdays(bits) } } /** An encoded form of a weekly repeat schedule. */ val bits: Int = ALL_DAYS and bits /** * @param calendarDay any of the following values * * * [Calendar.SUNDAY] * * [Calendar.MONDAY] * * [Calendar.TUESDAY] * * [Calendar.WEDNESDAY] * * [Calendar.THURSDAY] * * [Calendar.FRIDAY] * * [Calendar.SATURDAY] * * @param on `true` if the `calendarDay` is on; `false` otherwise * @return a WeekDays instance with the `calendarDay` mutated */ fun setBit(calendarDay: Int, on: Boolean): Weekdays { val bit = sCalendarDayToBit[calendarDay] ?: return this return Weekdays(if (on) bits or bit else bits and bit.inv()) } /** * @param calendarDay any of the following values * * * [Calendar.SUNDAY] * * [Calendar.MONDAY] * * [Calendar.TUESDAY] * * [Calendar.WEDNESDAY] * * [Calendar.THURSDAY] * * [Calendar.FRIDAY] * * [Calendar.SATURDAY] * * @return `true` if the given `calendarDay` */ fun isBitOn(calendarDay: Int): Boolean { val bit = sCalendarDayToBit[calendarDay] ?: throw IllegalArgumentException("$calendarDay is not a valid weekday") return bits and bit > 0 } /** * @return `true` iff at least one weekday is enabled in the repeat schedule */ val isRepeating: Boolean get() = bits != 0 /** * Note: only the day-of-week is read from the `time`. The time fields * are not considered in this computation. * * @param time a timestamp relative to which the answer is given * @return the number of days between the given `time` and the previous enabled weekday * which is always between 1 and 7 inclusive; `-1` if no weekdays are enabled */ fun getDistanceToPreviousDay(time: Calendar): Int { var calendarDay = time[Calendar.DAY_OF_WEEK] for (count in 1..7) { calendarDay-- if (calendarDay < Calendar.SUNDAY) { calendarDay = Calendar.SATURDAY } if (isBitOn(calendarDay)) { return count } } return -1 } /** * Note: only the day-of-week is read from the `time`. The time fields * are not considered in this computation. * * @param time a timestamp relative to which the answer is given * @return the number of days between the given `time` and the next enabled weekday which * is always between 0 and 6 inclusive; `-1` if no weekdays are enabled */ fun getDistanceToNextDay(time: Calendar): Int { var calendarDay = time[Calendar.DAY_OF_WEEK] for (count in 0..6) { if (isBitOn(calendarDay)) { return count } calendarDay++ if (calendarDay > Calendar.SATURDAY) { calendarDay = Calendar.SUNDAY } } return -1 } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false val weekdays = other as Weekdays return bits == weekdays.bits } override fun hashCode(): Int { return bits } override fun toString(): String { val builder = StringBuilder(19) builder.append("[") if (isBitOn(Calendar.MONDAY)) { builder.append(if (builder.length > 1) " M" else "M") } if (isBitOn(Calendar.TUESDAY)) { builder.append(if (builder.length > 1) " T" else "T") } if (isBitOn(Calendar.WEDNESDAY)) { builder.append(if (builder.length > 1) " W" else "W") } if (isBitOn(Calendar.THURSDAY)) { builder.append(if (builder.length > 1) " Th" else "Th") } if (isBitOn(Calendar.FRIDAY)) { builder.append(if (builder.length > 1) " F" else "F") } if (isBitOn(Calendar.SATURDAY)) { builder.append(if (builder.length > 1) " Sa" else "Sa") } if (isBitOn(Calendar.SUNDAY)) { builder.append(if (builder.length > 1) " Su" else "Su") } builder.append("]") return builder.toString() } /** * @param context for accessing resources * @param order the order in which to present the weekdays * @return the enabled weekdays in the given `order` */ fun toString(context: Context, order: Order): String { return toString(context, order, false /* forceLongNames */) } /** * @param context for accessing resources * @param order the order in which to present the weekdays * @return the enabled weekdays in the given `order` in a manner that * is most appropriate for talk-back */ fun toAccessibilityString(context: Context, order: Order): String { return toString(context, order, true /* forceLongNames */) } @get:VisibleForTesting val count: Int get() { var count = 0 for (calendarDay in Calendar.SUNDAY..Calendar.SATURDAY) { if (isBitOn(calendarDay)) { count++ } } return count } /** * @param context for accessing resources * @param order the order in which to present the weekdays * @param forceLongNames if `true` the un-abbreviated weekdays are used * @return the enabled weekdays in the given `order` */ private fun toString(context: Context, order: Order, forceLongNames: Boolean): String { if (!isRepeating) { return "" } if (bits == ALL_DAYS) { return context.getString(R.string.every_day) } val longNames = forceLongNames || count <= 1 val dfs = DateFormatSymbols() val weekdays = if (longNames) dfs.weekdays else dfs.shortWeekdays val separator: String = context.getString(R.string.day_concat) val builder = StringBuilder(40) for (calendarDay in order.calendarDays) { if (isBitOn(calendarDay)) { if (builder.isNotEmpty()) { builder.append(separator) } builder.append(weekdays[calendarDay]) } } return builder.toString() } }