001package biweekly.util; 002 003import java.text.DateFormat; 004import java.text.FieldPosition; 005import java.text.ParseException; 006import java.text.SimpleDateFormat; 007import java.util.Date; 008import java.util.TimeZone; 009import java.util.regex.Pattern; 010 011/* 012 Copyright (c) 2013-2015, Michael Angstadt 013 All rights reserved. 014 015 Redistribution and use in source and binary forms, with or without 016 modification, are permitted provided that the following conditions are met: 017 018 1. Redistributions of source code must retain the above copyright notice, this 019 list of conditions and the following disclaimer. 020 2. Redistributions in binary form must reproduce the above copyright notice, 021 this list of conditions and the following disclaimer in the documentation 022 and/or other materials provided with the distribution. 023 024 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 025 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 026 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 027 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 028 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 029 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 030 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 031 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 032 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 033 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 034 */ 035 036/** 037 * Defines all of the date formats that are used in iCalendar objects, and also 038 * parses/formats iCalendar dates. These date formats are defined in the ISO8601 039 * specification. 040 * @author Michael Angstadt 041 */ 042public enum ICalDateFormat { 043 //@formatter:off 044 /** 045 * Example: 20120701 046 */ 047 DATE_BASIC( 048 "\\d{8}", 049 "yyyyMMdd"), 050 051 /** 052 * Example: 2012-07-01 053 */ 054 DATE_EXTENDED( 055 "\\d{4}-\\d{2}-\\d{2}", 056 "yyyy-MM-dd"), 057 058 /** 059 * Example: 20120701T142110-0500 060 */ 061 DATE_TIME_BASIC( 062 "\\d{8}T\\d{6}[-\\+]\\d{4}", 063 "yyyyMMdd'T'HHmmssZ"), 064 065 /** 066 * Example: 20120701T142110 067 */ 068 DATE_TIME_BASIC_WITHOUT_TZ( 069 "\\d{8}T\\d{6}", 070 "yyyyMMdd'T'HHmmss"), 071 072 /** 073 * Example: 2012-07-01T14:21:10-05:00 074 */ 075 DATE_TIME_EXTENDED( 076 "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[-\\+]\\d{2}:\\d{2}", 077 "yyyy-MM-dd'T'HH:mm:ssZ"){ 078 @SuppressWarnings("serial") 079 @Override 080 public DateFormat getDateFormat(TimeZone timezone) { 081 DateFormat df = new SimpleDateFormat(formatStr){ 082 @Override 083 public Date parse(String str) throws ParseException { 084 //remove the colon from the timezone offset 085 //SimpleDateFormat doesn't recognize timezone offsets that have colons 086 int index = str.lastIndexOf(':'); 087 str = str.substring(0, index) + str.substring(index+1); 088 089 return super.parse(str); 090 } 091 092 @Override 093 public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition){ 094 StringBuffer sb = super.format(date, toAppendTo, fieldPosition); 095 096 //add a colon between the hour and minute offsets 097 sb.insert(sb.length()-2, ':'); 098 099 return sb; 100 } 101 }; 102 103 if (timezone != null){ 104 df.setTimeZone(timezone); 105 } 106 107 return df; 108 } 109 }, 110 111 /** 112 * Example: 2012-07-01T14:21:10 113 */ 114 DATE_TIME_EXTENDED_WITHOUT_TZ( 115 "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", 116 "yyyy-MM-dd'T'HH:mm:ss"), 117 118 /** 119 * Example: 20120701T192110Z 120 */ 121 UTC_TIME_BASIC( 122 "\\d{8}T\\d{6}Z", 123 "yyyyMMdd'T'HHmmss'Z'"){ 124 @Override 125 public DateFormat getDateFormat(TimeZone timezone) { 126 //always use the UTC timezone 127 timezone = TimeZone.getTimeZone("UTC"); 128 return super.getDateFormat(timezone); 129 } 130 }, 131 132 /** 133 * Example: 2012-07-01T19:21:10Z 134 */ 135 UTC_TIME_EXTENDED( 136 "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z", 137 "yyyy-MM-dd'T'HH:mm:ss'Z'"){ 138 @Override 139 public DateFormat getDateFormat(TimeZone timezone) { 140 //always use the UTC timezone 141 timezone = TimeZone.getTimeZone("UTC"); 142 return super.getDateFormat(timezone); 143 } 144 }; 145 //@formatter:on 146 147 /** 148 * The regular expression pattern for the date format. 149 */ 150 private final Pattern pattern; 151 152 /** 153 * The {@link SimpleDateFormat} format string used for parsing dates. 154 */ 155 protected final String formatStr; 156 157 /** 158 * @param regex the regular expression for the date format 159 * @param formatStr the {@link SimpleDateFormat} format string used for 160 * parsing dates. 161 */ 162 private ICalDateFormat(String regex, String formatStr) { 163 pattern = Pattern.compile(regex); 164 this.formatStr = formatStr; 165 } 166 167 /** 168 * Determines whether a date string is in this ISO format. 169 * @param dateStr the date string 170 * @return true if it matches the date format, false if not 171 */ 172 public boolean matches(String dateStr) { 173 return pattern.matcher(dateStr).matches(); 174 } 175 176 /** 177 * Builds a {@link DateFormat} object for parsing and formating dates in 178 * this ISO format. 179 * @return the {@link DateFormat} object 180 */ 181 public DateFormat getDateFormat() { 182 return getDateFormat(null); 183 } 184 185 /** 186 * Builds a {@link DateFormat} object for parsing and formating dates in 187 * this ISO format. 188 * @param timezone the timezone the date is in or null for the default 189 * timezone 190 * @return the {@link DateFormat} object 191 */ 192 public DateFormat getDateFormat(TimeZone timezone) { 193 DateFormat df = new SimpleDateFormat(formatStr); 194 if (timezone != null) { 195 df.setTimeZone(timezone); 196 } 197 return df; 198 } 199 200 /** 201 * Formats a date in this ISO format. 202 * @param date the date to format 203 * @return the date string 204 */ 205 public String format(Date date) { 206 return format(date, null); 207 } 208 209 /** 210 * Formats a date in this ISO format. 211 * @param date the date to format 212 * @param timezone the timezone to format the date in or null for the 213 * default timezone 214 * @return the date string 215 */ 216 public String format(Date date, TimeZone timezone) { 217 DateFormat df = getDateFormat(timezone); 218 return df.format(date); 219 } 220 221 /** 222 * Determines the ISO format a date string is in. 223 * @param dateStr the date string (e.g. "20140322T120000Z") 224 * @return the ISO format (e.g. DATETIME_BASIC) or null if not found 225 */ 226 public static ICalDateFormat find(String dateStr) { 227 for (ICalDateFormat format : values()) { 228 if (format.matches(dateStr)) { 229 return format; 230 } 231 } 232 return null; 233 } 234 235 /** 236 * Parses an iCalendar date. 237 * @param dateStr the date string to parse (e.g. "20130609T181023Z") 238 * @return the parsed date 239 * @throws IllegalArgumentException if the date string isn't in one of the 240 * accepted ISO8601 formats 241 */ 242 public static Date parse(String dateStr) { 243 return parse(dateStr, null); 244 } 245 246 /** 247 * Parses an iCalendar date. 248 * @param dateStr the date string to parse (e.g. "20130609T181023Z") 249 * @param timezone the timezone to parse the date as or null to use the 250 * JVM's default timezone (if the date string contains its own timezone, 251 * then that timezone will be used instead) 252 * @return the parsed date 253 * @throws IllegalArgumentException if the date string isn't in one of the 254 * accepted ISO8601 formats 255 */ 256 public static Date parse(String dateStr, TimeZone timezone) { 257 //determine which ISOFormat the date is in 258 ICalDateFormat format = find(dateStr); 259 if (format == null) { 260 throw parseException(dateStr); 261 } 262 263 //parse the date 264 DateFormat df = format.getDateFormat(timezone); 265 try { 266 return df.parse(dateStr); 267 } catch (ParseException e) { 268 //should never be thrown because the string is checked against a regex before being parsed 269 throw parseException(dateStr); 270 } 271 } 272 273 /** 274 * Determines whether a date string has a time component. 275 * @param dateStr the date string (e.g. "20130601T120000") 276 * @return true if it has a time component, false if not 277 */ 278 public static boolean dateHasTime(String dateStr) { 279 return dateStr.contains("T"); 280 } 281 282 /** 283 * Determines whether a date string is in UTC time or has a timezone offset. 284 * @param dateStr the date string (e.g. "20130601T120000Z", 285 * "20130601T120000-0400") 286 * @return true if it has a timezone, false if not 287 */ 288 public static boolean dateHasTimezone(String dateStr) { 289 return isUTC(dateStr) || dateStr.matches(".*?[-+]\\d\\d:?\\d\\d"); 290 } 291 292 /** 293 * Determines if a date string is in UTC time. 294 * @param dateStr the date string (e.g. "20130601T120000Z") 295 * @return true if it's in UTC, false if not 296 */ 297 public static boolean isUTC(String dateStr) { 298 return dateStr.endsWith("Z"); 299 } 300 301 /** 302 * Gets the {@link TimeZone} object that corresponds to the given ID. 303 * @param timezoneId the timezone ID (e.g. "America/New_York") 304 * @return the timezone object or null if not found 305 */ 306 public static TimeZone parseTimeZoneId(String timezoneId) { 307 TimeZone timezone = TimeZone.getTimeZone(timezoneId); 308 return "GMT".equals(timezone.getID()) && !"GMT".equalsIgnoreCase(timezoneId) ? null : timezone; 309 } 310 311 private static IllegalArgumentException parseException(String dateStr) { 312 return new IllegalArgumentException("Date string \"" + dateStr + "\" is not in a valid ISO-8601 format."); 313 } 314}