1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock.data 18 19 import android.content.Context 20 import androidx.annotation.VisibleForTesting 21 22 import com.android.deskclock.R 23 24 import java.text.DateFormatSymbols 25 import java.util.Calendar 26 27 /** 28 * This class is responsible for encoding a weekly repeat cycle in a [bitset][.getBits]. It 29 * also converts between those bits and the [Calendar.DAY_OF_WEEK] values for easier mutation 30 * and querying. 31 */ 32 class Weekdays private constructor(bits: Int) { 33 /** 34 * The preferred starting day of the week can differ by locale. This enumerated value is used to 35 * describe the preferred ordering. 36 */ 37 enum class Order(vararg calendarDays: Int) { 38 SAT_TO_FRI(Calendar.SATURDAY, Calendar.SUNDAY, Calendar.MONDAY, 39 Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY), 40 SUN_TO_SAT(Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, 41 Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY), 42 MON_TO_SUN(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, 43 Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY); 44 45 val calendarDays: List<Int> = calendarDays.asList() 46 } 47 48 companion object { 49 /** All valid bits set. */ 50 private const val ALL_DAYS = 0x7F 51 52 /** An instance with all weekdays in the weekly repeat cycle. */ 53 @JvmField 54 val ALL = fromBits(ALL_DAYS) 55 56 /** An instance with no weekdays in the weekly repeat cycle. */ 57 @JvmField 58 val NONE = fromBits(0) 59 60 /** Maps calendar weekdays to the bit masks that represent them in this class. */ 61 private val sCalendarDayToBit: Map<Int, Int> 62 63 init { 64 val map: MutableMap<Int, Int> = mutableMapOf() 65 map[Calendar.MONDAY] = 0x01 66 map[Calendar.TUESDAY] = 0x02 67 map[Calendar.WEDNESDAY] = 0x04 68 map[Calendar.THURSDAY] = 0x08 69 map[Calendar.FRIDAY] = 0x10 70 map[Calendar.SATURDAY] = 0x20 71 map[Calendar.SUNDAY] = 0x40 72 sCalendarDayToBit = map 73 } 74 75 /** 76 * @param bits [bits][.getBits] representing the encoded weekly repeat schedule 77 * @return a Weekdays instance representing the same repeat schedule as the `bits` 78 */ 79 @JvmStatic fromBitsnull80 fun fromBits(bits: Int): Weekdays { 81 return Weekdays(bits) 82 } 83 84 /** 85 * @param calendarDays an array containing any or all of the following values 86 * 87 * * [Calendar.SUNDAY] 88 * * [Calendar.MONDAY] 89 * * [Calendar.TUESDAY] 90 * * [Calendar.WEDNESDAY] 91 * * [Calendar.THURSDAY] 92 * * [Calendar.FRIDAY] 93 * * [Calendar.SATURDAY] 94 * 95 * @return a Weekdays instance representing the given `calendarDays` 96 */ 97 @JvmStatic fromCalendarDaysnull98 fun fromCalendarDays(vararg calendarDays: Int): Weekdays { 99 var bits = 0 100 for (calendarDay in calendarDays) { 101 val bit = sCalendarDayToBit[calendarDay] 102 if (bit != null) { 103 bits = bits or bit 104 } 105 } 106 return Weekdays(bits) 107 } 108 } 109 110 /** An encoded form of a weekly repeat schedule. */ 111 val bits: Int = ALL_DAYS and bits 112 113 /** 114 * @param calendarDay any of the following values 115 * 116 * * [Calendar.SUNDAY] 117 * * [Calendar.MONDAY] 118 * * [Calendar.TUESDAY] 119 * * [Calendar.WEDNESDAY] 120 * * [Calendar.THURSDAY] 121 * * [Calendar.FRIDAY] 122 * * [Calendar.SATURDAY] 123 * 124 * @param on `true` if the `calendarDay` is on; `false` otherwise 125 * @return a WeekDays instance with the `calendarDay` mutated 126 */ setBitnull127 fun setBit(calendarDay: Int, on: Boolean): Weekdays { 128 val bit = sCalendarDayToBit[calendarDay] ?: return this 129 return Weekdays(if (on) bits or bit else bits and bit.inv()) 130 } 131 132 /** 133 * @param calendarDay any of the following values 134 * 135 * * [Calendar.SUNDAY] 136 * * [Calendar.MONDAY] 137 * * [Calendar.TUESDAY] 138 * * [Calendar.WEDNESDAY] 139 * * [Calendar.THURSDAY] 140 * * [Calendar.FRIDAY] 141 * * [Calendar.SATURDAY] 142 * 143 * @return `true` if the given `calendarDay` 144 */ isBitOnnull145 fun isBitOn(calendarDay: Int): Boolean { 146 val bit = sCalendarDayToBit[calendarDay] 147 ?: throw IllegalArgumentException("$calendarDay is not a valid weekday") 148 return bits and bit > 0 149 } 150 151 /** 152 * @return `true` iff at least one weekday is enabled in the repeat schedule 153 */ 154 val isRepeating: Boolean 155 get() = bits != 0 156 157 /** 158 * Note: only the day-of-week is read from the `time`. The time fields 159 * are not considered in this computation. 160 * 161 * @param time a timestamp relative to which the answer is given 162 * @return the number of days between the given `time` and the previous enabled weekday 163 * which is always between 1 and 7 inclusive; `-1` if no weekdays are enabled 164 */ getDistanceToPreviousDaynull165 fun getDistanceToPreviousDay(time: Calendar): Int { 166 var calendarDay = time[Calendar.DAY_OF_WEEK] 167 for (count in 1..7) { 168 calendarDay-- 169 if (calendarDay < Calendar.SUNDAY) { 170 calendarDay = Calendar.SATURDAY 171 } 172 if (isBitOn(calendarDay)) { 173 return count 174 } 175 } 176 177 return -1 178 } 179 180 /** 181 * Note: only the day-of-week is read from the `time`. The time fields 182 * are not considered in this computation. 183 * 184 * @param time a timestamp relative to which the answer is given 185 * @return the number of days between the given `time` and the next enabled weekday which 186 * is always between 0 and 6 inclusive; `-1` if no weekdays are enabled 187 */ getDistanceToNextDaynull188 fun getDistanceToNextDay(time: Calendar): Int { 189 var calendarDay = time[Calendar.DAY_OF_WEEK] 190 for (count in 0..6) { 191 if (isBitOn(calendarDay)) { 192 return count 193 } 194 195 calendarDay++ 196 if (calendarDay > Calendar.SATURDAY) { 197 calendarDay = Calendar.SUNDAY 198 } 199 } 200 201 return -1 202 } 203 equalsnull204 override fun equals(other: Any?): Boolean { 205 if (this === other) return true 206 if (other == null || javaClass != other.javaClass) return false 207 208 val weekdays = other as Weekdays 209 return bits == weekdays.bits 210 } 211 hashCodenull212 override fun hashCode(): Int { 213 return bits 214 } 215 toStringnull216 override fun toString(): String { 217 val builder = StringBuilder(19) 218 builder.append("[") 219 if (isBitOn(Calendar.MONDAY)) { 220 builder.append(if (builder.length > 1) " M" else "M") 221 } 222 if (isBitOn(Calendar.TUESDAY)) { 223 builder.append(if (builder.length > 1) " T" else "T") 224 } 225 if (isBitOn(Calendar.WEDNESDAY)) { 226 builder.append(if (builder.length > 1) " W" else "W") 227 } 228 if (isBitOn(Calendar.THURSDAY)) { 229 builder.append(if (builder.length > 1) " Th" else "Th") 230 } 231 if (isBitOn(Calendar.FRIDAY)) { 232 builder.append(if (builder.length > 1) " F" else "F") 233 } 234 if (isBitOn(Calendar.SATURDAY)) { 235 builder.append(if (builder.length > 1) " Sa" else "Sa") 236 } 237 if (isBitOn(Calendar.SUNDAY)) { 238 builder.append(if (builder.length > 1) " Su" else "Su") 239 } 240 builder.append("]") 241 return builder.toString() 242 } 243 244 /** 245 * @param context for accessing resources 246 * @param order the order in which to present the weekdays 247 * @return the enabled weekdays in the given `order` 248 */ toStringnull249 fun toString(context: Context, order: Order): String { 250 return toString(context, order, false /* forceLongNames */) 251 } 252 253 /** 254 * @param context for accessing resources 255 * @param order the order in which to present the weekdays 256 * @return the enabled weekdays in the given `order` in a manner that 257 * is most appropriate for talk-back 258 */ toAccessibilityStringnull259 fun toAccessibilityString(context: Context, order: Order): String { 260 return toString(context, order, true /* forceLongNames */) 261 } 262 263 @get:VisibleForTesting 264 val count: Int 265 get() { 266 var count = 0 267 for (calendarDay in Calendar.SUNDAY..Calendar.SATURDAY) { 268 if (isBitOn(calendarDay)) { 269 count++ 270 } 271 } 272 return count 273 } 274 275 /** 276 * @param context for accessing resources 277 * @param order the order in which to present the weekdays 278 * @param forceLongNames if `true` the un-abbreviated weekdays are used 279 * @return the enabled weekdays in the given `order` 280 */ toStringnull281 private fun toString(context: Context, order: Order, forceLongNames: Boolean): String { 282 if (!isRepeating) { 283 return "" 284 } 285 286 if (bits == ALL_DAYS) { 287 return context.getString(R.string.every_day) 288 } 289 290 val longNames = forceLongNames || count <= 1 291 val dfs = DateFormatSymbols() 292 val weekdays = if (longNames) dfs.weekdays else dfs.shortWeekdays 293 294 val separator: String = context.getString(R.string.day_concat) 295 296 val builder = StringBuilder(40) 297 for (calendarDay in order.calendarDays) { 298 if (isBitOn(calendarDay)) { 299 if (builder.isNotEmpty()) { 300 builder.append(separator) 301 } 302 builder.append(weekdays[calendarDay]) 303 } 304 } 305 return builder.toString() 306 } 307 }