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 }