1 /*
2  * Copyright (C) 2016 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 import android.util.ArrayMap;
22 
23 import com.android.deskclock.R;
24 
25 import java.text.DateFormatSymbols;
26 import java.util.Arrays;
27 import java.util.Calendar;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Map;
31 
32 import static java.util.Calendar.DAY_OF_WEEK;
33 import static java.util.Calendar.FRIDAY;
34 import static java.util.Calendar.MONDAY;
35 import static java.util.Calendar.SATURDAY;
36 import static java.util.Calendar.SUNDAY;
37 import static java.util.Calendar.THURSDAY;
38 import static java.util.Calendar.TUESDAY;
39 import static java.util.Calendar.WEDNESDAY;
40 
41 /**
42  * This class is responsible for encoding a weekly repeat cycle in a {@link #getBits bitset}. It
43  * also converts between those bits and the {@link Calendar#DAY_OF_WEEK} values for easier mutation
44  * and querying.
45  */
46 public final class Weekdays {
47 
48     /**
49      * The preferred starting day of the week can differ by locale. This enumerated value is used to
50      * describe the preferred ordering.
51      */
52     public enum Order {
53         SAT_TO_FRI(SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY),
54         SUN_TO_SAT(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY),
55         MON_TO_SUN(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY);
56 
57         private final List<Integer> mCalendarDays;
58 
Order(Integer... calendarDays)59         Order(Integer... calendarDays) {
60             mCalendarDays = Arrays.asList(calendarDays);
61         }
62 
getCalendarDays()63         public List<Integer> getCalendarDays() {
64             return mCalendarDays;
65         }
66     }
67 
68     /** All valid bits set. */
69     private static final int ALL_DAYS = 0x7F;
70 
71     /** An instance with all weekdays in the weekly repeat cycle. */
72     public static final Weekdays ALL = Weekdays.fromBits(ALL_DAYS);
73 
74     /** An instance with no weekdays in the weekly repeat cycle. */
75     public static final Weekdays NONE = Weekdays.fromBits(0);
76 
77     /** Maps calendar weekdays to the bit masks that represent them in this class. */
78     private static final Map<Integer, Integer> sCalendarDayToBit;
79     static {
80         final Map<Integer, Integer> map = new ArrayMap<>(7);
map.put(MONDAY, 0x01)81         map.put(MONDAY,    0x01);
map.put(TUESDAY, 0x02)82         map.put(TUESDAY,   0x02);
map.put(WEDNESDAY, 0x04)83         map.put(WEDNESDAY, 0x04);
map.put(THURSDAY, 0x08)84         map.put(THURSDAY,  0x08);
map.put(FRIDAY, 0x10)85         map.put(FRIDAY,    0x10);
map.put(SATURDAY, 0x20)86         map.put(SATURDAY,  0x20);
map.put(SUNDAY, 0x40)87         map.put(SUNDAY,    0x40);
88         sCalendarDayToBit = Collections.unmodifiableMap(map);
89     }
90 
91     /** An encoded form of a weekly repeat schedule. */
92     private final int mBits;
93 
Weekdays(int bits)94     private Weekdays(int bits) {
95         // Mask off the unused bits.
96         mBits = ALL_DAYS & bits;
97     }
98 
99     /**
100      * @param bits {@link #getBits bits} representing the encoded weekly repeat schedule
101      * @return a Weekdays instance representing the same repeat schedule as the {@code bits}
102      */
fromBits(int bits)103     public static Weekdays fromBits(int bits) {
104         return new Weekdays(bits);
105     }
106 
107     /**
108      * @param calendarDays an array containing any or all of the following values
109      *                     <ul>
110      *                     <li>{@link Calendar#SUNDAY}</li>
111      *                     <li>{@link Calendar#MONDAY}</li>
112      *                     <li>{@link Calendar#TUESDAY}</li>
113      *                     <li>{@link Calendar#WEDNESDAY}</li>
114      *                     <li>{@link Calendar#THURSDAY}</li>
115      *                     <li>{@link Calendar#FRIDAY}</li>
116      *                     <li>{@link Calendar#SATURDAY}</li>
117      *                     </ul>
118      * @return a Weekdays instance representing the given {@code calendarDays}
119      */
fromCalendarDays(int... calendarDays)120     public static Weekdays fromCalendarDays(int... calendarDays) {
121         int bits = 0;
122         for (int calendarDay : calendarDays) {
123             final Integer bit = sCalendarDayToBit.get(calendarDay);
124             if (bit != null) {
125                 bits = bits | bit;
126             }
127         }
128         return new Weekdays(bits);
129     }
130 
131     /**
132      * @param calendarDay any of the following values
133      *                     <ul>
134      *                     <li>{@link Calendar#SUNDAY}</li>
135      *                     <li>{@link Calendar#MONDAY}</li>
136      *                     <li>{@link Calendar#TUESDAY}</li>
137      *                     <li>{@link Calendar#WEDNESDAY}</li>
138      *                     <li>{@link Calendar#THURSDAY}</li>
139      *                     <li>{@link Calendar#FRIDAY}</li>
140      *                     <li>{@link Calendar#SATURDAY}</li>
141      *                     </ul>
142      * @param on {@code true} if the {@code calendarDay} is on; {@code false} otherwise
143      * @return a WeekDays instance with the {@code calendarDay} mutated
144      */
setBit(int calendarDay, boolean on)145     public Weekdays setBit(int calendarDay, boolean on) {
146         final Integer bit = sCalendarDayToBit.get(calendarDay);
147         if (bit == null) {
148             return this;
149         }
150         return new Weekdays(on ? (mBits | bit) : (mBits & ~bit));
151     }
152 
153     /**
154      * @param calendarDay any of the following values
155      *                     <ul>
156      *                     <li>{@link Calendar#SUNDAY}</li>
157      *                     <li>{@link Calendar#MONDAY}</li>
158      *                     <li>{@link Calendar#TUESDAY}</li>
159      *                     <li>{@link Calendar#WEDNESDAY}</li>
160      *                     <li>{@link Calendar#THURSDAY}</li>
161      *                     <li>{@link Calendar#FRIDAY}</li>
162      *                     <li>{@link Calendar#SATURDAY}</li>
163      *                     </ul>
164      * @return {@code true} if the given {@code calendarDay}
165      */
isBitOn(int calendarDay)166     public boolean isBitOn(int calendarDay) {
167         final Integer bit = sCalendarDayToBit.get(calendarDay);
168         if (bit == null) {
169             throw new IllegalArgumentException(calendarDay + " is not a valid weekday");
170         }
171         return (mBits & bit) > 0;
172     }
173 
174     /**
175      * @return the weekly repeat schedule encoded as an integer
176      */
getBits()177     public int getBits() { return mBits; }
178 
179     /**
180      * @return {@code true} iff at least one weekday is enabled in the repeat schedule
181      */
isRepeating()182     public boolean isRepeating() { return mBits != 0; }
183 
184     /**
185      * Note: only the day-of-week is read from the {@code time}. The time fields
186      * are not considered in this computation.
187      *
188      * @param time a timestamp relative to which the answer is given
189      * @return the number of days between the given {@code time} and the previous enabled weekday
190      *      which is always between 1 and 7 inclusive; {@code -1} if no weekdays are enabled
191      */
getDistanceToPreviousDay(Calendar time)192     public int getDistanceToPreviousDay(Calendar time) {
193         int calendarDay = time.get(DAY_OF_WEEK);
194         for (int count = 1; count <= 7; count++) {
195             calendarDay--;
196             if (calendarDay < Calendar.SUNDAY) {
197                 calendarDay = Calendar.SATURDAY;
198             }
199             if (isBitOn(calendarDay)) {
200                 return count;
201             }
202         }
203 
204         return -1;
205     }
206 
207     /**
208      * Note: only the day-of-week is read from the {@code time}. The time fields
209      * are not considered in this computation.
210      *
211      * @param time a timestamp relative to which the answer is given
212      * @return the number of days between the given {@code time} and the next enabled weekday which
213      *      is always between 0 and 6 inclusive; {@code -1} if no weekdays are enabled
214      */
getDistanceToNextDay(Calendar time)215     public int getDistanceToNextDay(Calendar time) {
216         int calendarDay = time.get(DAY_OF_WEEK);
217         for (int count = 0; count < 7; count++) {
218             if (isBitOn(calendarDay)) {
219                 return count;
220             }
221 
222             calendarDay++;
223             if (calendarDay > Calendar.SATURDAY) {
224                 calendarDay = Calendar.SUNDAY;
225             }
226         }
227 
228         return -1;
229     }
230 
231     @Override
equals(Object o)232     public boolean equals(Object o) {
233         if (this == o) return true;
234         if (o == null || getClass() != o.getClass()) return false;
235 
236         final Weekdays weekdays = (Weekdays) o;
237         return mBits == weekdays.mBits;
238     }
239 
240     @Override
hashCode()241     public int hashCode() {
242         return mBits;
243     }
244 
245     @Override
toString()246     public String toString() {
247         final StringBuilder builder = new StringBuilder(19);
248         builder.append("[");
249         if (isBitOn(MONDAY)) {
250             builder.append(builder.length() > 1 ? " M" : "M");
251         }
252         if (isBitOn(TUESDAY)) {
253             builder.append(builder.length() > 1 ? " T" : "T");
254         }
255         if (isBitOn(WEDNESDAY)) {
256             builder.append(builder.length() > 1 ? " W" : "W");
257         }
258         if (isBitOn(THURSDAY)) {
259             builder.append(builder.length() > 1 ? " Th" : "Th");
260         }
261         if (isBitOn(FRIDAY)) {
262             builder.append(builder.length() > 1 ? " F" : "F");
263         }
264         if (isBitOn(SATURDAY)) {
265             builder.append(builder.length() > 1 ? " Sa" : "Sa");
266         }
267         if (isBitOn(SUNDAY)) {
268             builder.append(builder.length() > 1 ? " Su" : "Su");
269         }
270         builder.append("]");
271         return builder.toString();
272     }
273 
274     /**
275      * @param context for accessing resources
276      * @param order the order in which to present the weekdays
277      * @return the enabled weekdays in the given {@code order}
278      */
toString(Context context, Order order)279     public String toString(Context context, Order order) {
280         return toString(context, order, false /* forceLongNames */);
281     }
282 
283     /**
284      * @param context for accessing resources
285      * @param order the order in which to present the weekdays
286      * @return the enabled weekdays in the given {@code order} in a manner that
287      *      is most appropriate for talk-back
288      */
toAccessibilityString(Context context, Order order)289     public String toAccessibilityString(Context context, Order order) {
290         return toString(context, order, true /* forceLongNames */);
291     }
292 
293     @VisibleForTesting
getCount()294     int getCount() {
295         int count = 0;
296         for (int calendarDay = SUNDAY; calendarDay <= SATURDAY; calendarDay++) {
297             if (isBitOn(calendarDay)) {
298                 count++;
299             }
300         }
301         return count;
302     }
303 
304     /**
305      * @param context for accessing resources
306      * @param order the order in which to present the weekdays
307      * @param forceLongNames if {@code true} the un-abbreviated weekdays are used
308      * @return the enabled weekdays in the given {@code order}
309      */
toString(Context context, Order order, boolean forceLongNames)310     private String toString(Context context, Order order, boolean forceLongNames) {
311         if (!isRepeating()) {
312             return "";
313         }
314 
315         if (mBits == ALL_DAYS) {
316             return context.getString(R.string.every_day);
317         }
318 
319         final boolean longNames = forceLongNames || getCount() <= 1;
320         final DateFormatSymbols dfs = new DateFormatSymbols();
321         final String[] weekdays = longNames ? dfs.getWeekdays() : dfs.getShortWeekdays();
322 
323         final String separator = context.getString(R.string.day_concat);
324 
325         final StringBuilder builder = new StringBuilder(40);
326         for (int calendarDay : order.getCalendarDays()) {
327             if (isBitOn(calendarDay)) {
328                 if (builder.length() > 0) {
329                     builder.append(separator);
330                 }
331                 builder.append(weekdays[calendarDay]);
332             }
333         }
334         return builder.toString();
335     }
336 }