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}