001 package biweekly.util; 002 003 import java.text.DateFormat; 004 import java.text.ParseException; 005 import java.util.Date; 006 import java.util.TimeZone; 007 import java.util.regex.Matcher; 008 import java.util.regex.Pattern; 009 010 /* 011 Copyright (c) 2013, Michael Angstadt 012 All rights reserved. 013 014 Redistribution and use in source and binary forms, with or without 015 modification, are permitted provided that the following conditions are met: 016 017 1. Redistributions of source code must retain the above copyright notice, this 018 list of conditions and the following disclaimer. 019 2. Redistributions in binary form must reproduce the above copyright notice, 020 this list of conditions and the following disclaimer in the documentation 021 and/or other materials provided with the distribution. 022 023 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 024 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 025 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 026 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 027 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 028 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 029 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 030 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 031 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 032 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 033 */ 034 035 /** 036 * Helper class that formats and parses iCalendar dates. iCalendar dates adhere 037 * to the ISO8601 date format standard. 038 * @author Michael Angstadt 039 */ 040 public class ICalDateFormatter { 041 /** 042 * Regular expression used to parse timezone offset strings. 043 */ 044 private static final Pattern timeZoneRegex = Pattern.compile("^([-\\+])?(\\d{1,2})(:?(\\d{2}))?$"); 045 046 /** 047 * Formats a date for inclusion in an iCalendar object. 048 * @param date the date to format 049 * @param format the format to use 050 * @return the formatted date 051 */ 052 public static String format(Date date, ISOFormat format) { 053 return format(date, format, null); 054 } 055 056 /** 057 * Formats a date for inclusion in an iCalendar object. 058 * @param date the date to format 059 * @param format the format to use 060 * @param timeZone the timezone to format the date in or null to use the 061 * JVM's default timezone (ignored with "UTC" formats) 062 * @return the formatted date 063 */ 064 public static String format(Date date, ISOFormat format, TimeZone timeZone) { 065 switch (format) { 066 case UTC_TIME_BASIC: 067 case UTC_TIME_EXTENDED: 068 timeZone = TimeZone.getTimeZone("UTC"); 069 break; 070 } 071 072 DateFormat df = format.getFormatDateFormat(); 073 if (timeZone != null) { 074 df.setTimeZone(timeZone); 075 } 076 String str = df.format(date); 077 078 switch (format) { 079 case TIME_EXTENDED: 080 //add a colon to the timezone 081 //example: converts "2012-07-05T22:31:41-0400" to "2012-07-05T22:31:41-04:00" 082 str = str.replaceAll("([-\\+]\\d{2})(\\d{2})$", "$1:$2"); 083 break; 084 } 085 086 return str; 087 } 088 089 /** 090 * Parses an iCalendar date. 091 * @param dateStr the date string to parse (e.g. "20130609T181023Z") 092 * @return the parsed date 093 * @throws IllegalArgumentException if the date string isn't in one of the 094 * accepted ISO8601 formats 095 */ 096 public static Date parse(String dateStr) { 097 return parse(dateStr, null); 098 } 099 100 /** 101 * Parses an iCalendar date. 102 * @param dateStr the date string to parse (e.g. "20130609T181023Z") 103 * @param timezone the timezone to parse the date as or null to use the 104 * JVM's default timezone (if the date string contains its own timezone, 105 * then that timezone will be used instead) 106 * @return the parsed date 107 * @throws IllegalArgumentException if the date string isn't in one of the 108 * accepted ISO8601 formats 109 */ 110 public static Date parse(String dateStr, TimeZone timezone) { 111 //find out what ISOFormat the date is in 112 ISOFormat format = null; 113 for (ISOFormat f : ISOFormat.values()) { 114 if (f.matches(dateStr)) { 115 format = f; 116 break; 117 } 118 } 119 if (format == null) { 120 throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format."); 121 } 122 123 //tweak the date string to make it work with SimpleDateFormat 124 switch (format) { 125 case TIME_EXTENDED: 126 case HCARD_TIME_TAG: 127 //SimpleDateFormat doesn't recognize timezone offsets that have colons 128 //so remove the colon from the timezone offset 129 dateStr = dateStr.replaceAll("([-\\+]\\d{2}):(\\d{2})$", "$1$2"); 130 break; 131 case UTC_TIME_BASIC: 132 case UTC_TIME_EXTENDED: 133 //SimpleDateFormat doesn't recognize "Z" 134 dateStr = dateStr.replace("Z", "+0000"); 135 break; 136 } 137 138 //parse the date 139 DateFormat df = format.getParseDateFormat(); 140 if (timezone != null) { 141 df.setTimeZone(timezone); 142 } 143 try { 144 return df.parse(dateStr); 145 } catch (ParseException e) { 146 //should never be thrown because the string is checked against a regex 147 throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format."); 148 } 149 } 150 151 /** 152 * Parses a timezone that's in ISO8601 format. 153 * @param offsetStr the timezone offset string (e.g. "-0500" or "-05:00") 154 * @return the hour offset (index 0) and the minute offset (index 1) 155 * @throws IllegalArgumentException if the timezone string isn't in the 156 * right format 157 */ 158 public static int[] parseTimeZone(String offsetStr) { 159 Matcher m = timeZoneRegex.matcher(offsetStr); 160 161 if (!m.find()) { 162 throw new IllegalArgumentException("Offset string is not in ISO8610 format: " + offsetStr); 163 } 164 165 String sign = m.group(1); 166 boolean positive; 167 if ("-".equals(sign)) { 168 positive = false; 169 } else { 170 positive = true; 171 } 172 173 String hourStr = m.group(2); 174 int hourOffset = Integer.parseInt(hourStr); 175 if (!positive) { 176 hourOffset *= -1; 177 } 178 179 String minuteStr = m.group(4); 180 int minuteOffset = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr); 181 182 return new int[] { hourOffset, minuteOffset }; 183 } 184 185 /** 186 * Formats a {@link TimeZone} object according to ISO8601 rules. 187 * 188 * @param timeZone the timezone to format 189 * @param extended true to use "extended" format, false not to. Extended 190 * format will put a colon between the hour and minute. 191 * @return the formatted timezone (e.g. "+0530" or "+05:30") 192 */ 193 public static String formatTimeZone(TimeZone timeZone, boolean extended) { 194 int hours = timeZone.getRawOffset() / 1000 / 60 / 60; 195 int minutes = Math.abs((timeZone.getRawOffset() / 1000) / 60) % 60; 196 return formatTimeZone(hours, minutes, extended); 197 } 198 199 /** 200 * Formats a timezone offset according to ISO8601 rules. 201 * 202 * @param hourOffset the hour offset 203 * @param minuteOffset the minute offset (between 0 and 59) 204 * @param extended true to use "extended" format, false not to. Extended 205 * format will put a colon between the hour and minute. 206 * @return the formatted timezone (e.g. "+0530" or "+05:30") 207 */ 208 public static String formatTimeZone(int hourOffset, int minuteOffset, boolean extended) { 209 StringBuilder sb = new StringBuilder(); 210 boolean positive = hourOffset >= 0; 211 212 sb.append(positive ? '+' : '-'); 213 214 hourOffset = Math.abs(hourOffset); 215 if (hourOffset < 10) { 216 sb.append('0'); 217 } 218 sb.append(hourOffset); 219 220 if (extended) { 221 sb.append(':'); 222 } 223 224 if (minuteOffset < 10) { 225 sb.append('0'); 226 } 227 sb.append(minuteOffset); 228 229 return sb.toString(); 230 } 231 232 /** 233 * Gets the {@link TimeZone} object that corresponds to the given ID. 234 * @param timezoneId the timezone ID (e.g. "America/New_York") 235 * @return the timezone object or null if not found 236 */ 237 public static TimeZone parseTimeZoneId(String timezoneId) { 238 TimeZone timezone = TimeZone.getTimeZone(timezoneId); 239 return "GMT".equals(timezone.getID()) ? null : timezone; 240 } 241 242 private ICalDateFormatter() { 243 //hide constructor 244 } 245 }