1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.promotego.logic.storehours;
20
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.HashSet;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.Set;
29
30 import org.apache.commons.lang.math.IntRange;
31
32 /***
33 * A single specification of store hours, featuring hours for one day or
34 * same hours for a set of days. Generates IntRange objects representing
35 * the start and end seconds on a weekly schedule, and also contains helper
36 * methods for merging multiple days' schedules into a single HourSpecification
37 * object for nicer printing.
38 *
39 * @author alf
40 */
41 public class HourSpecification
42 {
43 private static int SECONDS_IN_DAY = 24*3600;
44 private static IntRangeComparator s_intRangeComparator = new IntRangeComparator();
45
46 /***
47 * There are only 7 days, so having an initial size of 10 and default
48 * load factor of 0.75 means we can hold all 7 days without resizing
49 * the set's hash table.
50 */
51 private Set<Day> m_days = new HashSet<Day>(10);
52 private List<IntRange> m_ranges = new ArrayList<IntRange>(2);
53
54 private HourSpecification()
55 { }
56
57 public HourSpecification(String stringSpec)
58 {
59 String [] components = stringSpec.split(" +");
60 if (components.length != 2)
61 {
62 throw new IllegalArgumentException("Wrong number of space-separated tokens in string: " + stringSpec);
63 }
64
65 String [] dayRanges = components[0].split(",");
66 for (String thisRange : dayRanges)
67 {
68 addDayRange(thisRange);
69 }
70
71 String [] hourRanges = components[1].split(",");
72 for (String thisRange : hourRanges)
73 {
74
75 IntRange theBaseRange = getRange(thisRange);
76
77 addHourRange(theBaseRange);
78 }
79 }
80
81 public HourSpecification(Collection<Day> days, List<IntRange> ranges)
82 {
83 m_days.addAll(days);
84 for (IntRange thisRange : ranges)
85 {
86 addHourRange(thisRange);
87 }
88 }
89
90 /***
91 * @param dayRange
92 */
93 private void addDayRange(String dayRange)
94 {
95
96 String [] days = dayRange.split("-");
97 if (days.length > 2)
98 {
99 throw new IllegalArgumentException("Too many dash-separated days in string: " + dayRange);
100 }
101
102
103 Day startResult = Day.mapAbbreviation(days[0]);
104 if (startResult == null)
105 {
106 throw new IllegalArgumentException("Illegal day string: " + days[0]);
107 }
108
109 int startDay = startResult.ordinal();
110
111
112 int endDay;
113 if (days.length > 1)
114 {
115 Day endResult = Day.mapAbbreviation(days[1]);
116 if (endResult == null)
117 {
118 throw new IllegalArgumentException("Illegal day string: " + days[1]);
119 }
120
121 endDay = endResult.ordinal();
122 }
123 else
124 {
125 endDay = startDay;
126 }
127
128
129 if (endDay < startDay)
130 {
131 endDay += 7;
132 }
133
134 for (int theDay = startDay; theDay <= endDay; theDay++)
135 {
136 m_days.add(Day.values()[theDay%7]);
137 }
138 }
139
140 /***
141 * @param theRange
142 */
143 private void addHourRange(IntRange theRange)
144 {
145 int searchResult = Collections.binarySearch(m_ranges, theRange, s_intRangeComparator);
146
147 if (searchResult >= 0)
148 {
149
150 IntRange currentRange = m_ranges.get(searchResult);
151 if (currentRange.containsRange(theRange))
152 {
153
154 }
155 else if (theRange.containsRange(currentRange))
156 {
157
158 m_ranges.remove(searchResult);
159 m_ranges.add(searchResult, theRange);
160 }
161 else
162 {
163
164 int newMinimum = minimum(currentRange.getMinimumInteger(), theRange.getMinimumInteger());
165 int newMaximum = maximum(currentRange.getMaximumInteger(), theRange.getMaximumInteger());
166 IntRange newRange = new IntRange(newMinimum, newMaximum);
167 m_ranges.remove(searchResult);
168 m_ranges.add(searchResult, newRange);
169 }
170 }
171 else
172 {
173
174 int insertionPoint = -searchResult - 1;
175 m_ranges.add(insertionPoint, theRange);
176 }
177 }
178
179 private int minimum(int a, int b)
180 {
181 if (a < b)
182 {
183 return a;
184 }
185 else
186 {
187 return b;
188 }
189 }
190
191 private int maximum(int a, int b)
192 {
193 if (a > b)
194 {
195 return a;
196 }
197 else
198 {
199 return b;
200 }
201 }
202
203 /***
204 * Given a range of hours in format HH[:MM]-HH[:MM], return an <code>IntRange</code>
205 * representing the time in seconds of the hours given. Hours are in 24-hour format,
206 * and an end time before the start time represents an end time the next day, such
207 * as 9-1:30, meaning the store closes at 1:30 am the following morning.
208 *
209 * @param string The hours to parse, in format HH[:MM]-HH[:MM].
210 * @return An <code>IntRange</code> representing the start and end times in seconds.
211 */
212 private IntRange getRange(String hourRange)
213 {
214 String [] hours = hourRange.split("-");
215
216 if (hours.length != 2)
217 {
218 throw new IllegalArgumentException("Illegal hour range string: " + hourRange);
219 }
220
221 int startHour = toSeconds(hours[0]);
222 int endHour = toSeconds(hours[1]);
223 if (endHour <= startHour)
224 {
225
226 endHour += SECONDS_IN_DAY;
227 }
228
229 return new IntRange(startHour, endHour);
230 }
231
232 /***
233 * Returns the time in seconds since midnight represented by the <code>String</code> passed in.
234 *
235 * @param string A time, in HH:MM format.
236 * @return The time in seconds since midnight.
237 */
238 private int toSeconds(String string)
239 {
240 String [] components = string.split(":");
241
242 if (components.length > 2)
243 {
244 throw new IllegalArgumentException("Illegal hour string: " + string);
245 }
246
247 int retval;
248
249 try
250 {
251 int hour = Integer.parseInt(components[0]);
252 retval = hour*3600;
253 if (components.length > 1)
254 {
255 int minutes = Integer.parseInt(components[1]);
256 retval += minutes*60;
257 }
258 } catch (NumberFormatException e)
259 {
260 throw new IllegalArgumentException("Illegal hour string: " + string);
261 }
262
263 return retval;
264 }
265
266 @Override
267 public boolean equals(Object obj)
268 {
269 if (obj == this)
270 {
271 return true;
272 }
273
274 if (!(obj instanceof HourSpecification))
275 {
276 return false;
277 }
278
279 HourSpecification otherSpec = (HourSpecification) obj;
280 return m_days.equals(otherSpec.m_days) && m_ranges.equals(otherSpec.m_ranges);
281 }
282
283 @Override
284 public int hashCode()
285 {
286 return m_days.hashCode() ^ m_ranges.hashCode();
287 }
288
289 @Override
290 public String toString()
291 {
292 return getDaysString() + " " + getHoursString();
293 }
294
295 private String getDaysString()
296 {
297 List<IntRange> daysList = new LinkedList<IntRange>();
298
299 Day [] days = m_days.toArray(new Day[m_days.size()]);
300 Arrays.sort(days);
301 Day startDay = null;
302 Day endDay = null;
303 for (Day thisDay : days)
304 {
305 if (startDay == null)
306 {
307
308 startDay = thisDay;
309 endDay = thisDay;
310 }
311 else
312 {
313 if (thisDay.ordinal() - endDay.ordinal() > 1)
314 {
315
316
317 daysList.add(new IntRange(startDay.ordinal(), endDay.ordinal()));
318
319
320 startDay = thisDay;
321 endDay = thisDay;
322 }
323 else
324 {
325
326 endDay = thisDay;
327 }
328 }
329 }
330
331
332 assert (startDay != null && endDay != null) : "HourSpecification must have at least one day";
333
334 daysList.add(new IntRange(startDay.ordinal(), endDay.ordinal()));
335
336 StringBuilder retval = new StringBuilder(12);
337 if (daysList.size() >= 2 && daysList.get(0).getMinimumInteger() == 0
338 && daysList.get(daysList.size()-1).getMaximumInteger() == 6)
339 {
340
341 IntRange lastRange = daysList.remove(daysList.size() - 1);
342 IntRange firstRange = daysList.remove(0);
343
344 retval.append(Day.values()[lastRange.getMinimumInteger()].getAbbreviation());
345 retval.append("-");
346 retval.append(Day.values()[firstRange.getMaximumInteger()].getAbbreviation());
347 }
348
349
350 for (IntRange thisRange : daysList)
351 {
352 if (retval.length() > 0)
353 {
354 retval.append(",");
355 }
356
357 startDay = Day.values()[thisRange.getMinimumInteger()];
358 endDay = Day.values()[thisRange.getMaximumInteger()];
359 retval.append(startDay.getAbbreviation());
360 if (!startDay.equals(endDay))
361 {
362 retval.append("-");
363 retval.append(endDay.getAbbreviation());
364 }
365 }
366
367 return retval.toString();
368 }
369
370 private String getHoursString()
371 {
372 StringBuilder retval = new StringBuilder(16);
373
374 for (IntRange thisRange : m_ranges)
375 {
376 if (retval.length() > 0)
377 {
378 retval.append(",");
379 }
380
381 retval.append(toHourString(thisRange.getMinimumInteger()));
382 retval.append("-");
383 retval.append(toHourString(thisRange.getMaximumInteger()));
384 }
385
386 return retval.toString();
387 }
388
389 /***
390 * @param minimumInteger
391 * @return
392 */
393 private String toHourString(int minimumInteger)
394 {
395
396 StringBuilder retval = new StringBuilder(5);
397 int hour = minimumInteger/3600;
398 if (hour >= 24)
399 {
400 hour -= 24;
401 }
402 retval.append(hour);
403
404 int leftoverSeconds = minimumInteger%3600;
405 if (leftoverSeconds > 60)
406 {
407 retval.append(":");
408 retval.append(leftoverSeconds/60);
409 }
410 return retval.toString();
411 }
412
413 public boolean sameDays(HourSpecification otherSpec)
414 {
415 return m_days.equals(otherSpec.m_days);
416 }
417
418 public boolean sameHours(HourSpecification otherSpec)
419 {
420 return m_ranges.equals(otherSpec.m_ranges);
421 }
422
423 /***
424 * Return a new HourSpecification with all days and hour ranges of this
425 * HourSpecification and the one provided. This operation is idempotent
426 * with respect to both HourSpecification objects used to calculate the
427 * new HourSpecification.
428 *
429 * @param otherSpec
430 * @return
431 */
432 public HourSpecification mergeWith(HourSpecification otherSpec)
433 {
434 HourSpecification retval = new HourSpecification();
435
436 retval.m_days.addAll(m_days);
437 retval.m_days.addAll(otherSpec.m_days);
438
439 retval.m_ranges.addAll(m_ranges);
440 for (IntRange thisRange : otherSpec.m_ranges)
441 {
442 retval.addHourRange(thisRange);
443 }
444
445 return retval;
446 }
447
448 public Set<Day> getDays()
449 {
450 return new HashSet<Day>(m_days);
451 }
452
453 public List<IntRange> getSecondRanges()
454 {
455 return new ArrayList<IntRange>(m_ranges);
456 }
457 }