001package com.randomnoun.common.jexl; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.io.Serializable; 008import java.text.*; 009import java.util.*; 010 011import com.randomnoun.common.Text; 012 013/** 014 * Data object representing a point in time, or a 24-hour day period. 015 * 016 * <p>You should probably use something in joda or the java 8 time package these days 017 * 018 * <p>DateSpans may be <i>fixed</i>, which refer to dates that do not necessarily have timezones 019 * (e.g. a valueDate, represented with the fixed date "2004-11-07" is always the 7th of November, 020 * regardless of where in the world this may be interpreted). Note that 021 * Date objects are stored internally as milliseconds since the Unix epoch UTC, and therefore 022 * do *not* qualify as 'fixed' dates. Anything that isn't fixed I refer to as 'timezoned' 023 * in the documentation here (because they only make sense when interpretted in a 024 * specific timezone). 025 * 026 * <p>Examples of DateSpans: 027 * <pre> 028 * new DateSpan("2004-11-07") - fixed day span (representing November 7, 2004, 24hr period)) 029 * new DateSpan("2004-11-07T20:00:00") - fixed timestamp (representing November 7, 2004, 8:00pm) 030 * new DateSpan( 031 * DateUtils.getStartOfDay(new Date(), 032 * TimeZone.getDefault()), true) - timezoned day span (representing today in local time (24hr period)) 033 * new DateSpan(new Date()) - timezoned timestamp (representing right now in local time) 034 * </pre> 035 * 036 * <p>Note that only yyyy-MM-dd or yyyy-MM-dd'T'HH:mm:ss (without timezone) string formats are permitted; 037 * this is more strict than that allowed by ISO8901, so I do not use that class here. 038 * 039 * <p>Note that timestamps are only precise down to 1 second. 040 * 041 * <p>'fixed' times may also be assigned default timezones, which can be used when comparing 042 * timezoned values with fixed DateSpans; e.g. the RUNDATE for Market Center "New York" may be 043 * specified as "2004-11-07" (and compared with valueDates with that same fixed date), 044 * but when compared against a SystemArrivalTime, needs to be interpretted in a time zone (in 045 * this case, TimeZone.getTimezone("America/New_York")). This can be specified at construction 046 * time, e.g. 047 * <pre style="code"> 048 * new DateSpan("2004-11-07", TimeZone.getTimezone("America/New_York")) 049 * </pre> 050 * 051 * <p>Note that if a timezone is not supplied for a 'fixed' time, then any call to {@link #getStartAsDate()} 052 * or {@link #getEndAsDate()} will return an IllegalStateException. 053 * 054 * <p>This class only supports the 'fixed day span', 'timezoned day span' or 'timezoned timestamp' 055 * examples above. Could extend this later on to 'fixed timestamp' values. 056 * Similarly, this class only supports 24hour spans, but could support more arbitrary durations 057 * if this was needed further down the track. 058 * 059 * <p>Only years between 1000 and 9999 will be handled correctly by this class. 060 * 061 * @author knoxg 062 */ 063public class DateSpan 064 implements Serializable 065{ 066 /** Generated serialVersionUID */ 067 private static final long serialVersionUID = 4189707923685011603L; 068 069 /** Contains the string representation of a 'fixed' timestamp or duration (or null if this is a timezoned DateSpan) */ 070 private String fixedTime; 071 072 /** Contains a 'timezoned' Date, or for fixed values, contains the Date of 'fixedTime' in the defaultTimeZone */ 073 private Date timezonedTime; 074 075 /** Is true if this DateSpan represents a 24hour period, or false for a 1 second instant. */ 076 boolean isDay; 077 078 /** For 'fixed' DateSpans, specifies a default timezone to interpret this in (if we need to) */ 079 private TimeZone defaultTimezone; 080 081 /** A single timestamp instant, without timezone. Equivalent to DateSpan(Date, false, null) */ 082 public DateSpan(Date timestamp) { 083 this(timestamp, false, null); 084 } 085 086 /** Create a 'timezoneless' date. A defaultTimezone may be supplied, which will be used 087 * when converting to Date objects. 088 * 089 * @param dateSpan A date in yyyy-MM-dd or yyyy-MM-dd'T'HH:mm:ss format (the former 090 * specifies a day period, the latter specifies a single instant 091 * @param defaultTimezone A default timezone to use when converting to Date objects. 092 * This parameter may be null. 093 * 094 * @throws NullPointerException if the dateSpan passed to this method is null 095 **/ 096 public DateSpan(String dateSpan, TimeZone defaultTimezone) 097 { 098 // ensure this is a date we can accept 099 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}