001 package ezvcard.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) 2012, 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 The views and conclusions contained in the software and documentation are those
035 of the authors and should not be interpreted as representing official policies,
036 either expressed or implied, of the FreeBSD Project.
037 */
038
039 /**
040 * Helper class that formats and parses vCard dates. vCard dates adhere to the
041 * ISO8601 date format standard.
042 * @author Michael Angstadt
043 */
044 public class VCardDateFormatter {
045 /**
046 * Regular expression used to parse timezone offset strings.
047 */
048 private static final Pattern timeZoneRegex = Pattern.compile("^([-\\+])?(\\d{1,2})(:?(\\d{2}))?$");
049
050 /**
051 * Formats a date for inclusion in a vCard.
052 * @param date the date to format
053 * @param format the format to use
054 * @return the formatted date
055 */
056 public static String format(Date date, ISOFormat format) {
057 return format(date, format, TimeZone.getDefault());
058 }
059
060 /**
061 * Formats a date for inclusion in a vCard.
062 * @param date the date to format
063 * @param format the format to use
064 * @param timeZone the time zone to format the date in. This will be ignored
065 * if the specified ISOFormat is a "UTC" format
066 * @return the formatted date
067 */
068 public static String format(Date date, ISOFormat format, TimeZone timeZone) {
069 switch (format) {
070 case UTC_TIME_BASIC:
071 case UTC_TIME_EXTENDED:
072 timeZone = TimeZone.getTimeZone("UTC");
073 break;
074 }
075
076 DateFormat df = format.getFormatDateFormat();
077 df.setTimeZone(timeZone);
078 String str = df.format(date);
079
080 switch (format) {
081 case TIME_EXTENDED:
082 //add a colon to the timezone
083 //example: converts "2012-07-05T22:31:41-0400" to "2012-07-05T22:31:41-04:00"
084 str = str.replaceAll("([-\\+]\\d{2})(\\d{2})$", "$1:$2");
085 break;
086 }
087
088 return str;
089 }
090
091 /**
092 * Parses a vCard date.
093 * @param dateStr the date string to parse
094 * @return the parsed date
095 * @throws IllegalArgumentException if the date string isn't in one of the
096 * accepted ISO8601 formats
097 */
098 public static Date parse(String dateStr) {
099 //find out what ISOFormat the date is in
100 ISOFormat format = null;
101 for (ISOFormat f : ISOFormat.values()) {
102 if (f.matches(dateStr)) {
103 format = f;
104 break;
105 }
106 }
107 if (format == null) {
108 throw new IllegalArgumentException("Date string is not in a valid ISO-8601 format.");
109 }
110
111 //tweak the date string to make it work with SimpleDateFormat
112 switch (format) {
113 case TIME_EXTENDED:
114 case HCARD_TIME_TAG:
115 //SimpleDateFormat doesn't recognize timezone offsets that have colons
116 //so remove the colon from the timezone offset
117 dateStr = dateStr.replaceAll("([-\\+]\\d{2}):(\\d{2})$", "$1$2");
118 break;
119 case UTC_TIME_BASIC:
120 case UTC_TIME_EXTENDED:
121 //SimpleDateFormat doesn't recognize "Z"
122 dateStr = dateStr.replace("Z", "+0000");
123 break;
124 }
125
126 //parse the date
127 DateFormat df = format.getParseDateFormat();
128 try {
129 return df.parse(dateStr);
130 } catch (ParseException e) {
131 //should never be thrown because the string is checked against a regex
132 return null;
133 }
134 }
135
136 /**
137 * Parses a timezone that's in ISO8601 format.
138 * @param offsetStr the timezone offset string (e.g. "-0500" or "-05:00")
139 * @return the hour offset (index 0) and the minute offset (index 1)
140 * @throws IllegalArgumentException if the timezone string isn't in the
141 * right format
142 */
143 public static int[] parseTimeZone(String offsetStr) {
144 Matcher m = timeZoneRegex.matcher(offsetStr);
145
146 if (!m.find()) {
147 throw new IllegalArgumentException("Offset string is not in ISO8610 format: " + offsetStr);
148 }
149
150 String sign = m.group(1);
151 boolean positive;
152 if ("-".equals(sign)) {
153 positive = false;
154 } else {
155 positive = true;
156 }
157
158 String hourStr = m.group(2);
159 int hourOffset = Integer.parseInt(hourStr);
160 if (!positive) {
161 hourOffset *= -1;
162 }
163
164 String minuteStr = m.group(4);
165 int minuteOffset = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr);
166
167 return new int[] { hourOffset, minuteOffset };
168 }
169
170 /**
171 * Formats a {@link TimeZone} object according to ISO8601 rules.
172 *
173 * @param timeZone the timezone to format
174 * @param extended true to use "extended" format, false not to. Extended
175 * format will put a colon between the hour and minute.
176 * @return the formatted timezone (e.g. "+0530" or "+05:30")
177 */
178 public static String formatTimeZone(TimeZone timeZone, boolean extended) {
179 int hours = timeZone.getRawOffset() / 1000 / 60 / 60;
180 int minutes = Math.abs((timeZone.getRawOffset() / 1000) / 60) % 60;
181 return formatTimeZone(hours, minutes, extended);
182 }
183
184 /**
185 * Formats a timezone offset according to ISO8601 rules.
186 *
187 * @param hourOffset the hour offset
188 * @param minuteOffset the minute offset (must be between 0 and 59)
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(int hourOffset, int minuteOffset, boolean extended) {
194 if (minuteOffset < 0 || minuteOffset > 59) {
195 throw new IllegalArgumentException("Minute offset must be between 0 and 59.");
196 }
197
198 StringBuilder sb = new StringBuilder();
199 boolean positive = hourOffset >= 0;
200
201 sb.append(positive ? '+' : '-');
202
203 hourOffset = Math.abs(hourOffset);
204 if (hourOffset < 10) {
205 sb.append('0');
206 }
207 sb.append(hourOffset);
208
209 if (extended) {
210 sb.append(':');
211 }
212
213 if (minuteOffset < 10) {
214 sb.append('0');
215 }
216 sb.append(minuteOffset);
217
218 return sb.toString();
219 }
220
221 private VCardDateFormatter() {
222 //hide constructor
223 }
224 }