001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.date; 003 004import java.text.DateFormat; 005import java.text.ParsePosition; 006import java.text.SimpleDateFormat; 007import java.util.Calendar; 008import java.util.Date; 009import java.util.GregorianCalendar; 010import java.util.Locale; 011import java.util.TimeZone; 012 013import javax.xml.datatype.DatatypeConfigurationException; 014import javax.xml.datatype.DatatypeFactory; 015import javax.xml.datatype.XMLGregorianCalendar; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.data.preferences.BooleanProperty; 019import org.openstreetmap.josm.tools.CheckParameterUtil; 020import org.openstreetmap.josm.tools.UncheckedParseException; 021 022/** 023 * A static utility class dealing with: 024 * <ul> 025 * <li>parsing XML date quickly and formatting a date to the XML UTC format regardless of current locale</li> 026 * <li>providing a single entry point for formatting dates to be displayed in JOSM GUI, based on user preferences</li> 027 * </ul> 028 * @author nenik 029 */ 030public final class DateUtils { 031 032 /** 033 * The UTC time zone. 034 */ 035 public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); 036 037 /** 038 * Property to enable display of ISO dates globally. 039 * @since 7299 040 */ 041 public static final BooleanProperty PROP_ISO_DATES = new BooleanProperty("iso.dates", false); 042 043 /** 044 * A shared instance used for conversion between individual date fields 045 * and long millis time. It is guarded against conflict by the class lock. 046 * The shared instance is used because the construction, together 047 * with the timezone lookup, is very expensive. 048 */ 049 private static final GregorianCalendar calendar = new GregorianCalendar(UTC); 050 /** 051 * A shared instance to convert local times. The time zone should be set before every conversion. 052 */ 053 private static final GregorianCalendar calendarLocale = new GregorianCalendar(TimeZone.getDefault()); 054 private static final DatatypeFactory XML_DATE; 055 056 static { 057 calendar.setTimeInMillis(0); 058 calendarLocale.setTimeInMillis(0); 059 060 DatatypeFactory fact = null; 061 try { 062 fact = DatatypeFactory.newInstance(); 063 } catch (DatatypeConfigurationException ce) { 064 Main.error(ce); 065 } 066 XML_DATE = fact; 067 } 068 069 protected DateUtils() { 070 // Hide default constructor for utils classes 071 } 072 073 /** 074 * Parses XML date quickly, regardless of current locale. 075 * @param str The XML date as string 076 * @return The date 077 * @throws UncheckedParseException if the date does not match any of the supported date formats 078 */ 079 public static synchronized Date fromString(String str) { 080 return new Date(tsFromString(str)); 081 } 082 083 /** 084 * Parses XML date quickly, regardless of current locale. 085 * @param str The XML date as string 086 * @return The date in milliseconds since epoch 087 * @throws UncheckedParseException if the date does not match any of the supported date formats 088 */ 089 public static synchronized long tsFromString(String str) { 090 // "2007-07-25T09:26:24{Z|{+|-}01[:00]}" 091 if (checkLayout(str, "xxxx-xx-xxTxx:xx:xxZ") || 092 checkLayout(str, "xxxx-xx-xxTxx:xx:xx") || 093 checkLayout(str, "xxxx:xx:xx xx:xx:xx") || 094 checkLayout(str, "xxxx-xx-xx xx:xx:xx UTC") || 095 checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx") || 096 checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx") || 097 checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx:00") || 098 checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx:00")) { 099 final Calendar c; // consider EXIF date in default timezone 100 if (checkLayout(str, "xxxx:xx:xx xx:xx:xx")) { 101 c = getLocalCalendar(); 102 } else { 103 c = calendar; 104 } 105 c.set( 106 parsePart4(str, 0), 107 parsePart2(str, 5)-1, 108 parsePart2(str, 8), 109 parsePart2(str, 11), 110 parsePart2(str, 14), 111 parsePart2(str, 17)); 112 c.set(Calendar.MILLISECOND, 0); 113 114 if (str.length() == 22 || str.length() == 25) { 115 int plusHr = parsePart2(str, 20); 116 int mul = str.charAt(19) == '+' ? -3600000 : 3600000; 117 return c.getTimeInMillis()+plusHr*mul; 118 } 119 120 return c.getTimeInMillis(); 121 } else if (checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxxZ") || 122 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx") || 123 checkLayout(str, "xxxx:xx:xx xx:xx:xx.xxx") || 124 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx+xx:00") || 125 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx-xx:00")) { 126 // consider EXIF date in default timezone 127 final Calendar c = checkLayout(str, "xxxx:xx:xx xx:xx:xx.xxx") ? getLocalCalendar() : calendar; 128 c.set( 129 parsePart4(str, 0), 130 parsePart2(str, 5)-1, 131 parsePart2(str, 8), 132 parsePart2(str, 11), 133 parsePart2(str, 14), 134 parsePart2(str, 17)); 135 c.set(Calendar.MILLISECOND, 0); 136 long millis = parsePart3(str, 20); 137 if (str.length() == 29) { 138 millis += parsePart2(str, 24) * (str.charAt(23) == '+' ? -3600000 : 3600000); 139 } 140 141 return c.getTimeInMillis() + millis; 142 } else { 143 // example date format "18-AUG-08 13:33:03" 144 SimpleDateFormat f = new SimpleDateFormat("dd-MMM-yy HH:mm:ss"); 145 Date d = f.parse(str, new ParsePosition(0)); 146 if (d != null) 147 return d.getTime(); 148 } 149 150 try { 151 return XML_DATE.newXMLGregorianCalendar(str).toGregorianCalendar().getTimeInMillis(); 152 } catch (IllegalArgumentException ex) { 153 throw new UncheckedParseException("The date string (" + str + ") could not be parsed.", ex); 154 } 155 } 156 157 private static Calendar getLocalCalendar() { 158 final Calendar c = calendarLocale; 159 c.setTimeZone(TimeZone.getDefault()); 160 return c; 161 } 162 163 private static String toXmlFormat(GregorianCalendar cal) { 164 XMLGregorianCalendar xgc = XML_DATE.newXMLGregorianCalendar(cal); 165 if (cal.get(Calendar.MILLISECOND) == 0) { 166 xgc.setFractionalSecond(null); 167 } 168 return xgc.toXMLFormat(); 169 } 170 171 /** 172 * Formats a date to the XML UTC format regardless of current locale. 173 * @param timestamp number of seconds since the epoch 174 * @return The formatted date 175 */ 176 public static synchronized String fromTimestamp(int timestamp) { 177 calendar.setTimeInMillis(timestamp * 1000L); 178 return toXmlFormat(calendar); 179 } 180 181 /** 182 * Formats a date to the XML UTC format regardless of current locale. 183 * @param date The date to format 184 * @return The formatted date 185 */ 186 public static synchronized String fromDate(Date date) { 187 calendar.setTime(date); 188 return toXmlFormat(calendar); 189 } 190 191 private static boolean checkLayout(String text, String pattern) { 192 if (text.length() != pattern.length()) 193 return false; 194 for (int i = 0; i < pattern.length(); i++) { 195 char pc = pattern.charAt(i); 196 char tc = text.charAt(i); 197 if (pc == 'x' && Character.isDigit(tc)) 198 continue; 199 else if (pc == 'x' || pc != tc) 200 return false; 201 } 202 return true; 203 } 204 205 private static int num(char c) { 206 return c - '0'; 207 } 208 209 private static int parsePart2(String str, int off) { 210 return 10 * num(str.charAt(off)) + num(str.charAt(off + 1)); 211 } 212 213 private static int parsePart3(String str, int off) { 214 return 100 * num(str.charAt(off)) + 10 * num(str.charAt(off + 1)) + num(str.charAt(off + 2)); 215 } 216 217 private static int parsePart4(String str, int off) { 218 return 1000 * num(str.charAt(off)) + 100 * num(str.charAt(off + 1)) + 10 * num(str.charAt(off + 2)) + num(str.charAt(off + 3)); 219 } 220 221 /** 222 * Returns a new {@code SimpleDateFormat} for date only, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>. 223 * @return a new ISO 8601 date format, for date only. 224 * @since 7299 225 */ 226 public static SimpleDateFormat newIsoDateFormat() { 227 return new SimpleDateFormat("yyyy-MM-dd"); 228 } 229 230 /** 231 * Returns a new {@code SimpleDateFormat} for date and time, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>. 232 * @return a new ISO 8601 date format, for date and time. 233 * @since 7299 234 */ 235 public static SimpleDateFormat newIsoDateTimeFormat() { 236 return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); 237 } 238 239 /** 240 * Returns a new {@code SimpleDateFormat} for date and time, according to format used in OSM API errors. 241 * @return a new date format, for date and time, to use for OSM API error handling. 242 * @since 7299 243 */ 244 public static SimpleDateFormat newOsmApiDateTimeFormat() { 245 // Example: "2010-09-07 14:39:41 UTC". 246 // Always parsed with US locale regardless of the current locale in JOSM 247 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.US); 248 } 249 250 /** 251 * Returns the date format to be used for current user, based on user preferences. 252 * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set 253 * @return The date format 254 * @since 7299 255 */ 256 public static DateFormat getDateFormat(int dateStyle) { 257 if (PROP_ISO_DATES.get()) { 258 return newIsoDateFormat(); 259 } else { 260 return DateFormat.getDateInstance(dateStyle, Locale.getDefault()); 261 } 262 } 263 264 /** 265 * Formats a date to be displayed to current user, based on user preferences. 266 * @param date The date to display. Must not be {@code null} 267 * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set 268 * @return The formatted date 269 * @since 7299 270 */ 271 public static String formatDate(Date date, int dateStyle) { 272 CheckParameterUtil.ensureParameterNotNull(date, "date"); 273 return getDateFormat(dateStyle).format(date); 274 } 275 276 /** 277 * Returns the time format to be used for current user, based on user preferences. 278 * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set 279 * @return The time format 280 * @since 7299 281 */ 282 public static DateFormat getTimeFormat(int timeStyle) { 283 if (PROP_ISO_DATES.get()) { 284 // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm 285 return new SimpleDateFormat("HH:mm:ss"); 286 } else { 287 return DateFormat.getTimeInstance(timeStyle, Locale.getDefault()); 288 } 289 } 290 291 /** 292 * Formats a time to be displayed to current user, based on user preferences. 293 * @param time The time to display. Must not be {@code null} 294 * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set 295 * @return The formatted time 296 * @since 7299 297 */ 298 public static String formatTime(Date time, int timeStyle) { 299 CheckParameterUtil.ensureParameterNotNull(time, "time"); 300 return getTimeFormat(timeStyle).format(time); 301 } 302 303 /** 304 * Returns the date/time format to be used for current user, based on user preferences. 305 * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set 306 * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set 307 * @return The date/time format 308 * @since 7299 309 */ 310 public static DateFormat getDateTimeFormat(int dateStyle, int timeStyle) { 311 if (PROP_ISO_DATES.get()) { 312 // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm 313 // and we don't want to use the 'T' separator as a space character is much more readable 314 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 315 } else { 316 return DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.getDefault()); 317 } 318 } 319 320 /** 321 * Formats a date/time to be displayed to current user, based on user preferences. 322 * @param datetime The date/time to display. Must not be {@code null} 323 * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set 324 * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set 325 * @return The formatted date/time 326 * @since 7299 327 */ 328 public static String formatDateTime(Date datetime, int dateStyle, int timeStyle) { 329 CheckParameterUtil.ensureParameterNotNull(datetime, "datetime"); 330 return getDateTimeFormat(dateStyle, timeStyle).format(datetime); 331 } 332}