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.ZoneId; 010import java.time.ZoneOffset; 011import java.time.ZonedDateTime; 012import java.time.format.DateTimeFormatter; 013import java.time.format.DateTimeFormatterBuilder; 014import java.time.format.DateTimeParseException; 015import java.time.format.FormatStyle; 016import java.util.Date; 017import java.util.Locale; 018import java.util.TimeZone; 019import java.util.concurrent.TimeUnit; 020 021import org.openstreetmap.josm.data.preferences.BooleanProperty; 022import org.openstreetmap.josm.tools.CheckParameterUtil; 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 /** 047 * Constructs a new {@code DateUtils}. 048 */ 049 private DateUtils() { 050 // Hide default constructor for utils classes 051 } 052 053 /** 054 * Parses XML date quickly, regardless of current locale. 055 * @param str The XML date as string 056 * @return The date 057 * @throws UncheckedParseException if the date does not match any of the supported date formats 058 * @throws DateTimeException if the value of any field is out of range, or if the day-of-month is invalid for the month-year 059 */ 060 public static Date fromString(String str) { 061 return new Date(tsFromString(str)); 062 } 063 064 /** 065 * Parses XML date quickly, regardless of current locale. 066 * @param str The XML date as string 067 * @return The date in milliseconds since epoch 068 * @throws UncheckedParseException if the date does not match any of the supported date formats 069 * @throws DateTimeException if the value of any field is out of range, or if the day-of-month is invalid for the month-year 070 */ 071 public static long tsFromString(String str) { 072 return parseInstant(str).toEpochMilli(); 073 } 074 075 /** 076 * Parses the given date string quickly, regardless of current locale. 077 * @param str the date string 078 * @return the parsed instant 079 * @throws UncheckedParseException if the date does not match any of the supported date formats 080 */ 081 public static Instant parseInstant(String str) { 082 // "2007-07-25T09:26:24{Z|{+|-}01[:00]}" 083 if (checkLayout(str, "xxxx-xx-xx") || 084 checkLayout(str, "xxxx-xx") || 085 checkLayout(str, "xxxx")) { 086 final ZonedDateTime local = ZonedDateTime.of( 087 parsePart4(str, 0), 088 str.length() > 5 ? parsePart2(str, 5) : 1, 089 str.length() > 8 ? parsePart2(str, 8) : 1, 090 0, 0, 0, 0, ZoneOffset.UTC); 091 return local.toInstant(); 092 } else if (checkLayout(str, "xxxx-xx-xxTxx:xx:xxZ") || 093 checkLayout(str, "xxxx-xx-xxTxx:xx:xx") || 094 checkLayout(str, "xxxx:xx:xx xx:xx:xx") || 095 checkLayout(str, "xxxx/xx/xx xx:xx:xx") || 096 checkLayout(str, "xxxx-xx-xx xx:xx:xxZ") || 097 checkLayout(str, "xxxx-xx-xx xx:xx:xx UTC") || 098 checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx") || 099 checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx") || 100 checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx:00") || 101 checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx:00")) { 102 final ZonedDateTime local = ZonedDateTime.of( 103 parsePart4(str, 0), 104 parsePart2(str, 5), 105 parsePart2(str, 8), 106 parsePart2(str, 11), 107 parsePart2(str, 14), 108 parsePart2(str, 17), 109 0, 110 ZoneOffset.UTC 111 ); 112 if (str.length() == 22 || str.length() == 25) { 113 final int plusHr = parsePart2(str, 20); 114 return local.plusHours(str.charAt(19) == '+' ? -plusHr : plusHr).toInstant(); 115 } 116 return local.toInstant(); 117 } else if (checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxxZ") || 118 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx") || 119 checkLayout(str, "xxxx:xx:xx xx:xx:xx.xxx") || 120 checkLayout(str, "xxxx/xx/xx xx:xx:xx.xxx") || 121 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx+xx:00") || 122 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx-xx:00")) { 123 final ZonedDateTime local = ZonedDateTime.of( 124 parsePart4(str, 0), 125 parsePart2(str, 5), 126 parsePart2(str, 8), 127 parsePart2(str, 11), 128 parsePart2(str, 14), 129 parsePart2(str, 17), 130 parsePart3(str, 20) * 1_000_000, 131 ZoneOffset.UTC 132 ); 133 if (str.length() == 29) { 134 final int plusHr = parsePart2(str, 24); 135 return local.plusHours(str.charAt(23) == '+' ? -plusHr : plusHr).toInstant(); 136 } 137 return local.toInstant(); 138 } else if (checkLayout(str, "xxxx/xx/xx xx:xx:xx.xxxxxx")) { 139 return ZonedDateTime.of( 140 parsePart4(str, 0), 141 parsePart2(str, 5), 142 parsePart2(str, 8), 143 parsePart2(str, 11), 144 parsePart2(str, 14), 145 parsePart2(str, 17), 146 parsePart6(str, 20) * 1_000, 147 ZoneOffset.UTC 148 ).toInstant(); 149 } else { 150 // example date format "18-AUG-08 13:33:03" 151 SimpleDateFormat f = new SimpleDateFormat("dd-MMM-yy HH:mm:ss"); 152 Date d = f.parse(str, new ParsePosition(0)); 153 if (d != null) 154 return d.toInstant(); 155 } 156 157 try { 158 // slow path for fractional seconds different from millisecond precision 159 return ZonedDateTime.parse(str).toInstant(); 160 } catch (IllegalArgumentException | DateTimeParseException ex) { 161 throw new UncheckedParseException("The date string (" + str + ") could not be parsed.", ex); 162 } 163 } 164 165 /** 166 * Formats a date to the XML UTC format regardless of current locale. 167 * @param timestamp number of seconds since the epoch 168 * @return The formatted date 169 * @since 14055 170 */ 171 public static String fromTimestamp(long timestamp) { 172 return fromTimestampInMillis(TimeUnit.SECONDS.toMillis(timestamp)); 173 } 174 175 /** 176 * Formats a date to the XML UTC format regardless of current locale. 177 * @param timestamp number of milliseconds since the epoch 178 * @return The formatted date 179 * @since 14434 180 */ 181 public static String fromTimestampInMillis(long timestamp) { 182 final ZonedDateTime temporal = Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC); 183 return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(temporal); 184 } 185 186 /** 187 * Formats a date to the XML UTC format regardless of current locale. 188 * @param timestamp number of seconds since the epoch 189 * @return The formatted date 190 */ 191 public static String fromTimestamp(int timestamp) { 192 return fromTimestamp(Integer.toUnsignedLong(timestamp)); 193 } 194 195 /** 196 * Formats a date to the XML UTC format regardless of current locale. 197 * @param date The date to format 198 * @return The formatted date 199 */ 200 public static String fromDate(Date date) { 201 final ZonedDateTime temporal = date.toInstant().atZone(ZoneOffset.UTC); 202 return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(temporal); 203 } 204 205 /** 206 * Null-safe date cloning method. 207 * @param d date to clone, or null 208 * @return cloned date, or null 209 * @since 11878 210 */ 211 public static Date cloneDate(Date d) { 212 return d != null ? (Date) d.clone() : null; 213 } 214 215 private static boolean checkLayout(String text, String pattern) { 216 if (text.length() != pattern.length()) 217 return false; 218 for (int i = 0; i < pattern.length(); i++) { 219 char pc = pattern.charAt(i); 220 char tc = text.charAt(i); 221 if (pc == 'x' && Character.isDigit(tc)) 222 continue; 223 else if (pc == 'x' || pc != tc) 224 return false; 225 } 226 return true; 227 } 228 229 private static int num(char c) { 230 return c - '0'; 231 } 232 233 private static int parsePart2(String str, int off) { 234 return 10 * num(str.charAt(off)) + num(str.charAt(off + 1)); 235 } 236 237 private static int parsePart3(String str, int off) { 238 return 100 * num(str.charAt(off)) + 10 * num(str.charAt(off + 1)) + num(str.charAt(off + 2)); 239 } 240 241 private static int parsePart4(String str, int off) { 242 return 1000 * num(str.charAt(off)) + 100 * num(str.charAt(off + 1)) + 10 * num(str.charAt(off + 2)) + num(str.charAt(off + 3)); 243 } 244 245 private static int parsePart6(String str, int off) { 246 return 100000 * num(str.charAt(off)) 247 + 10000 * num(str.charAt(off + 1)) 248 + 1000 * num(str.charAt(off + 2)) 249 + 100 * num(str.charAt(off + 3)) 250 + 10 * num(str.charAt(off + 4)) 251 + num(str.charAt(off + 5)); 252 } 253 254 /** 255 * Returns a new {@code SimpleDateFormat} for date only, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>. 256 * @return a new ISO 8601 date format, for date only. 257 * @since 7299 258 */ 259 public static SimpleDateFormat newIsoDateFormat() { 260 return new SimpleDateFormat("yyyy-MM-dd"); 261 } 262 263 /** 264 * Returns the date format to be used for current user, based on user preferences. 265 * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set 266 * @return The date format 267 * @since 7299 268 */ 269 public static DateFormat getDateFormat(int dateStyle) { 270 if (PROP_ISO_DATES.get()) { 271 return newIsoDateFormat(); 272 } else { 273 return DateFormat.getDateInstance(dateStyle, Locale.getDefault()); 274 } 275 } 276 277 /** 278 * Returns the date formatter to be used for current user, based on user preferences. 279 * @param dateStyle The date style. Ignored if "ISO dates" option is set. 280 * @return The date format 281 */ 282 public static DateTimeFormatter getDateFormatter(FormatStyle dateStyle) { 283 DateTimeFormatter formatter = PROP_ISO_DATES.get() 284 ? DateTimeFormatter.ISO_LOCAL_DATE 285 : DateTimeFormatter.ofLocalizedDate(dateStyle); 286 return formatter.withZone(ZoneId.systemDefault()); 287 } 288 289 /** 290 * Formats a date to be displayed to current user, based on user preferences. 291 * @param date The date to display. Must not be {@code null} 292 * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set 293 * @return The formatted date 294 * @since 7299 295 */ 296 public static String formatDate(Date date, int dateStyle) { 297 CheckParameterUtil.ensureParameterNotNull(date, "date"); 298 return getDateFormat(dateStyle).format(date); 299 } 300 301 /** 302 * Returns the time format to be used for current user, based on user preferences. 303 * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set 304 * @return The time format 305 * @since 7299 306 */ 307 public static DateFormat getTimeFormat(int timeStyle) { 308 if (PROP_ISO_DATES.get()) { 309 // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm 310 return new SimpleDateFormat("HH:mm:ss"); 311 } else { 312 return DateFormat.getTimeInstance(timeStyle, Locale.getDefault()); 313 } 314 } 315 316 /** 317 * Returns the time formatter to be used for current user, based on user preferences. 318 * @param timeStyle The time style. Ignored if "ISO dates" option is set. 319 * @return The time format 320 */ 321 public static DateTimeFormatter getTimeFormatter(FormatStyle timeStyle) { 322 DateTimeFormatter formatter = PROP_ISO_DATES.get() 323 ? DateTimeFormatter.ISO_LOCAL_TIME 324 : DateTimeFormatter.ofLocalizedTime(timeStyle); 325 return formatter.withZone(ZoneId.systemDefault()); 326 } 327 328 /** 329 * Formats a time to be displayed to current user, based on user preferences. 330 * @param time The time to display. Must not be {@code null} 331 * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set 332 * @return The formatted time 333 * @since 7299 334 */ 335 public static String formatTime(Date time, int timeStyle) { 336 CheckParameterUtil.ensureParameterNotNull(time, "time"); 337 return getTimeFormat(timeStyle).format(time); 338 } 339 340 /** 341 * Returns the date/time format to be used for current user, based on user preferences. 342 * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set 343 * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set 344 * @return The date/time format 345 * @since 7299 346 */ 347 public static DateFormat getDateTimeFormat(int dateStyle, int timeStyle) { 348 if (PROP_ISO_DATES.get()) { 349 // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm 350 // and we don't want to use the 'T' separator as a space character is much more readable 351 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 352 } else { 353 return DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.getDefault()); 354 } 355 } 356 357 /** 358 * Differs from {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME} by using ' ' instead of 'T' to separate date/time. 359 */ 360 private static final DateTimeFormatter ISO_LOCAL_DATE_TIME = new DateTimeFormatterBuilder() 361 .parseCaseInsensitive() 362 .append(DateTimeFormatter.ISO_LOCAL_DATE) 363 .appendLiteral(' ') 364 .append(DateTimeFormatter.ISO_LOCAL_TIME) 365 .toFormatter(); 366 367 /** 368 * Returns the date/time formatter to be used for current user, based on user preferences. 369 * @param dateStyle The date style. Ignored if "ISO dates" option is set. 370 * @param timeStyle The time style. Ignored if "ISO dates" option is set. 371 * @return The date/time format 372 */ 373 public static DateTimeFormatter getDateTimeFormatter(FormatStyle dateStyle, FormatStyle timeStyle) { 374 DateTimeFormatter formatter = PROP_ISO_DATES.get() 375 ? ISO_LOCAL_DATE_TIME 376 : DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); 377 return formatter.withZone(ZoneId.systemDefault()); 378 } 379 380 /** 381 * Formats a date/time to be displayed to current user, based on user preferences. 382 * @param datetime The date/time to display. Must not be {@code null} 383 * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set 384 * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set 385 * @return The formatted date/time 386 * @since 7299 387 */ 388 public static String formatDateTime(Date datetime, int dateStyle, int timeStyle) { 389 CheckParameterUtil.ensureParameterNotNull(datetime, "datetime"); 390 return getDateTimeFormat(dateStyle, timeStyle).format(datetime); 391 } 392}