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
008 /*
009 Copyright (c) 2013, Michael Angstadt
010 All rights reserved.
011
012 Redistribution and use in source and binary forms, with or without
013 modification, are permitted provided that the following conditions are met:
014
015 1. Redistributions of source code must retain the above copyright notice, this
016 list of conditions and the following disclaimer.
017 2. Redistributions in binary form must reproduce the above copyright notice,
018 this list of conditions and the following disclaimer in the documentation
019 and/or other materials provided with the distribution.
020
021 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
022 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
023 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
024 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
025 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
026 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
027 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
028 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
029 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
030 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
031 */
032
033 /**
034 * Helper class that formats and parses iCalendar dates. iCalendar dates adhere
035 * to the ISO8601 date format standard.
036 * @author Michael Angstadt
037 */
038 public class ICalDateFormatter {
039 /**
040 * Formats a date for inclusion in an iCalendar object.
041 * @param date the date to format
042 * @param format the format to use
043 * @return the formatted date
044 */
045 public static String format(Date date, ISOFormat format) {
046 return format(date, format, null);
047 }
048
049 /**
050 * Formats a date for inclusion in an iCalendar object.
051 * @param date the date to format
052 * @param format the format to use
053 * @param timeZone the timezone to format the date in or null to use the
054 * JVM's default timezone (ignored with "UTC" formats)
055 * @return the formatted date
056 */
057 public static String format(Date date, ISOFormat format, TimeZone timeZone) {
058 switch (format) {
059 case UTC_TIME_BASIC:
060 case UTC_TIME_EXTENDED:
061 timeZone = TimeZone.getTimeZone("UTC");
062 break;
063 }
064
065 DateFormat df = format.getFormatDateFormat();
066 if (timeZone != null) {
067 df.setTimeZone(timeZone);
068 }
069 String str = df.format(date);
070
071 switch (format) {
072 case TIME_EXTENDED:
073 //add a colon to the timezone
074 //example: converts "2012-07-05T22:31:41-0400" to "2012-07-05T22:31:41-04:00"
075 str = str.replaceAll("([-\\+]\\d{2})(\\d{2})$", "$1:$2");
076 break;
077 }
078
079 return str;
080 }
081
082 /**
083 * Parses an iCalendar date.
084 * @param dateStr the date string to parse (e.g. "20130609T181023Z")
085 * @return the parsed date
086 * @throws IllegalArgumentException if the date string isn't in one of the
087 * accepted ISO8601 formats
088 */
089 public static Date parse(String dateStr) {
090 return parse(dateStr, null);
091 }
092
093 /**
094 * Parses an iCalendar date.
095 * @param dateStr the date string to parse (e.g. "20130609T181023Z")
096 * @param timezone the timezone to parse the date as or null to use the
097 * JVM's default timezone (if the date string contains its own timezone,
098 * then that timezone will be used instead)
099 * @return the parsed date
100 * @throws IllegalArgumentException if the date string isn't in one of the
101 * accepted ISO8601 formats
102 */
103 public static Date parse(String dateStr, TimeZone timezone) {
104 //find out what ISOFormat the date is in
105 ISOFormat format = null;
106 for (ISOFormat f : ISOFormat.values()) {
107 if (f.matches(dateStr)) {
108 format = f;
109 break;
110 }
111 }
112 if (format == null) {
113 throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format.");
114 }
115
116 //tweak the date string to make it work with SimpleDateFormat
117 switch (format) {
118 case TIME_EXTENDED:
119 case HCARD_TIME_TAG:
120 //SimpleDateFormat doesn't recognize timezone offsets that have colons
121 //so remove the colon from the timezone offset
122 dateStr = dateStr.replaceAll("([-\\+]\\d{2}):(\\d{2})$", "$1$2");
123 break;
124 case UTC_TIME_BASIC:
125 case UTC_TIME_EXTENDED:
126 //SimpleDateFormat doesn't recognize "Z"
127 dateStr = dateStr.replace("Z", "+0000");
128 break;
129 }
130
131 //parse the date
132 DateFormat df = format.getParseDateFormat();
133 if (timezone != null) {
134 df.setTimeZone(timezone);
135 }
136 try {
137 return df.parse(dateStr);
138 } catch (ParseException e) {
139 //should never be thrown because the string is checked against a regex
140 throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format.");
141 }
142 }
143
144 /**
145 * Determines whether a date string has a time component.
146 * @param dateStr the date string (e.g. "20130601T120000")
147 * @return true if it has a time component, false if not
148 */
149 public static boolean dateHasTime(String dateStr) {
150 return dateStr.contains("T");
151 }
152
153 /**
154 * Determines whether a date string is in UTC time or has a timezone offset.
155 * @param dateStr the date string (e.g. "20130601T120000Z",
156 * "20130601T120000-0400")
157 * @return true if it has a timezone, false if not
158 */
159 public static boolean dateHasTimezone(String dateStr) {
160 return dateStr.endsWith("Z") || dateStr.matches(".*?[-+]\\d\\d:?\\d\\d");
161 }
162
163 /**
164 * Gets the {@link TimeZone} object that corresponds to the given ID.
165 * @param timezoneId the timezone ID (e.g. "America/New_York")
166 * @return the timezone object or null if not found
167 */
168 public static TimeZone parseTimeZoneId(String timezoneId) {
169 TimeZone timezone = TimeZone.getTimeZone(timezoneId);
170 return "GMT".equals(timezone.getID()) ? null : timezone;
171 }
172
173 private ICalDateFormatter() {
174 //hide constructor
175 }
176 }