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