001    package org.hackystat.utilities.time.period;
002    
003    import java.util.Date;
004    import java.util.Locale;
005    import java.text.SimpleDateFormat;
006    import java.util.Calendar;
007    
008    import javax.xml.datatype.XMLGregorianCalendar;
009    
010    
011    /**
012     * Provides a "cousin" of Date that represents only year, month, and day information. Many Hackystat
013     * facilities need date information only at the precision of year/month/day.  This abstraction
014     * represents a single 24 hour time period.
015     * <p>
016     * All Day constructors are private or package private. Clients of this class must use the 
017     * static getInstance() methods to obtain Day instances. 
018     * <p>
019     * The Calendar is forced to Locale.US to ensure constant week boundaries. 
020     * <p>
021     * Day instances are immutable.
022     * 
023     * @author    Philip M. Johnson
024     */
025    public final class Day implements TimePeriod {
026    
027      /** Used to represent the day. */
028      private Calendar cal = Calendar.getInstance(Locale.US);
029      /** The hashcode used for comparisons. */
030      private int hashCode = 0;
031      /** The date format for the toString method. */
032      private static SimpleDateFormat toStringFormat = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
033      /** Simplified date format. */
034      private static SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
035      /** The formats for the getting the day, month, and year fields as strings. */
036      private static SimpleDateFormat monthStringFormat = new SimpleDateFormat("MM", Locale.US);
037      /** The formats for the getting the day, month, and year fields as strings. */
038      private static SimpleDateFormat mediumMonthStringFormat = new SimpleDateFormat("MMM", Locale.US); 
039      /** Year string formatter. */
040      private static SimpleDateFormat yearStringFormat = new SimpleDateFormat("yyyy", Locale.US);
041      /** Day string formatter. */
042      private static SimpleDateFormat dayStringFormat = new SimpleDateFormat("dd", Locale.US);
043      /** The number of milliseconds in a day. */
044      private static long millisInDay = 1000 * 60 * 60 * 24;
045      
046      /**
047       * Returns a Day instance corresponding to today.
048       * @return A Day instance corresponding to today.
049       */
050      public static Day getInstance() {
051        return new Day();
052      }
053      
054      /**
055       * Returns a Day instance corresponding to the day associated with timestamp.
056       * 
057       * @param timestamp A UTC time from which a Day instance will be returned.
058       * @return A Day instance corresponding to timestamp.
059       */
060      public static Day getInstance(long timestamp) {
061        return new Day(timestamp);
062      }
063    
064      /**
065       * Returns a Day instance corresponding to the passed Date.
066       * 
067       * @param date A date from which a Day instance will be returned. 
068       * @return The Day instance corresponding to Date.
069       */  
070      public static Day getInstance(Date date) {
071        return new Day(date.getTime());
072      }
073      
074      /**
075       * Returns a Day instance corresponding to the passed year, month, and day.
076       * Historically used for testing, although getInstance(dayString) should be used instead
077       * for clarity's sake unless numeric values are being manipulated directly. 
078       *
079       * @param year   Year, such as 2003.
080       * @param month  Month, such as 0. Note that January is 0!
081       * @param day    Day, such as 1. The first day is 1. 
082       * @return The Day instance associated with this date.
083       */
084      public static Day getInstance(int year, int month, int day) {
085        Calendar cal = Calendar.getInstance(Locale.US);
086        cal.set(Calendar.YEAR, year);
087        cal.set(Calendar.MONTH, month);
088        cal.set(Calendar.DAY_OF_MONTH, day);
089        return new Day(cal.getTime().getTime());
090      }
091      
092      /**
093       * Returns a Day instance corresponding to the passed date in dd-MMM-yyyy format. 
094       * This creates a temporary SimpleDateFormat, so it shouldn't be used when efficiency is 
095       * important, but it's a nice method for test cases where dates are being supplied manually.
096       *
097       * @param dayString A string in dd-MMM-yyyy US Locale format, such as "01-Jan-2004".
098       * @return The Day instance associated with this date.
099       * @throws Exception If problems occur parsing dayString.
100       */
101      public static Day getInstance(String dayString) throws Exception {
102        Date date = new SimpleDateFormat("dd-MMM-yyyy", Locale.US).parse(dayString);
103        return new Day(date.getTime());
104      }
105      
106      /**
107       * Returns a Day instance associated with this passed XMLGregorianCalendar. 
108       * @param xmlDay The date.
109       * @return The corresponding day.
110       */
111      public static Day getInstance(XMLGregorianCalendar xmlDay) {
112        long millis = xmlDay.toGregorianCalendar().getTimeInMillis();
113        return new Day(millis);
114      }
115    
116      /** 
117       * Create a new Day, initializing it to today.
118       */
119      private Day() {
120        this(new Date());
121      }
122    
123      /**
124       * Creates a new Day instance, initializing it to the passed long UTC time.
125       * 
126       * @param timestamp A UTC value indicating a time. 
127       */
128      private Day(long timestamp) {
129        this(new Date(timestamp));
130      }
131    
132      /**
133       * Creates a new Day instance, initializing it from the passed Date.
134       * The internal date is always set to 00:00:00.000 midnight at the 
135       * beginning of the passed day.
136       * We do this upfront to make comparisons and interval calculations fast, since we're
137       * caching these Day instances there should be relatively few of them around.
138       * 
139       * @param date  A date.
140       */
141      private Day(Date date) {
142        this.cal = Calendar.getInstance(Locale.US);
143        this.cal.setTime(date);
144        this.cal.set(Calendar.HOUR_OF_DAY, 00);
145        this.cal.set(Calendar.MINUTE, 0);
146        this.cal.set(Calendar.SECOND, 0);
147        this.cal.set(Calendar.MILLISECOND, 0);
148        //this.date = cal.getTime();
149      }
150      
151      /**
152       * Returns a Day instance corresponding to a day plus or minus increment days in the future 
153       * or past. 
154       *
155       * @param increment  A positive or negative integer.
156       * @return           A Day corresponding to the increment from this day.
157       */
158      public Day inc(int increment) {
159        Calendar newCal = (Calendar)this.cal.clone();
160        newCal.add(Calendar.DAY_OF_YEAR, increment);
161        return new Day(newCal.getTime());
162      }
163      
164      /**
165       * Returns a freshly created Date object that corresponds to the current day.
166       * Note that this Date object may not correspond to the same time of day as was
167       * used to create this Day instance!
168       * @return A Date object corresponding to this day.
169       */
170      public Date getDate() {
171        return new Date(this.cal.getTimeInMillis());
172      }
173      
174      /**
175       * Returns the "first" day in the TimePeriod.  For Days, that's just this day.
176       * @return The current day.
177       */
178      public Day getFirstDay() {
179        return this;
180      }
181      
182      /**
183       * Returns the number of days (positive or negative) between day1 and day2.
184       * @param day1 A Day instance.
185       * @param day2 A Day instance.
186       * @return The interval in days between day1 and day2.
187       */
188      public static int daysBetween(Day day1, Day day2) {
189        return Math.round((float)(day2.cal.getTimeInMillis() - day1.cal.getTimeInMillis()) 
190            / millisInDay);
191      }
192    
193      /**
194       * Compares two Day objects.
195       *
196       * @param o  A Day instance.
197       * @return   The standard compareTo values.
198       */
199      public int compareTo(Object o) {
200        return this.cal.compareTo(((Day)o).cal);
201      }
202      
203      /**
204       * Returns true if this Day preceeds the passed Day, false if the two Days are equal or this
205       * day comes after the passed Day.
206       * @param day The day to be compared.
207       * @return True if the passed day comes after this Day.
208       */
209      public boolean isBefore(Day day) {
210        return (this.cal.before(day.cal));
211      }
212      
213      /**
214       * Two Day instances are equal() iff they have equal year, month, and day fields.
215       *
216       * @param obj  Any object.
217       * @return     True if obj is a Day and it's equal to this Day.
218       */
219      @Override
220      public boolean equals(Object obj) {
221        if (!(obj instanceof Day)) {
222          return false;
223        }
224        Calendar thisCal = this.cal;
225        Calendar otherCal = ((Day)obj).cal;
226        return 
227        ((thisCal.get(Calendar.YEAR) == otherCal.get(Calendar.YEAR)) &&
228            (thisCal.get(Calendar.DAY_OF_YEAR) == otherCal.get(Calendar.DAY_OF_YEAR)));
229      }
230    
231    
232      /**
233       * Compute the hashcode following recommendations in "Effective Java".
234       *
235       * @return   The hashcode.
236       */
237      @Override
238      public int hashCode() {
239        if (this.hashCode == 0) {
240          this.hashCode = 17;
241          this.hashCode = 37 * this.hashCode + this.cal.get(Calendar.YEAR);
242          this.hashCode = 37 * this.hashCode + this.cal.get(Calendar.MONTH);
243          this.hashCode = 37 * this.hashCode + this.cal.get(Calendar.DAY_OF_MONTH);
244        }
245        return this.hashCode;
246      }
247    
248      /**
249       * Returns this Day instance in YYYY-MM-DD format.
250       *
251       * @return   The day as a string.
252       */
253      @Override
254      public String toString() {
255        synchronized (Day.toStringFormat) {
256          return Day.toStringFormat.format(new Date(this.cal.getTimeInMillis()));
257        }
258      }
259    
260      /**
261       * Gets the simple date string in 2004-03-25 format.
262       * 
263       * @return Simply date format.
264       */
265      public String getSimpleDayString() {
266        synchronized (Day.simpleFormat) {
267          return Day.simpleFormat.format(new Date(this.cal.getTimeInMillis()));
268        }
269      }
270      
271      
272      /**
273       * Returns a two character string representing this Day's day (00-31).
274       *
275       * @return   The day as a string.
276       */
277      public String getDayString() {
278        synchronized (Day.dayStringFormat) {
279          return Day.dayStringFormat.format(new Date(this.cal.getTimeInMillis()));
280        }
281      }
282    
283      /**
284       * Returns a two character string representing this Day's month (01-12).
285       *
286       * @return   The month as a string.
287       */
288      public String getMonthString() {
289        synchronized (Day.monthStringFormat) {
290          return Day.monthStringFormat.format(new Date(this.cal.getTimeInMillis()));
291        }
292      }
293    
294    
295      /**
296       * Returns a three character string representing this Day's month, e.g. Jan, Feb.
297       *
298       * @return   The month as a string.
299       */
300      public String getMediumMonthString() {
301        synchronized (Day.mediumMonthStringFormat) {
302          return Day.mediumMonthStringFormat.format(new Date(this.cal.getTimeInMillis()));
303        }
304      }
305    
306      /**
307       * Returns a four character string representing this Day's year (2000, etc.).
308       *
309       * @return   The year as a string.
310       */
311      public String getYearString() {
312        synchronized (Day.yearStringFormat) {
313          return Day.yearStringFormat.format(new Date(this.cal.getTimeInMillis()));
314        }
315      }
316    
317      /**
318       * Gets the first tick of the day.
319       * 
320       * @return The first tick of the day in millis.
321       */
322      public long getFirstTickOfTheDay() {
323        return this.cal.getTimeInMillis();
324      }
325      
326      /**
327       * Gets the last tick of the day.
328       * 
329       * @return The last tick of the day in millis.
330       */
331      public long getLastTickOfTheDay() {
332        Calendar newCal = Calendar.getInstance(Locale.US);
333        newCal.setTimeInMillis(this.cal.getTimeInMillis());
334        newCal.set(Calendar.HOUR_OF_DAY, newCal.getActualMaximum(Calendar.HOUR_OF_DAY));
335        newCal.set(Calendar.MINUTE, newCal.getActualMaximum(Calendar.MINUTE));
336        newCal.set(Calendar.SECOND, newCal.getActualMaximum(Calendar.SECOND));
337        newCal.set(Calendar.MILLISECOND, newCal.getActualMaximum(Calendar.MILLISECOND));
338        return newCal.getTime().getTime();
339      }
340    }
341