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