001 /*
002 * JBoss, Home of Professional Open Source.
003 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004 * as indicated by the @author tags. See the copyright.txt file in the
005 * distribution for a full listing of individual contributors.
006 *
007 * This is free software; you can redistribute it and/or modify it
008 * under the terms of the GNU Lesser General Public License as
009 * published by the Free Software Foundation; either version 2.1 of
010 * the License, or (at your option) any later version.
011 *
012 * This software is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this software; if not, write to the Free
019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021 */
022 package org.jboss.dna.common.util;
023
024 import java.text.DateFormat;
025 import java.text.ParseException;
026 import java.text.SimpleDateFormat;
027 import java.util.Calendar;
028 import java.util.Date;
029 import java.util.Locale;
030 import java.util.regex.Matcher;
031 import java.util.regex.Pattern;
032 import net.jcip.annotations.ThreadSafe;
033 import org.jboss.dna.common.CommonI18n;
034
035 /**
036 * Utilities for working with dates.
037 * <p>
038 * Many of the methods that convert dates to and from strings utilize the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO
039 * 8601:2004</a> standard string format <code>yyyy-MM-ddTHH:mm:ss.SSSZ</code>, where <blockquote>
040 *
041 * <pre>
042 * Symbol Meaning Presentation Example
043 * ------ ------- ------------ -------
044 * y year (Number) 1996
045 * M month in year (Number) 07
046 * d day in month (Number) 10
047 * h hour in am/pm (1˜12) (Number) 12
048 * H hour in day (0˜23) (Number) 0
049 * m minute in hour (Number) 30
050 * s second in minute (Number) 55
051 * S millisecond (Number) 978
052 * Z time zone (Number) -0600
053 * </pre>
054 *
055 * </blockquote>
056 * </p>
057 * <p>
058 * This class is written to be thread safe. As {@link SimpleDateFormat} is not threadsafe, no shared instances are used.
059 * </p>
060 * @author Randall Hauch
061 */
062 @ThreadSafe
063 public class DateUtil {
064
065 public static final String ISO_8601_2004_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
066
067 /**
068 * Parse the date contained in the supplied string. The date must follow one of the standard ISO 8601 formats, of the form
069 * <code><i>datepart</i>T<i>timepart</i></code>, where <code><i>datepart</i></code> is one of the following forms:
070 * <p>
071 * <dl>
072 * <dt>YYYYMMDD</dt>
073 * <dd>The 4-digit year, the 2-digit month (00-12), and the 2-digit day of the month (00-31). The month and day are optional,
074 * but the month is required if the day is given.</dd>
075 * <dt>YYYY-MM-DD</dt>
076 * <dd>The 4-digit year, the 2-digit month (00-12), and the 2-digit day of the month (00-31). The month and day are optional,
077 * but the month is required if the day is given.</dd>
078 * <dt>YYYY-Www-D</dt>
079 * <dd>The 4-digit year followed by 'W', the 2-digit week number (00-53), and the day of the week (1-7). The day of week
080 * number is optional.</dd>
081 * <dt>YYYYWwwD</dt>
082 * <dd>The 4-digit year followed by 'W', the 2-digit week number (00-53), and the day of the week (1-7). The day of week
083 * number is optional.</dd>
084 * <dt>YYYY-DDD</dt>
085 * <dd>The 4-digit year followed by the 3-digit day of the year (000-365)</dd>
086 * <dt>YYYYDDD</dt>
087 * <dd>The 4-digit year followed by the 3-digit day of the year (000-365)</dd>
088 * </dl>
089 * </p>
090 * <p>
091 * The <code><i>timepart</i></code> consists of one of the following forms that contain the 2-digit hour (00-24), the
092 * 2-digit minutes (00-59), the 2-digit seconds (00-59), and the 1-to-3 digit milliseconds. The minutes, seconds and
093 * milliseconds are optional, but any component is required if it is followed by another component (e.g., minutes are required
094 * if the seconds are given).
095 * <dl>
096 * <dt>hh:mm:ss.SSS</dt>
097 * <dt>hhmmssSSS</dt>
098 * </dl>
099 * </p>
100 * <p>
101 * followed by one of the following time zone definitions:
102 * <dt>Z</dt>
103 * <dd>The uppercase or lowercase 'Z' to denote UTC time</dd>
104 * <dt>±hh:mm</dt>
105 * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
106 * <dt>±hhmm</dt>
107 * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
108 * <dt>±hh</dt>
109 * <dd>The 2-digit hour offset from UTC</dd>
110 * <dt>hh:mm</dt>
111 * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
112 * <dt>hhmm</dt>
113 * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
114 * <dt>hh</dt>
115 * <dd>The 2-digit hour offset from UTC</dd>
116 * </dl>
117 * </p>
118 * @param dateString the string containing the date to be parsed
119 * @return the parsed date as a {@link Calendar} object.
120 * @throws ParseException if there is a problem parsing the string
121 */
122 public static Calendar getCalendarFromStandardString( final String dateString ) throws ParseException {
123 // Example: 2008-02-16T12:30:45.123-0600
124 // Example: 2008-W06-6
125 // Example: 2008-053
126 //
127 // Group Optional Field Description
128 // ----- -------- --------- ------------------------------------------
129 // 1 no 2008 4 digit year as a number
130 // 2 yes 02-16 or W06-6 or 053
131 // 3 yes W06-6
132 // 4 yes 06 2 digit week number (00-59)
133 // 5 yes 6 1 digit day of week as a number (1-7)
134 // 6 yes 02-16
135 // 7 yes 02 2 digit month as a number (00-19)
136 // 8 yes -16
137 // 9 yes 16 2 digit day of month as a number (00-39)
138 // 10 yes 02 2 digit month as a number (00-19)
139 // 11 yes 16 2 digit day of month as a number (00-39)
140 // 12 yes 234 3 digit day of year as a number (000-399)
141 // 13 yes T12:30:45.123-0600
142 // 14 yes 12 2 digit hour as a number (00-29)
143 // 15 yes 30 2 digit minute as a number (00-59)
144 // 16 yes :45.123
145 // 17 yes 45 2 digit second as a number (00-59)
146 // 18 yes .123
147 // 19 yes 123 1, 2 or 3 digit milliseconds as a number (000-999)
148 // 20 yes -0600
149 // 21 yes Z The letter 'Z' if in UTC
150 // 22 yes -06 1 or 2 digit time zone hour offset as a signed number
151 // 23 yes + the plus or minus in the time zone offset
152 // 24 yes 00 1 or 2 digit time zone hour offset as an unsigned number (00-29)
153 // 25 yes 00 1 or 2 digit time zone minute offset as a number (00-59)
154 final String regex =
155 "^(\\d{4})-?(([wW]([012345]\\d)-?([1234567])?)|(([01]\\d)(-([0123]\\d))?)|([01]\\d)([0123]\\d)|([0123]\\d\\d))?(T([012]\\d):?([012345]\\d)(:?([012345]\\d)(.(\\d{1,3}))?)?((Z)|(([+-])(\\d{2})):?(\\d{2})?)?)?$";
156 final Pattern pattern = Pattern.compile(regex);
157 final Matcher matcher = pattern.matcher(dateString);
158 if (!matcher.matches()) {
159 throw new ParseException(CommonI18n.dateParsingFailure.text(dateString), 0);
160 }
161 String year = matcher.group(1);
162 String week = matcher.group(4);
163 String dayOfWeek = matcher.group(5);
164 String month = matcher.group(7);
165 if (month == null) month = matcher.group(10);
166 String dayOfMonth = matcher.group(9);
167 if (dayOfMonth == null) dayOfMonth = matcher.group(11);
168 String dayOfYear = matcher.group(12);
169 String hourOfDay = matcher.group(14);
170 String minutesOfHour = matcher.group(15);
171 String seconds = matcher.group(17);
172 String milliseconds = matcher.group(19);
173 String timeZoneSign = matcher.group(23);
174 String timeZoneHour = matcher.group(24);
175 String timeZoneMinutes = matcher.group(25);
176 if (matcher.group(21) != null) {
177 timeZoneHour = "00";
178 timeZoneMinutes = "00";
179 }
180
181 // Create the calendar object and start setting the fields ...
182 Calendar calendar = Calendar.getInstance();
183 calendar.clear();
184
185 // And start setting the fields. Note that Integer.parseInt should never fail, since we're checking for null and the
186 // regular expression should only have digits in these strings!
187 if (year != null) calendar.set(Calendar.YEAR, Integer.parseInt(year));
188 if (month != null) {
189 calendar.set(Calendar.MONTH, Integer.parseInt(month) - 1); // month is zero-based!
190 if (dayOfMonth != null) calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dayOfMonth));
191 } else if (week != null) {
192 calendar.set(Calendar.WEEK_OF_YEAR, Integer.parseInt(week));
193 if (dayOfWeek != null) calendar.set(Calendar.DAY_OF_WEEK, Integer.parseInt(dayOfWeek));
194 } else if (dayOfYear != null) {
195 calendar.set(Calendar.DAY_OF_YEAR, Integer.parseInt(dayOfYear));
196 }
197 if (hourOfDay != null) calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hourOfDay));
198 if (minutesOfHour != null) calendar.set(Calendar.MINUTE, Integer.parseInt(minutesOfHour));
199 if (seconds != null) calendar.set(Calendar.SECOND, Integer.parseInt(seconds));
200 if (milliseconds != null) calendar.set(Calendar.MILLISECOND, Integer.parseInt(milliseconds));
201 if (timeZoneHour != null) {
202 int zoneOffsetInMillis = Integer.parseInt(timeZoneHour) * 60 * 60 * 1000;
203 if ("-".equals(timeZoneSign)) zoneOffsetInMillis *= -1;
204 if (timeZoneMinutes != null) {
205 int minuteOffsetInMillis = Integer.parseInt(timeZoneMinutes) * 60 * 1000;
206 if (zoneOffsetInMillis < 0) {
207 zoneOffsetInMillis -= minuteOffsetInMillis;
208 } else {
209 zoneOffsetInMillis += minuteOffsetInMillis;
210 }
211 }
212 calendar.set(Calendar.ZONE_OFFSET, zoneOffsetInMillis);
213 }
214 return calendar;
215 }
216
217 /**
218 * Parse the date contained in the supplied string. This method simply calls {@link Calendar#getTime()} on the result of
219 * {@link #getCalendarFromStandardString(String)}.
220 * @param dateString the string containing the date to be parsed
221 * @return the parsed date as a {@link Calendar} object.
222 * @throws ParseException if there is a problem parsing the string
223 * @see #getCalendarFromStandardString(String)
224 */
225 public static Date getDateFromStandardString( String dateString ) throws ParseException {
226 return getCalendarFromStandardString(dateString).getTime();
227 }
228
229 /**
230 * Obtain an ISO 8601:2004 string representation of the date given the supplied milliseconds since the epoch.
231 * @param millisecondsSinceEpoch the milliseconds for the date
232 * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
233 * @see #getDateAsStandardString(Date)
234 * @see #getDateFromStandardString(String)
235 * @see #getCalendarFromStandardString(String)
236 */
237 public static String getDateAsStandardString( final long millisecondsSinceEpoch ) {
238 return getDateAsStandardString(new Date(millisecondsSinceEpoch));
239 }
240
241 /**
242 * Obtain an ISO 8601:2004 string representation of the date given the supplied milliseconds since the epoch.
243 * @param date the date in calendar form
244 * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
245 * @see #getDateAsStandardString(Date)
246 * @see #getDateFromStandardString(String)
247 * @see #getCalendarFromStandardString(String)
248 */
249 public static String getDateAsStandardString( final Calendar date ) {
250 return getDateAsStandardString(date.getTime());
251 }
252
253 /**
254 * Obtain an ISO 8601:2004 string representation of the supplied date.
255 * @param date the date
256 * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
257 * @see #getDateAsStandardString(long)
258 * @see #getDateFromStandardString(String)
259 * @see #getCalendarFromStandardString(String)
260 */
261 public static String getDateAsStandardString( final java.util.Date date ) {
262 return new SimpleDateFormat(ISO_8601_2004_FORMAT).format(date);
263 }
264
265 public static String getDateAsStringForCurrentLocale( final java.util.Date date ) {
266 return getDateAsStringForLocale(date, Locale.getDefault());
267 }
268
269 public static String getDateAsStringForLocale( final java.util.Date date, Locale locale ) {
270 return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(date);
271 }
272
273 private DateUtil() {
274 // Prevent instantiation
275 }
276
277 }