View Javadoc
1   package com.randomnoun.common.jexl;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.io.Serializable;
8   import java.text.*;
9   import java.util.*;
10  
11  import com.randomnoun.common.Text;
12  
13  /**
14   * Data object representing a point in time, or a 24-hour day period.
15   * 
16   * <p>You should probably use something in joda or the java 8 time package these days
17   *
18   * <p>DateSpans may be <i>fixed</i>, which refer to dates that do not necessarily have timezones
19   * (e.g. a valueDate, represented with the fixed date "2004-11-07" is always the 7th of November,
20   * regardless of where in the world this may be interpreted). Note that
21   * Date objects are stored internally as milliseconds since the Unix epoch UTC, and therefore
22   * do *not* qualify as 'fixed' dates. Anything that isn't fixed I refer to as 'timezoned'
23   * in the documentation here (because they only make sense when interpretted in a
24   * specific timezone).
25   *
26   * <p>Examples of DateSpans:
27   * <pre>
28   * new DateSpan("2004-11-07")          - fixed day span       (representing November 7, 2004, 24hr period))
29   * new DateSpan("2004-11-07T20:00:00") - fixed timestamp      (representing November 7, 2004, 8:00pm)
30   * new DateSpan(
31   *   DateUtils.getStartOfDay(new Date(),
32   *   TimeZone.getDefault()), true)     - timezoned day span   (representing today in local time (24hr period))
33   * new DateSpan(new Date())            - timezoned timestamp  (representing right now in local time)
34   * </pre>
35   *
36   * <p>Note that only yyyy-MM-dd or yyyy-MM-dd'T'HH:mm:ss (without timezone) string formats are permitted;
37   * this is more strict than that allowed by ISO8901, so I do not use that class here.
38   *
39   * <p>Note that timestamps are only precise down to 1 second.
40   *
41   * <p>'fixed' times may also be assigned default timezones, which can be used when comparing
42   * timezoned values with fixed DateSpans; e.g. the RUNDATE for Market Center "New York" may be
43   * specified as "2004-11-07" (and compared with valueDates with that same fixed date),
44   * but when compared against a SystemArrivalTime, needs to be interpretted in a time zone (in
45   * this case, TimeZone.getTimezone("America/New_York")). This can be specified at construction
46   * time, e.g.
47   * <pre style="code">
48   *   new DateSpan("2004-11-07", TimeZone.getTimezone("America/New_York"))
49   * </pre>
50   *
51   * <p>Note that if a timezone is not supplied for a 'fixed' time, then any call to {@link #getStartAsDate()}
52   * or {@link #getEndAsDate()} will return an IllegalStateException.
53   *
54   * <p>This class only supports the 'fixed day span', 'timezoned day span' or 'timezoned timestamp'
55   * examples above. Could extend this later on to 'fixed timestamp' values. 
56   * Similarly, this class only supports 24hour spans, but could support more arbitrary durations 
57   * if this was needed further down the track.
58   *
59   * <p>Only years between 1000 and 9999 will be handled correctly by this class.
60   * 
61   * @author knoxg
62   */
63  public class DateSpan
64      implements Serializable
65  {
66      /** Generated serialVersionUID */
67  	private static final long serialVersionUID = 4189707923685011603L;
68  
69  	/** Contains the string representation of a 'fixed' timestamp or duration (or null if this is a timezoned DateSpan) */
70      private String fixedTime;
71  
72      /** Contains a 'timezoned' Date, or for fixed values, contains the Date of 'fixedTime' in the defaultTimeZone */
73      private Date timezonedTime;
74  
75      /** Is true if this DateSpan represents a 24hour period, or false for a 1 second instant. */
76      boolean isDay;
77  
78      /** For 'fixed' DateSpans, specifies a default timezone to interpret this in (if we need to) */
79      private TimeZone defaultTimezone;
80      
81      /** A single timestamp instant, without timezone. Equivalent to DateSpan(Date, false, null) */
82      public DateSpan(Date timestamp) {
83          this(timestamp, false, null);
84      }
85  
86      /** Create a 'timezoneless' date. A defaultTimezone may be supplied, which will be used
87       *  when converting to Date objects.
88       *
89       * @param dateSpan A date in yyyy-MM-dd or yyyy-MM-dd'T'HH:mm:ss format (the former
90       *    specifies a day period, the latter specifies a single instant
91       * @param defaultTimezone A default timezone to use when converting to Date objects.
92       *    This parameter may be null.
93       *
94       * @throws NullPointerException if the dateSpan passed to this method is null
95       **/
96      public DateSpan(String dateSpan, TimeZone defaultTimezone)
97      {
98          // ensure this is a date we can accept
99          if (dateSpan == null) {
100             throw new NullPointerException("null dateSpan");
101         }
102 
103         DateFormat timestampFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
104         DateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd");
105         TimeZone timezone;
106 
107         if (defaultTimezone == null) {
108             timezone = TimeZone.getTimeZone("UTC"); // a simple, known timezone.
109         } else {
110             timezone = defaultTimezone;
111         }
112 
113         // we want dates accepted here to be exactly right
114         timestampFormat.setLenient(false);
115         dayFormat.setLenient(false);
116         timestampFormat.setTimeZone(timezone);
117         dayFormat.setTimeZone(timezone);
118 
119         Date date;
120 
121         try {
122             date = timestampFormat.parse(dateSpan);
123 
124             // parse succeeded, must be a timestamp
125             this.isDay = false;
126             this.fixedTime = dateSpan;
127             this.timezonedTime = date;
128             this.defaultTimezone = defaultTimezone;
129         } catch (ParseException pe) {
130             try {
131             	// set timestamp component for this date to 00:00:00 in this timezone
132             	date = timestampFormat.parse(dateSpan + "T00:00:00");
133                 //date = dayFormat.parse(dateSpan);
134                 
135                 // parse succeeded, must be a day span
136                 this.isDay = true;
137                 this.fixedTime = dateSpan;
138                 this.timezonedTime = date;
139                 this.defaultTimezone = defaultTimezone;
140             } catch (ParseException pe2) {
141                 throw new IllegalArgumentException("dateSpan '" + dateSpan +
142                     "' could not be parsed as a timestamp or day value");
143             }
144         }
145     }
146 
147     /** Specify a 24-hour period, starting at point (e.g. "today" spans 24 hours).
148      *
149      * @param dateSpanStart the time of this DateSpan (or the beginning, if a period)
150      * @param isDay set this to true if this DateSpan represents a 24 hour day, false otherwise
151      * @param defaultTimezone the timezone to interpret this date in, if we need to convert
152      *   it to string form.
153      *
154      * @throws NullPointerException if the dateSpanStart parameter passed to this method is null
155      */
156     public DateSpan(Date dateSpanStart, boolean isDay, TimeZone defaultTimezone)
157     {
158         if (dateSpanStart == null) {
159             throw new NullPointerException("null dateSpanStart");
160         }
161 
162         this.timezonedTime = dateSpanStart;
163         this.isDay = isDay;
164         this.defaultTimezone = defaultTimezone;
165     }
166 
167     /** Returns true for a  'fixed' DateSpan. See the class javadocs for details.
168      *
169      * @return true for a 'fixed' DateSpan
170      */
171     public boolean isFixed()
172     {
173         return (this.fixedTime != null);
174     }
175 
176     /** Returns true for a 24hour period. See the class javadocs for details
177      *
178      * @return true if this DateSpan represents a 24 hour period, false otherwise
179      */
180     public boolean isDay()
181     {
182         return this.isDay;
183     }
184 
185     /** Gets the 'fixed' representation of this DateSpan, if the DateSpan is fixed. This
186      * may return a day string (yyyy-MM-dd) or a day/time (yyyy-MM-dd'T'HH:mm:ss).
187      *
188      * @return the 'fixed' representation of this DateSpan, if the DateSpan is fixed
189      *
190      * @throws IllegalStateException if this DateSpan is not fixed.
191      */
192     public String getFixed()
193     {
194         if (!isFixed()) {
195             throw new IllegalStateException(
196                 "getFixed() only allowed on fixed DateSpan objects");
197         }
198 
199         return fixedTime;
200     }
201 
202     /** Returns the start of this DateSpan in yyyyMMdd format (presumably for comparing
203      *  against the value date column). If this is a timezoned DateSpan and no default
204      * timezone has been set, raises an IllegalStateException
205      *
206      * @return  the start of this DateSpan in yyyyMMdd format
207      *
208      * @throws IllegalStateException if no defaultTimezone has been specified for a timezoned DateSpan
209      */
210     public String getStartAsYyyymmdd()
211     {
212         if (isFixed()) {
213             // yyyy-MM-ddTHH:mm:ss
214             // 0123456789012345678
215             return fixedTime.substring(0, 4) + fixedTime.substring(5, 7) +
216             fixedTime.substring(8, 10);
217         } else {
218             if (defaultTimezone == null) {
219                 throw new IllegalStateException("Default timezone must be set");
220             }
221             SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
222             sdf.setTimeZone(defaultTimezone);
223             return sdf.format(timezonedTime);
224         }
225     }
226 
227     /** Returns the start of this DateSpan as a Date (milliseconds from UTC epoch).
228      * If this is a fixed DateSpan and no default timezone has been set, raises
229      * an IllegalStateException
230      *
231      * @return  the start of this DateSpan as a Date
232      *
233      * @throws IllegalStateException if no defaultTimezone has been specified for a fixed DateSpan
234      */
235     public Date getStartAsDate()
236     {
237         if (isFixed()) {
238             if (defaultTimezone == null) {
239                 throw new IllegalStateException("Default timezone must be set");
240             }
241             return timezonedTime;
242         } else {
243             return timezonedTime;
244         }
245     }
246 
247     /** Returns the end of this DateSpan in yyyyMMdd format (presumably for comparing
248      *  against the value date column). If this is a timezoned DateSpan and no default
249      * timezone has been set, raises an IllegalStateException
250      *
251      * @return  the end of this DateSpan in yyyyMMdd format
252      *
253      * @throws IllegalStateException if this DateSpan represents a point in time (not a duration),
254      *   or if no defaultTimezone has been specified for a timezoned DateSpan
255      */
256     public String getEndAsYyyymmdd()
257     {
258         if (!isDay) {
259             throw new IllegalStateException(
260                 "getEndAsString() only supported for time periods, not timestamps");
261         }
262 
263         if (isFixed()) {
264             Calendar cal = new GregorianCalendar(Integer.parseInt(fixedTime.substring(0, 4)),
265                     Integer.parseInt(fixedTime.substring(5, 7)),
266                     Integer.parseInt(fixedTime.substring(8, 10)));
267 
268             cal.add(Calendar.DAY_OF_YEAR, 1);
269 
270             int day = cal.get(Calendar.DAY_OF_MONTH);
271             int month = cal.get(Calendar.MONTH) + 1;
272             int year = cal.get(Calendar.YEAR);
273 
274             return ((day < 10 ? "0" : "") + day) + ((month < 10 ? "0" : "") + month) +
275             year;
276         } else {
277             if (defaultTimezone == null) {
278                 throw new IllegalStateException("Default timezone must be set");
279             }
280 
281             SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
282             sdf.setTimeZone(defaultTimezone);
283             return sdf.format(new Date(timezonedTime.getTime() + 86399999)); // 86400000ms in 1 day
284         }
285     }
286 
287     /** Returns the end of this DateSpan as a Date (milliseconds from UTC epoch).
288      * If this is a fixed DateSpan and no default timezone has been set, raises
289      * an IllegalStateException
290      *
291      * @return the end of this DateSpan as a Date
292      *
293      * @throws IllegalStateException if no defaultTimezone has been specified for a fixed DateSpan
294      */
295     public Date getEndAsDate()
296     {
297         if (isFixed()) {
298             if (defaultTimezone == null) {
299                 throw new IllegalStateException("Default timezone must be set");
300             }
301             return new Date(timezonedTime.getTime() + 86399999); // 86400000ms in 1 day
302         } else {
303             return new Date(timezonedTime.getTime() + 86399999); // 86400000ms in 1 day
304         }
305     }
306 
307     /** A string representation of this object, useful for debugging.
308      *
309      * @return a string representation of this object, useful for debugging.
310      */
311     public String toString()
312     {
313         if (isFixed()) {
314             return "(\"" + fixedTime + "\", isDay=" + isDay + ", tz=" +
315             (defaultTimezone == null ? "(null)" : defaultTimezone.getID()) +
316             ")";
317         } else {
318             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'");
319             sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
320             return "(t=" + sdf.format(timezonedTime) + ", isDay=" + isDay + ", tz=" +
321             (defaultTimezone == null ? "(null)" : defaultTimezone.getID()) +
322             ")";
323         }
324     }
325     
326     /** Inverse of toString(); required for {@link com.randomnoun.common.Text#parseStructOutput(String)} 
327      * to do it's thing.
328      * 
329      * @param string The text representation of this object (as returned by .toString())
330      * 
331      * @return an instance of a DateSpan
332      */
333     public static DateSpan valueOf(String text) throws ParseException {
334         if (!(text.startsWith("(") && text.endsWith(")"))) {
335             throw new ParseException("DateSpan representation must begin and end with brackets", 0);
336         }
337         text = text.substring(1, text.length()-1);
338         List<String> list = Text.parseCsv(text);
339         if (list.size()!=3) { throw new ParseException("Expected 3 elements in DateSpan representation", 0); }
340         String time = (String) list.get(0);
341         String isDay = (String) list.get(1);
342         String tz = (String) list.get(2);
343         TimeZone timezone = null; 
344         if (!isDay.startsWith("isDay=")) { throw new ParseException("Expected 'isDay=' in second element", 0); }
345         if (!tz.startsWith("tz=")) { throw new ParseException("Expected 'tz=' in third element", 0); }
346         if (!tz.equals("(null)")) { timezone = TimeZone.getTimeZone(tz.substring(3)); }
347           
348         if (time.startsWith("t=")) {
349             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'");
350             sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
351             Date date = sdf.parse(time.substring(2));
352             return new DateSpan(date, isDay.substring(6).equals("true"), timezone);
353                  
354         } else {
355             return new DateSpan(time, timezone);
356             
357         }
358     }
359     
360 
361     /** Returns true if the object passed to this method as an equivalent dateSpan instance
362      *  to the current object. (Used in unit testing only) 
363      * 
364      *  @param other the object we are comparing agianst
365      * 
366      *  @return true if the other object is identical to this one, false otherwise
367      */
368     public boolean equals(Object obj) {
369         if (!(obj instanceof DateSpan)) { return false; }
370         DateSpan other = (DateSpan) obj;
371         if ( ((fixedTime == null && other.fixedTime==null) ||
372               (fixedTime != null && fixedTime.equals(other.fixedTime))) &&
373              ((timezonedTime == null && other.timezonedTime==null) ||
374               (timezonedTime != null && timezonedTime.equals(other.timezonedTime))) &&
375              ((defaultTimezone == null && other.defaultTimezone==null) ||
376               (defaultTimezone != null && defaultTimezone.equals(other.defaultTimezone))) &&
377              isDay == other.isDay ) {
378             return true;
379         } else {
380             return false;
381         }
382     }
383 
384 
385 }