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