blob: 3f792fc8c469f23f0faaff5973c27ddd1a3e382d [file] [log] [blame]
/*
* 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<Int> = 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<Int, Int>
init {
val map: MutableMap<Int, Int> = 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()
}
}