View Javadoc

1   /*
2    * Copyright (C) 2007 Alf Mikula
3    * 
4    * This file is part of PromoteGo.
5    *
6    * PromoteGo is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * PromoteGo is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with PromoteGo.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.promotego.logic.storehours;
20  
21  import java.util.ArrayList;
22  import java.util.Calendar;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Date;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.StringTokenizer;
32  import java.util.TimeZone;
33  
34  import org.apache.commons.lang.StringUtils;
35  import org.apache.commons.lang.math.IntRange;
36  
37  /***
38   * A representation of times a store is open and closed.
39   * 
40   * The StoreHours object is mapped to and from a simple language for
41   * purposes of mapping the object to a persistent store.  The format
42   * is as follows:
43   * 
44   * MON 8-19;TUE-FRI 7-21;SAT-SUN 10-19
45   * 
46   * @author alf
47   */
48  public class StoreHours
49  {
50  	private static Map<String,Integer> s_dayMapping;
51  	private static int SECONDS_IN_DAY = 24*3600;
52  	static int SECONDS_IN_WEEK = 7*SECONDS_IN_DAY;
53  	
54  	private WeeklyHours m_hours;
55  	private Set<HourSpecification> m_hourSpecs;
56  	
57  	static {
58  		s_dayMapping = new HashMap<String,Integer>();
59  		s_dayMapping.put("SUN", 0);
60  		s_dayMapping.put("MON", 1);
61  		s_dayMapping.put("TUE", 2);
62  		s_dayMapping.put("WED", 3);
63  		s_dayMapping.put("THU", 4);
64  		s_dayMapping.put("FRI", 5);
65  		s_dayMapping.put("SAT", 6);
66  	}
67  	
68  	public StoreHours()
69  	{
70  	    m_hours = new WeeklyHours();
71  	}
72  	
73  	public StoreHours(String hourString)
74  	{
75  	    this();
76  		setHours(hourString);
77  	}
78  	
79  	public StoreHours(Collection<HourSpecification> hourSpecs)
80  	{
81  	    this();
82  		m_hourSpecs = new HashSet<HourSpecification>(hourSpecs);
83  	}
84  	
85  	public boolean isOpen(Date startTime, Date endTime)
86  	{
87  		if (endTime.before(startTime))
88  		{
89  			throw new IllegalArgumentException("endTime (" + endTime + " ) may not occur before startTime (" + startTime + ").");
90  		}
91  		
92  		if (endTime.getTime() - startTime.getTime() > SECONDS_IN_DAY*1000)
93  		{
94  			throw new IllegalArgumentException("Date ranges greater than one day not supported (startTime: " + startTime
95  					+ " endTime: " + endTime + ")");
96  		}
97  		
98  		// Convert dates to IntRange
99  		IntRange theRange = getSecondsRange(startTime, endTime);
100 		
101 		// Look up the IntRange
102 		return m_hours.isOpen(theRange);
103 	}
104 	
105 	/***
106 	 * @param startTime
107 	 * @param endTime
108 	 * @return
109 	 */
110 	private IntRange getSecondsRange(Date startTime, Date endTime)
111 	{
112 		if (startTime.after(endTime))
113 		{
114 			throw new IllegalArgumentException("startTime (" + startTime + ") may not occur after endTime (" + endTime + ")");
115 		}
116 		
117 		Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("America/Los_Angeles"));
118 		int startSeconds = getSecondsInWeek(startTime, calendar);
119 		int endSeconds = getSecondsInWeek(endTime, calendar);
120 		
121 		if (endSeconds < startSeconds)
122 		{
123 			endSeconds += SECONDS_IN_WEEK;
124 		}
125 		
126 		return new IntRange(startSeconds, endSeconds);
127 	}
128 
129 	/***
130 	 * @param time
131 	 * @param calendar
132 	 */
133 	private int getSecondsInWeek(Date time, Calendar calendar)
134 	{
135 		calendar.setTime(time);
136 		
137 		// Get start day, normalized to a start day of Sunday.
138 		// Our Sunday is always 0, but Java spec doesn't specify the int value of Sunday.
139 		// Still assuming that the days are sequentially numbered.
140 		int day = (calendar.get(Calendar.DAY_OF_WEEK) - Calendar.SUNDAY + 7)%7;
141 		
142 		// Get hour in day, and minutes in hour
143 		int hours = calendar.get(Calendar.HOUR_OF_DAY);
144 		int minutes = calendar.get(Calendar.MINUTE);
145 		
146 		return day*SECONDS_IN_DAY + 3600*hours + 60*minutes;
147 	}
148 
149 	@Override
150 	public String toString()
151 	{
152 		StringBuilder retval = new StringBuilder();
153 		List<HourSpecification> theList = new ArrayList<HourSpecification>(m_hourSpecs);
154 		Collections.sort(theList, new HourSpecificationComparator());
155 		for (HourSpecification thisSpec : theList)
156 		{
157 			if (retval.length() != 0)
158 			{
159 				retval.append(";");
160 			}
161 			
162 			retval.append(thisSpec.toString());
163 		}
164 		
165 		return retval.toString();
166 	}
167 	
168 	public void setHours(String hourString)
169 	{
170 		m_hours = new WeeklyHours();
171 		m_hourSpecs = new HashSet<HourSpecification>();
172 		
173 		StringTokenizer strtok = new StringTokenizer(hourString, ";");
174 		
175 		while (strtok.hasMoreTokens())
176 		{
177 			// Split string on space to get the days and times components
178 			String thisToken = StringUtils.strip(strtok.nextToken());
179 			
180 			HourSpecification theSpec = new HourSpecification(thisToken);
181 			
182 			m_hourSpecs.add(theSpec);
183 			
184 			for (Day day : theSpec.getDays())
185 			{
186 				for (IntRange range : theSpec.getSecondRanges())
187 				{
188 					m_hours.addRange(new IntRange(range.getMinimumInteger()+day.ordinal()*SECONDS_IN_DAY,
189 							range.getMaximumInteger()+day.ordinal()*SECONDS_IN_DAY));
190 				}
191 			}
192 		}
193 		
194 		coalesceHourSpecs();
195 	}
196 	
197 	private void coalesceHourSpecs()
198 	{
199 		List<HourSpecification> theList = new ArrayList<HourSpecification>(m_hourSpecs);
200 		
201 		// Merge hourspecifications whose days match.
202 		for (int i=0; i<theList.size(); i++)
203 		{
204 			for (int j=i+1; j<theList.size(); j++)
205 			{
206 				HourSpecification spec1 = theList.get(i);
207 				HourSpecification spec2 = theList.get(j);
208 				
209 				if (spec1.sameDays(spec2))
210 				{
211 					assert j > i : "j must be greater than i";
212 					
213 					// We have to remove j first, because it's greater than i.  Otherwise,
214 					// we will end up removing the wrong element after all the indices shift.
215 					theList.remove(j);
216 					theList.remove(i);
217 					theList.add(i, spec1.mergeWith(spec2));
218 					
219 					// We've reduced the size of the list, but we replaced the element
220 					// at i.  Just reduce the j index.
221 					j--;
222 				}
223 			}
224 		}
225 		
226 		// Merge hourspecifications whose hours match.
227 		for (int i=0; i<theList.size(); i++)
228 		{
229 			for (int j=i+1; j<theList.size(); j++)
230 			{
231 				HourSpecification spec1 = theList.get(i);
232 				HourSpecification spec2 = theList.get(j);
233 				
234 				if (spec1.sameHours(spec2))
235 				{
236 					assert j > i : "j must be greater than i";
237 					
238 					// We have to remove j first, because it's greater than i.  Otherwise,
239 					// we will end up removing the wrong element after all the indices shift.
240 					theList.remove(j);
241 					theList.remove(i);
242 					theList.add(i, spec1.mergeWith(spec2));
243 					
244 					// We've reduced the size of the list, but we replaced the element
245 					// at i.  Just reduce the j index.
246 					j--;
247 				}
248 			}
249 		}
250 		
251 		Set<HourSpecification> newSet = new HashSet<HourSpecification>();
252 		newSet.addAll(theList);
253 		m_hourSpecs = newSet;
254 	}
255 	
256 	public Set<HourSpecification> getHourSpecifications()
257 	{
258 		return new HashSet<HourSpecification>(m_hourSpecs);
259 	}
260 
261 	@Override
262 	public boolean equals(Object anotherObject)
263 	{
264 		if (anotherObject == null || !(anotherObject instanceof StoreHours))
265 		{
266 			return false;
267 		}
268 		
269 		StoreHours otherHours = (StoreHours)anotherObject;
270 		
271 		return m_hours.equals(otherHours.m_hours);
272 	}
273 	
274 	@Override
275 	public int hashCode()
276 	{
277 		return m_hours.hashCode();
278 	}
279 }