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