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