• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }