]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/data/Timestamp.java
623d80497eb995e36073e31e6067926192e9c626
[GpsPrune.git] / tim / prune / data / Timestamp.java
1 package tim.prune.data;
2
3 import java.text.DateFormat;
4 import java.text.ParsePosition;
5 import java.text.SimpleDateFormat;
6 import java.util.Calendar;
7 import java.util.Date;
8 import java.util.TimeZone;
9 import java.util.regex.Matcher;
10 import java.util.regex.Pattern;
11
12 /**
13  * Class to hold the timestamp of a track point
14  * and provide conversion functions
15  */
16 public class Timestamp
17 {
18         private boolean _valid = false;
19         private long _milliseconds = 0L;
20         private String _text = null;
21         private String _timeText = null;
22
23         private static final DateFormat DEFAULT_DATE_FORMAT = DateFormat.getDateTimeInstance();
24         private static final DateFormat DEFAULT_TIME_FORMAT = DateFormat.getTimeInstance();
25         private static final DateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
26         private static final DateFormat ISO_8601_FORMAT_NOZ = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
27         private static DateFormat[] ALL_DATE_FORMATS = null;
28         private static Calendar CALENDAR = null;
29         private static final Pattern ISO8601_FRACTIONAL_PATTERN
30                 = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:[\\.,](\\d{1,3}))?(Z|[\\+-]\\d{2}(?::?\\d{2})?)?");
31             //                    year     month     day T  hour    minute    sec             millisec   Z or +/-  hours  :   minutes
32         private static final Pattern GENERAL_TIMESTAMP_PATTERN
33                 = Pattern.compile("(\\d{4})\\D(\\d{2})\\D(\\d{2})\\D(\\d{2})\\D(\\d{2})\\D(\\d{2})");
34         private static long SECS_SINCE_1970 = 0L;
35         private static long SECS_SINCE_GARTRIP = 0L;
36         private static long MSECS_SINCE_1970 = 0L;
37         private static long MSECS_SINCE_1990 = 0L;
38         private static long TWENTY_YEARS_IN_SECS = 0L;
39         private static final long GARTRIP_OFFSET = 631065600L;
40
41         /** Specifies original timestamp format */
42         public static final int FORMAT_ORIGINAL = 0;
43         /** Specifies locale-dependent timestamp format */
44         public static final int FORMAT_LOCALE = 1;
45         /** Specifies ISO 8601 timestamp format */
46         public static final int FORMAT_ISO_8601 = 2;
47
48         /** Identifier for the parsing strategy to use */
49         private enum ParseType
50         {
51                 NONE,
52                 ISO8601_FRACTIONAL,
53                 LONG,
54                 FIXED_FORMAT0,
55                 FIXED_FORMAT1,
56                 FIXED_FORMAT2,
57                 FIXED_FORMAT3,
58                 FIXED_FORMAT4,
59                 FIXED_FORMAT5,
60                 FIXED_FORMAT6,
61                 GENERAL_STRING
62         }
63
64         /** Array of parse types to loop through (first one is changed to last successful type) */
65         private static ParseType[] ALL_PARSE_TYPES = {ParseType.NONE, ParseType.ISO8601_FRACTIONAL, ParseType.LONG,
66                 ParseType.FIXED_FORMAT0, ParseType.FIXED_FORMAT1, ParseType.FIXED_FORMAT2, ParseType.FIXED_FORMAT3,
67                 ParseType.FIXED_FORMAT4, ParseType.FIXED_FORMAT5, ParseType.FIXED_FORMAT6, ParseType.GENERAL_STRING};
68
69         // Static block to initialise offsets
70         static
71         {
72                 CALENDAR = Calendar.getInstance();
73                 TimeZone gmtZone = TimeZone.getTimeZone("GMT");
74                 CALENDAR.setTimeZone(gmtZone);
75                 MSECS_SINCE_1970 = CALENDAR.getTimeInMillis();
76                 SECS_SINCE_1970 = MSECS_SINCE_1970 / 1000L;
77                 SECS_SINCE_GARTRIP = SECS_SINCE_1970 - GARTRIP_OFFSET;
78                 CALENDAR.add(Calendar.YEAR, -20);
79                 MSECS_SINCE_1990 = CALENDAR.getTimeInMillis();
80                 TWENTY_YEARS_IN_SECS = (MSECS_SINCE_1970 - MSECS_SINCE_1990) / 1000L;
81                 // Set timezone for output
82                 ISO_8601_FORMAT.setTimeZone(gmtZone);
83                 DEFAULT_DATE_FORMAT.setTimeZone(gmtZone);
84                 // Date formats
85                 ALL_DATE_FORMATS = new DateFormat[] {
86                         DEFAULT_DATE_FORMAT,
87                         new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy"),
88                         new SimpleDateFormat("HH:mm:ss dd MMM yyyy"),
89                         new SimpleDateFormat("dd MMM yyyy HH:mm:ss"),
90                         new SimpleDateFormat("yyyy MMM dd HH:mm:ss"),
91                         ISO_8601_FORMAT, ISO_8601_FORMAT_NOZ
92                 };
93         }
94
95
96         /**
97          * Constructor
98          * @param inString String containing timestamp
99          */
100         public Timestamp(String inString)
101         {
102                 _valid = false;
103                 if (inString != null && !inString.equals(""))
104                 {
105                         // Try each of the parse types in turn
106                         for (ParseType type : ALL_PARSE_TYPES)
107                         {
108                                 if (parseString(inString, type))
109                                 {
110                                         ALL_PARSE_TYPES[0] = type;
111                                         _valid = true;
112                                         return;
113                                 }
114                         }
115                 }
116         }
117
118         /**
119          * Try to parse the given string in the specified way
120          * @param inString String to parse
121          * @param inType parse type to use
122          * @return true if successful
123          */
124         private boolean parseString(String inString, ParseType inType)
125         {
126                 if (inString == null || inString.equals("")) {
127                         return false;
128                 }
129                 switch (inType)
130                 {
131                         case NONE: return false;
132                         case LONG:
133                                 // Try to parse into a long
134                                 try
135                                 {
136                                         long rawValue = Long.parseLong(inString.trim());
137                                         _milliseconds = getMilliseconds(rawValue);
138                                         return true;
139                                 }
140                                 catch (NumberFormatException nfe)
141                                 {}
142                                 break;
143
144                         case ISO8601_FRACTIONAL:
145                                 final Matcher fmatcher = ISO8601_FRACTIONAL_PATTERN.matcher(inString);
146                                 if (fmatcher.matches())
147                                 {
148                                         try {
149                                                 _milliseconds = getMilliseconds(Integer.parseInt(fmatcher.group(1)), // year
150                                                         Integer.parseInt(fmatcher.group(2)), // month
151                                                         Integer.parseInt(fmatcher.group(3)), // day
152                                                         Integer.parseInt(fmatcher.group(4)), // hour
153                                                         Integer.parseInt(fmatcher.group(5)), // minute
154                                                         Integer.parseInt(fmatcher.group(6)), // second
155                                                         fmatcher.group(7),                   // fractional seconds
156                                                         fmatcher.group(8));                  // timezone, if any
157                                                 return true;
158                                         }
159                                         catch (NumberFormatException nfe) {}
160                                 }
161                                 break;
162
163                         case FIXED_FORMAT0: return parseString(inString, ALL_DATE_FORMATS[0]);
164                         case FIXED_FORMAT1: return parseString(inString, ALL_DATE_FORMATS[1]);
165                         case FIXED_FORMAT2: return parseString(inString, ALL_DATE_FORMATS[2]);
166                         case FIXED_FORMAT3: return parseString(inString, ALL_DATE_FORMATS[3]);
167                         case FIXED_FORMAT4: return parseString(inString, ALL_DATE_FORMATS[4]);
168                         case FIXED_FORMAT5: return parseString(inString, ALL_DATE_FORMATS[5]);
169                         case FIXED_FORMAT6: return parseString(inString, ALL_DATE_FORMATS[6]);
170
171                         case GENERAL_STRING:
172                                 if (inString.length() == 19)
173                                 {
174                                         final Matcher matcher = GENERAL_TIMESTAMP_PATTERN.matcher(inString);
175                                         if (matcher.matches())
176                                         {
177                                                 try {
178                                                         _milliseconds = getMilliseconds(Integer.parseInt(matcher.group(1)),
179                                                                 Integer.parseInt(matcher.group(2)),
180                                                                 Integer.parseInt(matcher.group(3)),
181                                                                 Integer.parseInt(matcher.group(4)),
182                                                                 Integer.parseInt(matcher.group(5)),
183                                                                 Integer.parseInt(matcher.group(6)),
184                                                                 null, null); // no fractions of a second and no timezone
185                                                         return true;
186                                                 }
187                                                 catch (NumberFormatException nfe2) {} // parse shouldn't fail if matcher matched
188                                         }
189                                 }
190                                 return false;
191                 }
192                 return false;
193         }
194
195
196         /**
197          * Try to parse the given string with the given date format
198          * @param inString String to parse
199          * @param inDateFormat Date format to use
200          * @return true if successful
201          */
202         private boolean parseString(String inString, DateFormat inDateFormat)
203         {
204                 inDateFormat.setLenient(false);
205                 ParsePosition pPos = new ParsePosition(0);
206                 Date date = inDateFormat.parse(inString, pPos);
207                 if (date != null && inString.length() == pPos.getIndex()) // require use of _all_ the string, not just the beginning
208                 {
209                         CALENDAR.setTime(date);
210                         _milliseconds = CALENDAR.getTimeInMillis();
211                         return true;
212                 }
213
214                 return false;
215         }
216
217
218         /**
219          * Constructor giving each field value individually
220          * @param inYear year
221          * @param inMonth month, beginning with 1
222          * @param inDay day of month, beginning with 1
223          * @param inHour hour of day, 0-24
224          * @param inMinute minute
225          * @param inSecond seconds
226          */
227         public Timestamp(int inYear, int inMonth, int inDay, int inHour, int inMinute, int inSecond)
228         {
229                 _milliseconds = getMilliseconds(inYear, inMonth, inDay, inHour, inMinute, inSecond, null, null);
230                 _valid = true;
231         }
232
233
234         /**
235          * Constructor giving millis
236          * @param inMillis milliseconds since 1970
237          */
238         public Timestamp(long inMillis)
239         {
240                 _milliseconds = inMillis;
241                 _valid = true;
242         }
243
244
245         /**
246          * Convert the given timestamp parameters into a number of milliseconds
247          * @param inYear year
248          * @param inMonth month, beginning with 1
249          * @param inDay day of month, beginning with 1
250          * @param inHour hour of day, 0-24
251          * @param inMinute minute
252          * @param inSecond seconds
253          * @param inFraction fractions of a second
254          * @param inTimezone timezone, if any
255          * @return number of milliseconds
256          */
257         private static long getMilliseconds(int inYear, int inMonth, int inDay,
258                 int inHour, int inMinute, int inSecond, String inFraction, String inTimezone)
259         {
260                 Calendar cal = Calendar.getInstance();
261                 // Timezone, if any
262                 if (inTimezone == null || inTimezone.equals("") || inTimezone.equals("Z")) {
263                         // No timezone, use zulu
264                         cal.setTimeZone(TimeZone.getTimeZone("GMT"));
265                 }
266                 else {
267                         // Timezone specified, pass to calendar
268                         cal.setTimeZone(TimeZone.getTimeZone("GMT" + inTimezone));
269                 }
270                 cal.set(Calendar.YEAR, inYear);
271                 cal.set(Calendar.MONTH, inMonth - 1);
272                 cal.set(Calendar.DAY_OF_MONTH, inDay);
273                 cal.set(Calendar.HOUR_OF_DAY, inHour);
274                 cal.set(Calendar.MINUTE, inMinute);
275                 cal.set(Calendar.SECOND, inSecond);
276                 int millis = 0;
277                 if (inFraction != null)
278                 {
279                         try {
280                                 int frac = Integer.parseInt(inFraction);
281                                 final int fracLen = inFraction.length();
282                                 switch (fracLen) {
283                                         case 1: millis = frac * 100; break;
284                                         case 2: millis = frac * 10;  break;
285                                         case 3: millis = frac;       break;
286                                 }
287                         }
288                         catch (NumberFormatException nfe) {} // ignore errors, millis stay at 0
289                 }
290                 cal.set(Calendar.MILLISECOND, millis);
291                 return cal.getTimeInMillis();
292         }
293
294         /**
295          * Convert the given long parameters into a number of millisseconds
296          * @param inRawValue long value representing seconds / milliseconds
297          * @return number of milliseconds
298          */
299         private static long getMilliseconds(long inRawValue)
300         {
301                 // check for each format possibility and pick nearest
302                 long diff1 = Math.abs(SECS_SINCE_1970 - inRawValue);
303                 long diff2 = Math.abs(MSECS_SINCE_1970 - inRawValue);
304                 long diff3 = Math.abs(MSECS_SINCE_1990 - inRawValue);
305                 long diff4 = Math.abs(SECS_SINCE_GARTRIP - inRawValue);
306
307                 // Start off with "seconds since 1970" format
308                 long smallestDiff = diff1;
309                 long millis = inRawValue * 1000;
310                 // Now check millis since 1970
311                 if (diff2 < smallestDiff)
312                 {
313                         // milliseconds since 1970
314                         millis = inRawValue;
315                         smallestDiff = diff2;
316                 }
317                 // Now millis since 1990
318                 if (diff3 < smallestDiff)
319                 {
320                         // milliseconds since 1990
321                         millis = inRawValue + TWENTY_YEARS_IN_SECS * 1000L;
322                         smallestDiff = diff3;
323                 }
324                 // Lastly, check gartrip offset
325                 if (diff4 < smallestDiff)
326                 {
327                         // seconds since gartrip offset
328                         millis = (inRawValue + GARTRIP_OFFSET) * 1000L;
329                 }
330                 return millis;
331         }
332
333         /**
334          * @return true if timestamp is valid
335          */
336         public boolean isValid()
337         {
338                 return _valid;
339         }
340
341         /**
342          * @param inOther other Timestamp
343          * @return true if this one is at least a second after the other
344          */
345         public boolean isAfter(Timestamp inOther)
346         {
347                 return getSecondsSince(inOther) > 0L;
348         }
349
350         /**
351          * Calculate the difference between two Timestamps in seconds
352          * @param inOther other, earlier Timestamp
353          * @return number of seconds since other timestamp
354          */
355         public long getSecondsSince(Timestamp inOther)
356         {
357                 return (_milliseconds - inOther._milliseconds) / 1000L;
358         }
359
360         /**
361          * Calculate the difference between two Timestamps in milliseconds
362          * @param inOther other, earlier Timestamp
363          * @return number of millisseconds since other timestamp
364          */
365         public long getMillisecondsSince(Timestamp inOther)
366         {
367                 return _milliseconds - inOther._milliseconds;
368         }
369
370         /**
371          * @param inOther other timestamp to compare
372          * @return true if they're equal to the nearest second
373          */
374         public boolean isEqual(Timestamp inOther)
375         {
376                 return getSecondsSince(inOther) == 0L;
377         }
378
379         /**
380          * @param inOther other Timestamp
381          * @return true if this one is before the other
382          */
383         public boolean isBefore(Timestamp inOther)
384         {
385                 return getSecondsSince(inOther) < 0L;
386         }
387
388         /**
389          * Add the given number of seconds offset
390          * @param inOffset number of seconds to add/subtract
391          */
392         public void addOffset(long inOffset)
393         {
394                 _milliseconds += (inOffset * 1000L);
395                 _text = null;
396         }
397
398         /**
399          * Add the given TimeDifference to this Timestamp
400          * @param inOffset TimeDifference to add
401          * @return new Timestamp object
402          */
403         public Timestamp createPlusOffset(TimeDifference inOffset)
404         {
405                 return createPlusOffset(inOffset.getTotalSeconds());
406         }
407
408         /**
409          * Add the given number of seconds to this Timestamp
410          * @param inSeconds number of seconds to add
411          * @return new Timestamp object
412          */
413         public Timestamp createPlusOffset(long inSeconds)
414         {
415                 return new Timestamp(_milliseconds + (inSeconds * 1000L));
416         }
417
418
419         /**
420          * Subtract the given TimeDifference from this Timestamp
421          * @param inOffset TimeDifference to subtract
422          * @return new Timestamp object
423          */
424         public Timestamp createMinusOffset(TimeDifference inOffset)
425         {
426                 return new Timestamp(_milliseconds - (inOffset.getTotalSeconds() * 1000L));
427         }
428
429
430         /**
431          * @return Description of timestamp in locale-specific format
432          */
433         public String getText()
434         {
435                 return getText(FORMAT_LOCALE);
436         }
437
438         /**
439          * @param inFormat format of timestamp
440          * @return Description of timestamp in required format
441          */
442         public String getText(int inFormat)
443         {
444                 if (!_valid) {return "";}
445                 if (inFormat == FORMAT_ISO_8601) {
446                         return format(ISO_8601_FORMAT);
447                 }
448                 if (_text == null) {
449                         _text = format(DEFAULT_DATE_FORMAT);
450                 }
451                 return _text;
452         }
453
454         /**
455          * @return Description of time part of timestamp in locale-specific format
456          */
457         public String getTimeText()
458         {
459                 if (_timeText == null)
460                 {
461                         if (_valid) {
462                                 _timeText = format(DEFAULT_TIME_FORMAT);
463                         }
464                         else _timeText = "";
465                 }
466                 return _timeText;
467         }
468
469         /**
470          * Utility method for formatting dates / times
471          * @param inFormat formatter object
472          * @return formatted String
473          */
474         private String format(DateFormat inFormat)
475         {
476                 CALENDAR.setTimeZone(TimeZone.getTimeZone("GMT"));
477                 CALENDAR.setTimeInMillis(_milliseconds);
478                 return inFormat.format(CALENDAR.getTime());
479         }
480
481         /**
482          * @return a Calendar object representing this timestamp
483          */
484         public Calendar getCalendar()
485         {
486                 Calendar cal = Calendar.getInstance();
487                 cal.setTimeZone(TimeZone.getTimeZone("GMT"));
488                 cal.setTimeInMillis(_milliseconds);
489                 return cal;
490         }
491 }