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
17package com.android.deskclock.data;
18
19import android.content.Context;
20import android.support.annotation.VisibleForTesting;
21import android.util.ArrayMap;
22
23import com.android.deskclock.R;
24
25import java.text.DateFormatSymbols;
26import java.util.Arrays;
27import java.util.Calendar;
28import java.util.Collections;
29import java.util.List;
30import java.util.Map;
31
32import static java.util.Calendar.DAY_OF_WEEK;
33import static java.util.Calendar.FRIDAY;
34import static java.util.Calendar.MONDAY;
35import static java.util.Calendar.SATURDAY;
36import static java.util.Calendar.SUNDAY;
37import static java.util.Calendar.THURSDAY;
38import static java.util.Calendar.TUESDAY;
39import 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 */
46public 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
59        Order(Integer... calendarDays) {
60            mCalendarDays = Arrays.asList(calendarDays);
61        }
62
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);
81        map.put(MONDAY,    0x01);
82        map.put(TUESDAY,   0x02);
83        map.put(WEDNESDAY, 0x04);
84        map.put(THURSDAY,  0x08);
85        map.put(FRIDAY,    0x10);
86        map.put(SATURDAY,  0x20);
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
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     */
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     */
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     */
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     */
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     */
177    public int getBits() { return mBits; }
178
179    /**
180     * @return {@code true} iff at least one weekday is enabled in the repeat schedule
181     */
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     */
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     */
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
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
241    public int hashCode() {
242        return mBits;
243    }
244
245    @Override
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     */
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     */
289    public String toAccessibilityString(Context context, Order order) {
290        return toString(context, order, true /* forceLongNames */);
291    }
292
293    @VisibleForTesting
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     */
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}