source: josm/trunk/src/org/openstreetmap/josm/tools/date/DateUtils.java@ 17715

Last change on this file since 17715 was 17715, checked in by simon04, 3 years ago

see #14176 - Migrate GPX to Instant

  • Property svn:eol-style set to native
File size: 15.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools.date;
3
4import java.text.DateFormat;
5import java.text.ParsePosition;
6import java.text.SimpleDateFormat;
7import java.time.DateTimeException;
8import java.time.Instant;
9import java.time.ZoneId;
10import java.time.ZoneOffset;
11import java.time.ZonedDateTime;
12import java.time.format.DateTimeFormatter;
13import java.time.format.DateTimeParseException;
14import java.time.format.FormatStyle;
15import java.util.Date;
16import java.util.Locale;
17import java.util.TimeZone;
18import java.util.concurrent.TimeUnit;
19
20import org.openstreetmap.josm.data.preferences.BooleanProperty;
21import org.openstreetmap.josm.tools.CheckParameterUtil;
22import org.openstreetmap.josm.tools.UncheckedParseException;
23
24/**
25 * A static utility class dealing with:
26 * <ul>
27 * <li>parsing XML date quickly and formatting a date to the XML UTC format regardless of current locale</li>
28 * <li>providing a single entry point for formatting dates to be displayed in JOSM GUI, based on user preferences</li>
29 * </ul>
30 * @author nenik
31 */
32public final class DateUtils {
33
34 /**
35 * The UTC time zone.
36 */
37 public static final TimeZone UTC = TimeZone.getTimeZone(ZoneOffset.UTC);
38
39 /**
40 * Property to enable display of ISO dates globally.
41 * @since 7299
42 */
43 public static final BooleanProperty PROP_ISO_DATES = new BooleanProperty("iso.dates", false);
44
45 /**
46 * Constructs a new {@code DateUtils}.
47 */
48 private DateUtils() {
49 // Hide default constructor for utils classes
50 }
51
52 /**
53 * Parses XML date quickly, regardless of current locale.
54 * @param str The XML date as string
55 * @return The date
56 * @throws UncheckedParseException if the date does not match any of the supported date formats
57 * @throws DateTimeException if the value of any field is out of range, or if the day-of-month is invalid for the month-year
58 */
59 public static Date fromString(String str) {
60 return new Date(tsFromString(str));
61 }
62
63 /**
64 * Parses XML date quickly, regardless of current locale.
65 * @param str The XML date as string
66 * @return The date in milliseconds since epoch
67 * @throws UncheckedParseException if the date does not match any of the supported date formats
68 * @throws DateTimeException if the value of any field is out of range, or if the day-of-month is invalid for the month-year
69 */
70 public static long tsFromString(String str) {
71 return parseInstant(str).toEpochMilli();
72 }
73
74 /**
75 * Parses the given date string quickly, regardless of current locale.
76 * @param str the date string
77 * @return the parsed instant
78 */
79 public static Instant parseInstant(String str) {
80 // "2007-07-25T09:26:24{Z|{+|-}01[:00]}"
81 if (checkLayout(str, "xxxx-xx-xx") ||
82 checkLayout(str, "xxxx-xx") ||
83 checkLayout(str, "xxxx")) {
84 final ZonedDateTime local = ZonedDateTime.of(
85 parsePart4(str, 0),
86 str.length() > 5 ? parsePart2(str, 5) : 1,
87 str.length() > 8 ? parsePart2(str, 8) : 1,
88 0, 0, 0, 0, ZoneOffset.UTC);
89 return local.toInstant();
90 } else if (checkLayout(str, "xxxx-xx-xxTxx:xx:xxZ") ||
91 checkLayout(str, "xxxx-xx-xxTxx:xx:xx") ||
92 checkLayout(str, "xxxx:xx:xx xx:xx:xx") ||
93 checkLayout(str, "xxxx-xx-xx xx:xx:xxZ") ||
94 checkLayout(str, "xxxx-xx-xx xx:xx:xx UTC") ||
95 checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx") ||
96 checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx") ||
97 checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx:00") ||
98 checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx:00")) {
99 final ZonedDateTime local = ZonedDateTime.of(
100 parsePart4(str, 0),
101 parsePart2(str, 5),
102 parsePart2(str, 8),
103 parsePart2(str, 11),
104 parsePart2(str, 14),
105 parsePart2(str, 17),
106 0,
107 ZoneOffset.UTC
108 );
109 if (str.length() == 22 || str.length() == 25) {
110 final int plusHr = parsePart2(str, 20);
111 return local.plusHours(str.charAt(19) == '+' ? -plusHr : plusHr).toInstant();
112 }
113 return local.toInstant();
114 } else if (checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxxZ") ||
115 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx") ||
116 checkLayout(str, "xxxx:xx:xx xx:xx:xx.xxx") ||
117 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx+xx:00") ||
118 checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx-xx:00")) {
119 final ZonedDateTime local = ZonedDateTime.of(
120 parsePart4(str, 0),
121 parsePart2(str, 5),
122 parsePart2(str, 8),
123 parsePart2(str, 11),
124 parsePart2(str, 14),
125 parsePart2(str, 17),
126 parsePart3(str, 20) * 1_000_000,
127 ZoneOffset.UTC
128 );
129 if (str.length() == 29) {
130 final int plusHr = parsePart2(str, 24);
131 return local.plusHours(str.charAt(23) == '+' ? -plusHr : plusHr).toInstant();
132 }
133 return local.toInstant();
134 } else {
135 // example date format "18-AUG-08 13:33:03"
136 SimpleDateFormat f = new SimpleDateFormat("dd-MMM-yy HH:mm:ss");
137 Date d = f.parse(str, new ParsePosition(0));
138 if (d != null)
139 return d.toInstant();
140 }
141
142 try {
143 // slow path for fractional seconds different from millisecond precision
144 return ZonedDateTime.parse(str).toInstant();
145 } catch (IllegalArgumentException | DateTimeParseException ex) {
146 throw new UncheckedParseException("The date string (" + str + ") could not be parsed.", ex);
147 }
148 }
149
150 /**
151 * Formats a date to the XML UTC format regardless of current locale.
152 * @param timestamp number of seconds since the epoch
153 * @return The formatted date
154 * @since 14055
155 */
156 public static String fromTimestamp(long timestamp) {
157 return fromTimestampInMillis(TimeUnit.SECONDS.toMillis(timestamp));
158 }
159
160 /**
161 * Formats a date to the XML UTC format regardless of current locale.
162 * @param timestamp number of milliseconds since the epoch
163 * @return The formatted date
164 * @since 14434
165 */
166 public static String fromTimestampInMillis(long timestamp) {
167 final ZonedDateTime temporal = Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.UTC);
168 return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(temporal);
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 String fromTimestamp(int timestamp) {
177 return fromTimestamp(Integer.toUnsignedLong(timestamp));
178 }
179
180 /**
181 * Formats a date to the XML UTC format regardless of current locale.
182 * @param date The date to format
183 * @return The formatted date
184 */
185 public static String fromDate(Date date) {
186 final ZonedDateTime temporal = date.toInstant().atZone(ZoneOffset.UTC);
187 return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(temporal);
188 }
189
190 /**
191 * Null-safe date cloning method.
192 * @param d date to clone, or null
193 * @return cloned date, or null
194 * @since 11878
195 */
196 public static Date cloneDate(Date d) {
197 return d != null ? (Date) d.clone() : null;
198 }
199
200 private static boolean checkLayout(String text, String pattern) {
201 if (text.length() != pattern.length())
202 return false;
203 for (int i = 0; i < pattern.length(); i++) {
204 char pc = pattern.charAt(i);
205 char tc = text.charAt(i);
206 if (pc == 'x' && Character.isDigit(tc))
207 continue;
208 else if (pc == 'x' || pc != tc)
209 return false;
210 }
211 return true;
212 }
213
214 private static int num(char c) {
215 return c - '0';
216 }
217
218 private static int parsePart2(String str, int off) {
219 return 10 * num(str.charAt(off)) + num(str.charAt(off + 1));
220 }
221
222 private static int parsePart3(String str, int off) {
223 return 100 * num(str.charAt(off)) + 10 * num(str.charAt(off + 1)) + num(str.charAt(off + 2));
224 }
225
226 private static int parsePart4(String str, int off) {
227 return 1000 * num(str.charAt(off)) + 100 * num(str.charAt(off + 1)) + 10 * num(str.charAt(off + 2)) + num(str.charAt(off + 3));
228 }
229
230 /**
231 * Returns a new {@code SimpleDateFormat} for date only, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
232 * @return a new ISO 8601 date format, for date only.
233 * @since 7299
234 */
235 public static SimpleDateFormat newIsoDateFormat() {
236 return new SimpleDateFormat("yyyy-MM-dd");
237 }
238
239 /**
240 * Returns a new {@code SimpleDateFormat} for date and time, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
241 * @return a new ISO 8601 date format, for date and time.
242 * @since 7299
243 */
244 public static SimpleDateFormat newIsoDateTimeFormat() {
245 return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
246 }
247
248 /**
249 * Returns a new {@code SimpleDateFormat} for date and time, according to format used in OSM API errors.
250 * @return a new date format, for date and time, to use for OSM API error handling.
251 * @since 7299
252 */
253 public static SimpleDateFormat newOsmApiDateTimeFormat() {
254 // Example: "2010-09-07 14:39:41 UTC".
255 // Always parsed with US locale regardless of the current locale in JOSM
256 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.US);
257 }
258
259 /**
260 * Returns the date format to be used for current user, based on user preferences.
261 * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set
262 * @return The date format
263 * @since 7299
264 */
265 public static DateFormat getDateFormat(int dateStyle) {
266 if (PROP_ISO_DATES.get()) {
267 return newIsoDateFormat();
268 } else {
269 return DateFormat.getDateInstance(dateStyle, Locale.getDefault());
270 }
271 }
272
273 /**
274 * Returns the date formatter to be used for current user, based on user preferences.
275 * @param dateStyle The date style. Ignored if "ISO dates" option is set.
276 * @return The date format
277 */
278 public static DateTimeFormatter getDateFormatter(FormatStyle dateStyle) {
279 DateTimeFormatter formatter = PROP_ISO_DATES.get()
280 ? DateTimeFormatter.ISO_LOCAL_DATE
281 : DateTimeFormatter.ofLocalizedDate(dateStyle);
282 return formatter.withZone(ZoneId.systemDefault());
283 }
284
285 /**
286 * Returns the date format used for GPX waypoints.
287 * @return the date format used for GPX waypoints
288 * @since 14055
289 * @deprecated Use {@link Instant#toString()}
290 */
291 @Deprecated
292 public static DateFormat getGpxFormat() {
293 SimpleDateFormat result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
294 result.setTimeZone(UTC);
295 return result;
296 }
297
298 /**
299 * Formats a date to be displayed to current user, based on user preferences.
300 * @param date The date to display. Must not be {@code null}
301 * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set
302 * @return The formatted date
303 * @since 7299
304 */
305 public static String formatDate(Date date, int dateStyle) {
306 CheckParameterUtil.ensureParameterNotNull(date, "date");
307 return getDateFormat(dateStyle).format(date);
308 }
309
310 /**
311 * Returns the time format to be used for current user, based on user preferences.
312 * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set
313 * @return The time format
314 * @since 7299
315 */
316 public static DateFormat getTimeFormat(int timeStyle) {
317 if (PROP_ISO_DATES.get()) {
318 // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm
319 return new SimpleDateFormat("HH:mm:ss");
320 } else {
321 return DateFormat.getTimeInstance(timeStyle, Locale.getDefault());
322 }
323 }
324
325 /**
326 * Returns the time formatter to be used for current user, based on user preferences.
327 * @param timeStyle The time style. Ignored if "ISO dates" option is set.
328 * @return The time format
329 */
330 public static DateTimeFormatter getTimeFormatter(FormatStyle timeStyle) {
331 DateTimeFormatter formatter = PROP_ISO_DATES.get()
332 ? DateTimeFormatter.ISO_LOCAL_TIME
333 : DateTimeFormatter.ofLocalizedTime(timeStyle);
334 return formatter.withZone(ZoneId.systemDefault());
335 }
336
337 /**
338 * Formats a time to be displayed to current user, based on user preferences.
339 * @param time The time to display. Must not be {@code null}
340 * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set
341 * @return The formatted time
342 * @since 7299
343 */
344 public static String formatTime(Date time, int timeStyle) {
345 CheckParameterUtil.ensureParameterNotNull(time, "time");
346 return getTimeFormat(timeStyle).format(time);
347 }
348
349 /**
350 * Returns the date/time format to be used for current user, based on user preferences.
351 * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set
352 * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set
353 * @return The date/time format
354 * @since 7299
355 */
356 public static DateFormat getDateTimeFormat(int dateStyle, int timeStyle) {
357 if (PROP_ISO_DATES.get()) {
358 // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm
359 // and we don't want to use the 'T' separator as a space character is much more readable
360 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
361 } else {
362 return DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.getDefault());
363 }
364 }
365
366 /**
367 * Returns the date/time formatter to be used for current user, based on user preferences.
368 * @param dateStyle The date style. Ignored if "ISO dates" option is set.
369 * @param timeStyle The time style. Ignored if "ISO dates" option is set.
370 * @return The date/time format
371 */
372 public static DateTimeFormatter getDateTimeFormatter(FormatStyle dateStyle, FormatStyle timeStyle) {
373 DateTimeFormatter formatter = PROP_ISO_DATES.get()
374 ? DateTimeFormatter.ISO_LOCAL_DATE_TIME
375 : DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle);
376 return formatter.withZone(ZoneId.systemDefault());
377 }
378
379 /**
380 * Formats a date/time to be displayed to current user, based on user preferences.
381 * @param datetime The date/time to display. Must not be {@code null}
382 * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set
383 * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set
384 * @return The formatted date/time
385 * @since 7299
386 */
387 public static String formatDateTime(Date datetime, int dateStyle, int timeStyle) {
388 CheckParameterUtil.ensureParameterNotNull(datetime, "datetime");
389 return getDateTimeFormat(dateStyle, timeStyle).format(datetime);
390 }
391}
Note: See TracBrowser for help on using the repository browser.