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}