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 }