Commit | Line | Data |
---|---|---|
f8177ba2 | 1 | /******************************************************************************* |
cd772d23 | 2 | * Copyright (c) 2012, 2015 Ericsson |
f8177ba2 FC |
3 | * |
4 | * All rights reserved. This program and the accompanying materials are | |
5 | * made available under the terms of the Eclipse Public License v1.0 which | |
6 | * accompanies this distribution, and is available at | |
7 | * http://www.eclipse.org/legal/epl-v10.html | |
8 | * | |
9 | * Contributors: | |
c1cd9635 MAL |
10 | * Francois Chouinard - Initial API and implementation |
11 | * Marc-Andre Laperle - Add time zone preference | |
081a0804 | 12 | * Patrick Tasse - Updated for negative value formatting and fraction of sec |
f8177ba2 FC |
13 | *******************************************************************************/ |
14 | ||
2bdf0193 | 15 | package org.eclipse.tracecompass.tmf.core.timestamp; |
f8177ba2 FC |
16 | |
17 | import java.text.DecimalFormat; | |
18 | import java.text.ParseException; | |
19 | import java.text.SimpleDateFormat; | |
20 | import java.util.ArrayList; | |
21 | import java.util.Calendar; | |
22 | import java.util.Date; | |
23 | import java.util.List; | |
8765b2d4 | 24 | import java.util.Locale; |
c1cd9635 | 25 | import java.util.TimeZone; |
f8177ba2 | 26 | |
f8177ba2 FC |
27 | /** |
28 | * A formatting and parsing facility that can handle timestamps that span the | |
081a0804 PT |
29 | * epoch with a precision down to the nanosecond. It can be understood as an |
30 | * extension of SimpleDateFormat that supports seconds since the epoch (Jan 1, | |
31 | * 1970, 00:00:00 GMT), additional sub-second patterns and optional delimiters. | |
f8177ba2 FC |
32 | * <p> |
33 | * The timestamp representation is broken down into a number of optional | |
34 | * components that can be assembled into a fairly simple way. | |
35 | * | |
081a0804 PT |
36 | * <h4>Date and Time Patterns</h4> |
37 | * All date and time pattern letters defined in {@link SimpleDateFormat} are | |
38 | * supported with the following exceptions: | |
f8177ba2 FC |
39 | * <blockquote> |
40 | * <table border=0 cellspacing=3 cellpadding=0 > | |
41 | * <tr bgcolor="#ccccff"> | |
42 | * <th align=left>Format | |
43 | * <th align=left>Description | |
44 | * <th align=left>Value Range | |
45 | * <th align=left>Example | |
f8177ba2 FC |
46 | * <tr bgcolor="#eeeeff"> |
47 | * <td><code>T</code> | |
48 | * <td>The seconds since the epoch | |
081a0804 | 49 | * <td><code>0-9223372036</code> |
f8177ba2 | 50 | * <td><code>1332170682</code> |
081a0804 PT |
51 | * <tr> |
52 | * <td><code>S</code> | |
53 | * <td>Millisecond | |
54 | * <td><code>N/A</code> | |
55 | * <td><code>Not supported</code> | |
56 | * <tr bgcolor="#eeeeff"> | |
57 | * <td><code>W</code> | |
58 | * <td>Week in month | |
59 | * <td><code>N/A</code> | |
60 | * <td><code>Not supported</code> | |
f8177ba2 FC |
61 | * </table> |
62 | * </blockquote> | |
081a0804 PT |
63 | * <p> |
64 | * <strong>Note:</strong> When parsing, if "T" is used, no other Date and Time | |
65 | * pattern letter will be interpreted and the entire pre-delimiter input string | |
66 | * will be parsed as a number. Also, "T" should be used for time intervals. | |
67 | * <p> | |
68 | * <strong>Note:</strong> The decimal separator between the Date and Time | |
69 | * pattern and the Sub-Seconds pattern is mandatory (if there is a fractional | |
70 | * part) and must be one of the sub-second delimiters. Date and Time pattern | |
71 | * letters are not interpreted after the decimal separator. | |
72 | * <p> | |
73 | * <h4>Sub-Seconds Patterns</h4> | |
f8177ba2 FC |
74 | * <blockquote> |
75 | * <table border=0 cellspacing=3 cellpadding=0 > | |
76 | * <tr bgcolor="#ccccff"> | |
77 | * <th align=left>Format | |
78 | * <th align=left>Description | |
79 | * <th align=left>Value Range | |
80 | * <th align=left>Example | |
81 | * <tr> | |
081a0804 PT |
82 | * <td><code>S</code> |
83 | * <td>Fraction of second | |
84 | * <td><code>0-999999999</code> | |
85 | * <td><code>123456789</code> | |
f8177ba2 | 86 | * <tr bgcolor="#eeeeff"> |
081a0804 | 87 | * <td><code>C</code> |
f8177ba2 | 88 | * <td>Microseconds in ms |
081a0804 | 89 | * <td><code>0-999</code> |
f8177ba2 FC |
90 | * <td><code>456</code> |
91 | * <tr> | |
081a0804 | 92 | * <td><code>N</code> |
18ec12d7 | 93 | * <td>Nanoseconds in µs |
081a0804 | 94 | * <td><code>0-999</code> |
f8177ba2 FC |
95 | * <td><code>789</code> |
96 | * </table> | |
97 | * </blockquote> | |
081a0804 PT |
98 | * <strong>Note:</strong> The fraction of second pattern can be split, in which |
99 | * case parsing and formatting continues at the next digit. Digits beyond the | |
100 | * total number of pattern letters are ignored when parsing and truncated when | |
101 | * formatting. | |
f8177ba2 | 102 | * <p> |
081a0804 PT |
103 | * <strong>Note:</strong> When parsing, "S", "C" and "N" are interchangeable |
104 | * and are all handled as fraction of second ("S"). The use of "C" and "N" is | |
105 | * discouraged but is supported for backward compatibility. | |
f8177ba2 | 106 | * <p> |
081a0804 PT |
107 | * |
108 | * The recognized sub-second delimiters are: | |
f8177ba2 FC |
109 | * <ul> |
110 | * <li>Space ("<code> </code>") | |
081a0804 | 111 | * <li>Period ("<code>.</code>") |
f8177ba2 FC |
112 | * <li>Comma ("<code>,</code>") |
113 | * <li>Dash ("<code>-</code>") | |
114 | * <li>Underline ("<code>_</code>") | |
115 | * <li>Colon ("<code>:</code>") | |
116 | * <li>Semicolon ("<code>;</code>") | |
117 | * <li>Slash ("<code>/</code>") | |
081a0804 | 118 | * <li>Single-quote ("<code>''</code>") |
f8177ba2 FC |
119 | * <li>Double-quote ("<code>"</code>") |
120 | * </ul> | |
081a0804 PT |
121 | * <p> |
122 | * <strong>Note:</strong> When parsing, sub-second delimiters are optional if | |
123 | * unquoted. However, an extra delimiter or any other unexpected character in | |
124 | * the input string ends the parsing of digits. All other quoted or unquoted | |
125 | * characters in the sub-second pattern are matched against the input string. | |
f8177ba2 FC |
126 | * |
127 | * <h4>Examples</h4> | |
128 | * The following examples show how timestamp patterns are interpreted in | |
129 | * the U.S. locale. The given timestamp is 1332170682539677389L, the number | |
130 | * of nanoseconds since 1970/01/01. | |
131 | * | |
132 | * <blockquote> | |
133 | * <table border=0 cellspacing=3 cellpadding=0> | |
134 | * <tr bgcolor="#ccccff"> | |
135 | * <th align=left>Date and Time Pattern | |
136 | * <th align=left>Result | |
137 | * <tr> | |
081a0804 | 138 | * <td><code>"yyyy-MM-dd HH:mm:ss.SSS.SSS.SSS"</code> |
f8177ba2 FC |
139 | * <td><code>2012-03-19 11:24:42.539.677.389</code> |
140 | * <tr bgcolor="#eeeeff"> | |
081a0804 | 141 | * <td><code>"yyyy-MM-dd HH:mm:ss.SSS.SSS"</code> |
f8177ba2 FC |
142 | * <td><code>2012-03-19 11:24:42.539.677</code> |
143 | * <tr> | |
081a0804 | 144 | * <td><code>"yyyy-D HH:mm:ss.SSS.SSS"</code> |
f8177ba2 FC |
145 | * <td><code>2012-79 11:24:42.539.677</code> |
146 | * <tr bgcolor="#eeeeff"> | |
081a0804 PT |
147 | * <td><code>"ss,SSSS"</code> |
148 | * <td><code>42,5397</code> | |
f8177ba2 | 149 | * <tr> |
081a0804 | 150 | * <td><code>"T.SSS SSS SSS"</code> |
f8177ba2 FC |
151 | * <td><code>1332170682.539 677 389</code> |
152 | * <tr bgcolor="#eeeeff"> | |
153 | * <td><code>"T"</code> | |
154 | * <td><code>1332170682</code> | |
155 | * </table> | |
156 | * </blockquote> | |
157 | * <p> | |
f8177ba2 FC |
158 | * @author Francois Chouinard |
159 | */ | |
160 | public class TmfTimestampFormat extends SimpleDateFormat { | |
161 | ||
162 | // ------------------------------------------------------------------------ | |
163 | // Constants | |
164 | // ------------------------------------------------------------------------ | |
165 | ||
166 | /** | |
167 | * This class' serialization ID | |
168 | */ | |
169 | private static final long serialVersionUID = 2835829763122454020L; | |
170 | ||
171 | /** | |
172 | * The default timestamp pattern | |
173 | */ | |
081a0804 | 174 | public static final String DEFAULT_TIME_PATTERN = "HH:mm:ss.SSS SSS SSS"; //$NON-NLS-1$ |
f8177ba2 FC |
175 | |
176 | /** | |
18ec12d7 | 177 | * The default interval pattern |
f8177ba2 | 178 | */ |
081a0804 | 179 | public static final String DEFAULT_INTERVAL_PATTERN = "TTT.SSS SSS SSS"; //$NON-NLS-1$ |
f8177ba2 FC |
180 | |
181 | // ------------------------------------------------------------------------ | |
182 | // Attributes | |
183 | // ------------------------------------------------------------------------ | |
184 | ||
185 | // The default timestamp pattern | |
f8177ba2 FC |
186 | private static TmfTimestampFormat fDefaultTimeFormat = null; |
187 | ||
188 | // The default time interval format | |
f8177ba2 FC |
189 | private static TmfTimestampFormat fDefaultIntervalFormat = null; |
190 | ||
191 | // The timestamp pattern | |
192 | private String fPattern; | |
193 | ||
081a0804 PT |
194 | // The index of the decimal separator in the pattern |
195 | private int fPatternDecimalSeparatorIndex; | |
196 | ||
197 | // The decimal separator | |
198 | private char fDecimalSeparator = '\0'; | |
199 | ||
200 | // The date and time pattern unquoted characters | |
201 | private String fDateTimePattern; | |
202 | ||
18ec12d7 PT |
203 | // The sub-seconds pattern |
204 | private String fSubSecPattern; | |
205 | ||
206 | // The list of supplementary patterns | |
a4524c1b | 207 | private List<String> fSupplPatterns = new ArrayList<>(); |
f8177ba2 | 208 | |
8765b2d4 PT |
209 | // The locale |
210 | private final Locale fLocale; | |
211 | ||
f8177ba2 FC |
212 | /** |
213 | * The supplementary pattern letters. Can be redefined by sub-classes | |
214 | * to either override existing letters or augment the letter set. | |
215 | * If so, the format() method must provide the (re-)implementation of the | |
216 | * pattern. | |
217 | */ | |
218 | protected String fSupplPatternLetters = "TSCN"; //$NON-NLS-1$ | |
081a0804 PT |
219 | /** |
220 | * The sub-second pattern letters. | |
081a0804 PT |
221 | */ |
222 | protected String fSubSecPatternChars = "SCN"; //$NON-NLS-1$ | |
223 | /** | |
224 | * The optional sub-second delimiter characters. | |
081a0804 PT |
225 | */ |
226 | protected String fDelimiterChars = " .,-_:;/'\""; //$NON-NLS-1$ | |
f8177ba2 | 227 | |
6f4e8ec0 | 228 | /* |
f8177ba2 FC |
229 | * The bracketing symbols used to mitigate the risk of a format string |
230 | * that contains escaped sequences that would conflict with our format | |
231 | * extension. | |
232 | */ | |
6f4e8ec0 | 233 | /** The open bracket symbol */ |
f8177ba2 | 234 | protected String fOpenBracket = "[&"; //$NON-NLS-1$ |
6f4e8ec0 AM |
235 | |
236 | /** The closing bracket symbol */ | |
f8177ba2 FC |
237 | protected String fCloseBracket = "&]"; //$NON-NLS-1$ |
238 | ||
239 | // ------------------------------------------------------------------------ | |
240 | // Constructors | |
241 | // ------------------------------------------------------------------------ | |
242 | ||
243 | /** | |
244 | * The default constructor (uses the default pattern) | |
245 | */ | |
246 | public TmfTimestampFormat() { | |
7c34a4ea | 247 | this(TmfTimePreferences.getTimePattern()); |
f8177ba2 FC |
248 | } |
249 | ||
250 | /** | |
251 | * The normal constructor | |
252 | * | |
253 | * @param pattern the format pattern | |
254 | */ | |
255 | public TmfTimestampFormat(String pattern) { | |
8765b2d4 | 256 | fLocale = Locale.getDefault(); |
f8177ba2 FC |
257 | applyPattern(pattern); |
258 | } | |
259 | ||
c1cd9635 MAL |
260 | /** |
261 | * The full constructor | |
262 | * | |
263 | * @param pattern the format pattern | |
264 | * @param timeZone the time zone | |
c1cd9635 MAL |
265 | */ |
266 | public TmfTimestampFormat(String pattern, TimeZone timeZone) { | |
8765b2d4 PT |
267 | fLocale = Locale.getDefault(); |
268 | setTimeZone(timeZone); | |
269 | applyPattern(pattern); | |
270 | } | |
271 | ||
272 | /** | |
273 | * The fuller constructor | |
274 | * | |
275 | * @param pattern the format pattern | |
276 | * @param timeZone the time zone | |
277 | * @param locale the locale | |
8765b2d4 PT |
278 | */ |
279 | public TmfTimestampFormat(String pattern, TimeZone timeZone, Locale locale) { | |
280 | super("", locale); //$NON-NLS-1$ | |
281 | fLocale = locale; | |
c1cd9635 | 282 | setTimeZone(timeZone); |
8765b2d4 | 283 | setCalendar(Calendar.getInstance(timeZone, locale)); |
c1cd9635 MAL |
284 | applyPattern(pattern); |
285 | } | |
286 | ||
f8177ba2 FC |
287 | /** |
288 | * The copy constructor | |
289 | * | |
290 | * @param other the other format pattern | |
291 | */ | |
292 | public TmfTimestampFormat(TmfTimestampFormat other) { | |
8765b2d4 | 293 | this(other.fPattern, other.getTimeZone(), other.fLocale); |
f8177ba2 FC |
294 | } |
295 | ||
296 | // ------------------------------------------------------------------------ | |
297 | // Getters/setters | |
298 | // ------------------------------------------------------------------------ | |
299 | ||
213148ca MK |
300 | /** |
301 | * Update the default time and default interval formats | |
302 | */ | |
c08a62ab | 303 | public static synchronized void updateDefaultFormats() { |
8765b2d4 | 304 | fDefaultTimeFormat = new TmfTimestampFormat( |
7c34a4ea AM |
305 | TmfTimePreferences.getTimePattern(), |
306 | TmfTimePreferences.getTimeZone(), | |
307 | TmfTimePreferences.getLocale()); | |
308 | fDefaultIntervalFormat = new TmfTimestampFormat(TmfTimePreferences.getIntervalPattern()); | |
c1cd9635 MAL |
309 | } |
310 | ||
f8177ba2 FC |
311 | /** |
312 | * @return the default time format pattern | |
313 | */ | |
c08a62ab | 314 | public static synchronized TmfTimestampFormat getDefaulTimeFormat() { |
f8177ba2 | 315 | if (fDefaultTimeFormat == null) { |
8765b2d4 | 316 | fDefaultTimeFormat = new TmfTimestampFormat( |
7c34a4ea AM |
317 | TmfTimePreferences.getTimePattern(), |
318 | TmfTimePreferences.getTimeZone(), | |
319 | TmfTimePreferences.getLocale()); | |
f8177ba2 FC |
320 | } |
321 | return fDefaultTimeFormat; | |
322 | } | |
323 | ||
f8177ba2 FC |
324 | /** |
325 | * @return the default interval format pattern | |
326 | */ | |
c08a62ab | 327 | public static synchronized TmfTimestampFormat getDefaulIntervalFormat() { |
f8177ba2 | 328 | if (fDefaultIntervalFormat == null) { |
7c34a4ea | 329 | fDefaultIntervalFormat = new TmfTimestampFormat(TmfTimePreferences.getIntervalPattern()); |
f8177ba2 FC |
330 | } |
331 | return fDefaultIntervalFormat; | |
332 | } | |
333 | ||
f8177ba2 FC |
334 | @Override |
335 | public void applyPattern(String pattern) { | |
336 | fPattern = pattern; | |
081a0804 PT |
337 | fPatternDecimalSeparatorIndex = indexOfPatternDecimalSeparator(pattern); |
338 | fDateTimePattern = unquotePattern(pattern.substring(0, fPatternDecimalSeparatorIndex)); | |
339 | // Check that 'S' is not present in the date and time pattern | |
340 | if (fDateTimePattern.indexOf('S') != -1) { | |
341 | throw new IllegalArgumentException("Illegal pattern character 'S'"); //$NON-NLS-1$ | |
342 | } | |
343 | // Check that 'W' is not present in the date and time pattern | |
344 | if (fDateTimePattern.indexOf('W') != -1) { | |
345 | throw new IllegalArgumentException("Illegal pattern character 'W'"); //$NON-NLS-1$ | |
18ec12d7 PT |
346 | } |
347 | // The super pattern is the date/time pattern, quoted and bracketed | |
081a0804 | 348 | super.applyPattern(quoteSpecificTags(pattern.substring(0, fPatternDecimalSeparatorIndex), true)); |
18ec12d7 | 349 | // The sub-seconds pattern is bracketed (but not quoted) |
081a0804 | 350 | fSubSecPattern = quoteSpecificTags(pattern.substring(fPatternDecimalSeparatorIndex), false); |
f8177ba2 FC |
351 | } |
352 | ||
f8177ba2 FC |
353 | @Override |
354 | public String toPattern() { | |
355 | return fPattern; | |
356 | } | |
357 | ||
358 | // ------------------------------------------------------------------------ | |
359 | // Operations | |
360 | // ------------------------------------------------------------------------ | |
361 | ||
362 | /** | |
363 | * Format the timestamp according to its pattern. | |
364 | * | |
365 | * @param value the timestamp value to format (in ns) | |
366 | * @return the formatted timestamp | |
367 | */ | |
1e10705b | 368 | public synchronized String format(long value) { |
f8177ba2 FC |
369 | |
370 | // Split the timestamp value into its sub-components | |
0fb3c830 PT |
371 | long date = value / 1000000; // milliseconds since January 1, 1970, 00:00:00 GMT |
372 | long sec = value / 1000000000; // seconds since January 1, 1970, 00:00:00 GMT | |
18ec12d7 PT |
373 | long ms = Math.abs((value % 1000000000) / 1000000); // milliseconds |
374 | long cs = Math.abs((value % 1000000) / 1000); // microseconds | |
375 | long ns = Math.abs(value % 1000); // nanoseconds | |
0fb3c830 PT |
376 | |
377 | // Adjust for negative value when formatted as a date | |
18ec12d7 | 378 | if (value < 0 && ms + cs + ns > 0 && !super.toPattern().contains(fOpenBracket + "T")) { //$NON-NLS-1$ |
0fb3c830 PT |
379 | date -= 1; |
380 | long nanosec = 1000000000 - (1000000 * ms + 1000 * cs + ns); | |
381 | ms = nanosec / 1000000; | |
18ec12d7 | 382 | cs = (nanosec % 1000000) / 1000; |
0fb3c830 PT |
383 | ns = nanosec % 1000; |
384 | } | |
f8177ba2 | 385 | |
18ec12d7 | 386 | // Let the base class format the date/time pattern |
0fb3c830 | 387 | StringBuffer result = new StringBuffer(super.format(date)); |
18ec12d7 PT |
388 | // Append the sub-second pattern |
389 | result.append(fSubSecPattern); | |
f8177ba2 | 390 | |
081a0804 | 391 | int fractionDigitsPrinted = 0; |
f8177ba2 FC |
392 | // Fill in our extensions |
393 | for (String pattern : fSupplPatterns) { | |
394 | int length = pattern.length(); | |
081a0804 PT |
395 | long val = 0; |
396 | int bufLength = 0; | |
f8177ba2 FC |
397 | |
398 | // Format the proper value as per the pattern | |
399 | switch (pattern.charAt(0)) { | |
400 | case 'T': | |
0fb3c830 PT |
401 | if (value < 0 && sec == 0) { |
402 | result.insert(0, '-'); | |
403 | } | |
081a0804 PT |
404 | val = sec; |
405 | bufLength = Math.min(length, 10); | |
f8177ba2 FC |
406 | break; |
407 | case 'S': | |
081a0804 PT |
408 | val = 1000000 * ms + 1000 * cs + ns; |
409 | bufLength = 9; | |
f8177ba2 FC |
410 | break; |
411 | case 'C': | |
081a0804 PT |
412 | val = cs; |
413 | bufLength = Math.min(length, 3); | |
f8177ba2 FC |
414 | break; |
415 | case 'N': | |
081a0804 PT |
416 | val = ns; |
417 | bufLength = Math.min(length, 3); | |
f8177ba2 FC |
418 | break; |
419 | default: | |
420 | break; | |
421 | } | |
422 | ||
081a0804 PT |
423 | // Prepare the format buffer |
424 | StringBuffer fmt = new StringBuffer(); | |
425 | for (int i = 0; i < bufLength; i++) { | |
426 | fmt.append("0"); //$NON-NLS-1$ | |
427 | } | |
428 | DecimalFormat dfmt = new DecimalFormat(fmt.toString()); | |
429 | String fmtVal = dfmt.format(val); | |
430 | if (pattern.charAt(0) == 'S') { | |
431 | fmtVal = fmtVal.substring(fractionDigitsPrinted, Math.min(bufLength, fractionDigitsPrinted + length)); | |
432 | fractionDigitsPrinted += fmtVal.length(); | |
433 | } | |
434 | ||
18ec12d7 | 435 | // Substitute the placeholder pattern with the formatted value |
f8177ba2 | 436 | String ph = new StringBuffer(fOpenBracket + pattern + fCloseBracket).toString(); |
18ec12d7 | 437 | int loc = result.indexOf(ph); |
f8177ba2 FC |
438 | result.replace(loc, loc + length + fOpenBracket.length() + fCloseBracket.length(), fmtVal); |
439 | } | |
440 | ||
441 | return result.toString(); | |
442 | } | |
443 | ||
444 | /** | |
445 | * Parse a string according to the format pattern | |
446 | * | |
081a0804 | 447 | * @param source the source string |
18ec12d7 PT |
448 | * @param ref the reference (base) time (in ns) |
449 | * @return the parsed value (in ns) | |
f8177ba2 FC |
450 | * @throws ParseException if the string has an invalid format |
451 | */ | |
081a0804 | 452 | public synchronized long parseValue(final String source, final long ref) throws ParseException { |
f8177ba2 FC |
453 | |
454 | // Trivial case | |
081a0804 | 455 | if (source == null || source.length() == 0) { |
f8177ba2 FC |
456 | return 0; |
457 | } | |
458 | ||
081a0804 PT |
459 | long seconds = 0; |
460 | boolean isNegative = source.charAt(0) == '-'; | |
461 | boolean isDateTimeFormat = true; | |
f8177ba2 | 462 | |
081a0804 | 463 | int index = indexOfSourceDecimalSeparator(source); |
f8177ba2 | 464 | |
081a0804 | 465 | // Check for seconds in epoch pattern |
f8177ba2 | 466 | for (String pattern : fSupplPatterns) { |
081a0804 PT |
467 | if (pattern.charAt(0) == 'T') { |
468 | isDateTimeFormat = false; | |
469 | // Remove everything up to the first "." and compute the | |
470 | // number of seconds since the epoch. If there is no period, | |
471 | // assume an integer value and return immediately | |
472 | if (index == 0 || (isNegative && index <= 1)) { | |
473 | seconds = 0; | |
474 | } else if (index == source.length()) { | |
475 | return new DecimalFormat("0").parse(source).longValue() * 1000000000; //$NON-NLS-1$ | |
476 | } else { | |
477 | seconds = new DecimalFormat("0").parse(source.substring(0, index)).longValue(); //$NON-NLS-1$ | |
478 | } | |
479 | break; | |
f8177ba2 FC |
480 | } |
481 | } | |
482 | ||
483 | // If there was no "T" (thus not an interval), parse as a date | |
081a0804 PT |
484 | if (isDateTimeFormat && super.toPattern().length() > 0) { |
485 | Date baseDate = super.parse(source.substring(0, index)); | |
486 | getCalendar(); | |
487 | ||
488 | if (ref != Long.MIN_VALUE) { | |
8765b2d4 | 489 | Calendar baseTime = Calendar.getInstance(getTimeZone(), fLocale); |
081a0804 | 490 | baseTime.setTimeInMillis(baseDate.getTime()); |
8765b2d4 | 491 | Calendar newTime = Calendar.getInstance(getTimeZone(), fLocale); |
081a0804 PT |
492 | newTime.setTimeInMillis(ref / 1000000); |
493 | boolean setRemainingFields = false; | |
494 | if (dateTimePatternContains("yY")) { //$NON-NLS-1$ | |
495 | newTime.set(Calendar.YEAR, baseTime.get(Calendar.YEAR)); | |
496 | setRemainingFields = true; | |
f8177ba2 | 497 | } |
081a0804 PT |
498 | if (setRemainingFields || dateTimePatternContains("M")) { //$NON-NLS-1$ |
499 | newTime.set(Calendar.MONTH, baseTime.get(Calendar.MONTH)); | |
500 | setRemainingFields = true; | |
501 | } | |
502 | if (setRemainingFields || dateTimePatternContains("d")) { //$NON-NLS-1$ | |
503 | newTime.set(Calendar.DATE, baseTime.get(Calendar.DATE)); | |
504 | setRemainingFields = true; | |
505 | } else if (dateTimePatternContains("D")) { //$NON-NLS-1$ | |
506 | newTime.set(Calendar.DAY_OF_YEAR, baseTime.get(Calendar.DAY_OF_YEAR)); | |
507 | setRemainingFields = true; | |
508 | } else if (dateTimePatternContains("w")) { //$NON-NLS-1$ | |
509 | newTime.set(Calendar.WEEK_OF_YEAR, baseTime.get(Calendar.WEEK_OF_YEAR)); | |
510 | setRemainingFields = true; | |
511 | } | |
512 | if (dateTimePatternContains("F")) { //$NON-NLS-1$ | |
513 | newTime.set(Calendar.DAY_OF_WEEK_IN_MONTH, baseTime.get(Calendar.DAY_OF_WEEK_IN_MONTH)); | |
514 | setRemainingFields = true; | |
515 | } | |
516 | if (dateTimePatternContains("Eu")) { //$NON-NLS-1$ | |
517 | newTime.set(Calendar.DAY_OF_WEEK, baseTime.get(Calendar.DAY_OF_WEEK)); | |
518 | setRemainingFields = true; | |
f8177ba2 | 519 | } |
081a0804 PT |
520 | if (setRemainingFields || dateTimePatternContains("aHkKh")) { //$NON-NLS-1$ |
521 | newTime.set(Calendar.HOUR_OF_DAY, baseTime.get(Calendar.HOUR_OF_DAY)); | |
522 | setRemainingFields = true; | |
523 | } | |
524 | if (setRemainingFields || dateTimePatternContains("m")) { //$NON-NLS-1$ | |
525 | newTime.set(Calendar.MINUTE, baseTime.get(Calendar.MINUTE)); | |
526 | setRemainingFields = true; | |
527 | } | |
528 | if (setRemainingFields || dateTimePatternContains("s")) { //$NON-NLS-1$ | |
529 | newTime.set(Calendar.SECOND, baseTime.get(Calendar.SECOND)); | |
530 | } | |
8765b2d4 | 531 | newTime.set(Calendar.MILLISECOND, 0); |
081a0804 PT |
532 | seconds = newTime.getTimeInMillis() / 1000; |
533 | } else { | |
534 | seconds = baseDate.getTime() / 1000; | |
f8177ba2 | 535 | } |
081a0804 PT |
536 | } else if (isDateTimeFormat && ref != Long.MIN_VALUE) { |
537 | // If the date and time pattern is empty, adjust for reference | |
538 | seconds = ref / 1000000000; | |
f8177ba2 FC |
539 | } |
540 | ||
081a0804 PT |
541 | long nanos = parseSubSeconds(source.substring(index)); |
542 | if (isNegative && !isDateTimeFormat) { | |
543 | nanos = -nanos; | |
544 | } | |
f8177ba2 | 545 | // Compute the value in ns |
081a0804 | 546 | return seconds * 1000000000 + nanos; |
f8177ba2 FC |
547 | } |
548 | ||
549 | /** | |
550 | * Parse a string according to the format pattern | |
551 | * | |
081a0804 | 552 | * @param source the source string |
18ec12d7 | 553 | * @return the parsed value (in ns) |
f8177ba2 FC |
554 | * @throws ParseException if the string has an invalid format |
555 | */ | |
081a0804 PT |
556 | public long parseValue(final String source) throws ParseException { |
557 | long result = parseValue(source, Long.MIN_VALUE); | |
f8177ba2 FC |
558 | return result; |
559 | ||
560 | } | |
561 | ||
562 | // ------------------------------------------------------------------------ | |
563 | // Helper functions | |
564 | // ------------------------------------------------------------------------ | |
565 | ||
081a0804 PT |
566 | /** |
567 | * Finds the index of the decimal separator in the pattern string, which is | |
568 | * the last delimiter found before the first sub-second pattern character. | |
569 | * Returns the pattern string length if decimal separator is not found. | |
570 | */ | |
571 | private int indexOfPatternDecimalSeparator(String pattern) { | |
572 | int lastDelimiterIndex = pattern.length(); | |
573 | boolean inQuote = false; | |
574 | int index = 0; | |
575 | while (index < pattern.length()) { | |
576 | char ch = pattern.charAt(index); | |
577 | if (ch == '\'') { | |
578 | if (index + 1 < pattern.length()) { | |
579 | index++; | |
580 | ch = pattern.charAt(index); | |
581 | if (ch != '\'') { | |
582 | inQuote = !inQuote; | |
583 | } | |
584 | } | |
585 | } | |
586 | if (!inQuote) { | |
587 | if (fSubSecPatternChars.indexOf(ch) != -1) { | |
588 | if (lastDelimiterIndex < pattern.length()) { | |
589 | fDecimalSeparator = pattern.charAt(lastDelimiterIndex); | |
590 | } | |
591 | return lastDelimiterIndex; | |
592 | } | |
593 | if (fDelimiterChars.indexOf(ch) != -1) { | |
594 | lastDelimiterIndex = index; | |
595 | if (ch == '\'') { | |
596 | lastDelimiterIndex--; | |
597 | } | |
598 | } | |
599 | } | |
600 | index++; | |
601 | } | |
602 | return pattern.length(); | |
603 | } | |
604 | ||
605 | /** | |
606 | * Finds the first index of a decimal separator in the source string. | |
607 | * Skips the number of decimal separators in the format pattern. | |
608 | * Returns the source string length if decimal separator is not found. | |
609 | */ | |
610 | private int indexOfSourceDecimalSeparator(String source) { | |
081a0804 | 611 | String separator = fDecimalSeparator == '\'' ? "''" : String.valueOf(fDecimalSeparator); //$NON-NLS-1$ |
cd772d23 PT |
612 | int patternPos = fPattern.indexOf(separator); |
613 | int sourcePos = -1; | |
614 | while (patternPos != -1 && patternPos <= fPatternDecimalSeparatorIndex) { | |
081a0804 | 615 | sourcePos = source.indexOf(fDecimalSeparator, sourcePos + 1); |
cd772d23 PT |
616 | if (sourcePos == -1) { |
617 | break; | |
618 | } | |
619 | // skip optional spaces and tabs before a pattern letter | |
620 | char p = patternPos < fPattern.length() - 1 ? fPattern.charAt(patternPos + 1) : '\0'; | |
621 | if ((p >= 'a' && p <= 'z') || (p >= 'A' && p <= 'Z')) { | |
622 | while (sourcePos < source.length() - 1) { | |
623 | char s = source.charAt(sourcePos + 1); | |
624 | if (s == ' ' || s == '\t') { | |
625 | sourcePos++; | |
626 | } else { | |
627 | break; | |
628 | } | |
629 | } | |
630 | } | |
631 | patternPos = fPattern.indexOf(separator, patternPos + separator.length()); | |
081a0804 PT |
632 | } |
633 | if (sourcePos == -1) { | |
634 | sourcePos = source.length(); | |
635 | } | |
636 | return sourcePos; | |
637 | } | |
638 | ||
639 | /** | |
640 | * Parse the sub-second digits in the input. Handle delimiters as optional | |
641 | * characters. Match any non-pattern and non-delimiter pattern characters | |
642 | * against the input. Returns the number of nanoseconds. | |
643 | */ | |
644 | private long parseSubSeconds(String input) throws ParseException { | |
645 | StringBuilder digits = new StringBuilder("000000000"); //$NON-NLS-1$ | |
646 | String pattern = fPattern.substring(fPatternDecimalSeparatorIndex); | |
647 | boolean inQuote = false; | |
648 | int digitIndex = 0; | |
649 | int inputIndex = 0; | |
650 | int patternIndex = 0; | |
651 | while (patternIndex < pattern.length()) { | |
652 | char ch = pattern.charAt(patternIndex); | |
653 | if (ch == '\'') { | |
654 | patternIndex++; | |
655 | if (patternIndex < pattern.length()) { | |
656 | ch = pattern.charAt(patternIndex); | |
657 | if (ch != '\'') { | |
658 | inQuote = !inQuote; | |
659 | } | |
660 | } else if (inQuote) { | |
661 | // final end quote | |
662 | break; | |
663 | } | |
664 | } | |
665 | if (fDelimiterChars.indexOf(ch) != -1 && !inQuote) { | |
666 | // delimiter is optional if not in quote | |
667 | if (inputIndex < input.length() && input.charAt(inputIndex) == ch) { | |
668 | inputIndex++; | |
669 | } | |
670 | patternIndex++; | |
671 | continue; | |
672 | } else if (fSubSecPatternChars.indexOf(ch) != -1 && !inQuote) { | |
673 | // read digit if not in quote | |
674 | if (inputIndex < input.length() && Character.isDigit(input.charAt(inputIndex))) { | |
675 | if (digitIndex < digits.length()) { | |
676 | digits.setCharAt(digitIndex, input.charAt(inputIndex)); | |
677 | digitIndex++; | |
678 | } | |
679 | inputIndex++; | |
680 | } else { | |
681 | // not a digit, stop parsing digits | |
682 | digitIndex = digits.length(); | |
683 | } | |
684 | patternIndex++; | |
685 | continue; | |
686 | } | |
687 | if (inputIndex >= input.length() || input.charAt(inputIndex) != ch) { | |
688 | throw new ParseException("Unparseable sub-seconds: \"" + input + '\"', inputIndex); //$NON-NLS-1$ | |
689 | } | |
690 | patternIndex++; | |
691 | inputIndex++; | |
692 | } | |
693 | return Long.parseLong(digits.toString()); | |
694 | } | |
695 | ||
f8177ba2 FC |
696 | /** |
697 | * Copy the pattern but quote (bracket with "[&" and "&]") the | |
18ec12d7 PT |
698 | * TmfTimestampFormat specific tags. Optionally surround tags with single |
699 | * quotes so these fields are treated as comments by the base class. | |
f8177ba2 FC |
700 | * |
701 | * It also keeps track of the corresponding quoted fields so they can be | |
702 | * properly populated later on (by format()). | |
703 | * | |
081a0804 PT |
704 | * @param pattern |
705 | * the 'extended' pattern | |
706 | * @param includeQuotes | |
707 | * true to include quotes from pattern and add single quotes | |
708 | * around tags | |
f8177ba2 FC |
709 | * @return the quoted and bracketed pattern |
710 | */ | |
081a0804 | 711 | private String quoteSpecificTags(final String pattern, boolean includeQuotes) { |
f8177ba2 FC |
712 | |
713 | StringBuffer result = new StringBuffer(); | |
714 | ||
715 | int length = pattern.length(); | |
716 | boolean inQuote = false; | |
717 | ||
718 | for (int i = 0; i < length; i++) { | |
719 | char c = pattern.charAt(i); | |
081a0804 PT |
720 | if (c != '\'' || includeQuotes) { |
721 | result.append(c); | |
722 | } | |
f8177ba2 FC |
723 | if (c == '\'') { |
724 | // '' is treated as a single quote regardless of being | |
725 | // in a quoted section. | |
726 | if ((i + 1) < length) { | |
727 | c = pattern.charAt(i + 1); | |
728 | if (c == '\'') { | |
729 | i++; | |
730 | result.append(c); | |
731 | continue; | |
732 | } | |
733 | } | |
734 | inQuote = !inQuote; | |
735 | continue; | |
736 | } | |
081a0804 PT |
737 | if (!inQuote && (fSupplPatternLetters.indexOf(c) != -1)) { |
738 | if (pattern.charAt(0) == fDecimalSeparator) { | |
739 | if (fSubSecPatternChars.indexOf(c) == -1) { | |
740 | // do not quote non-sub-second pattern letters in sub-second pattern | |
741 | continue; | |
18ec12d7 | 742 | } |
081a0804 PT |
743 | } else { |
744 | if (fSubSecPatternChars.indexOf(c) != -1) { | |
745 | // do not quote sub-second pattern letters in date and time pattern | |
746 | continue; | |
f8177ba2 | 747 | } |
081a0804 PT |
748 | } |
749 | StringBuilder pat = new StringBuilder(); | |
750 | pat.append(c); | |
751 | if (includeQuotes) { | |
752 | result.insert(result.length() - 1, "'"); //$NON-NLS-1$ | |
753 | } | |
754 | result.insert(result.length() - 1, fOpenBracket); | |
755 | while ((i + 1) < length && pattern.charAt(i + 1) == c) { | |
756 | result.append(c); | |
757 | pat.append(c); | |
758 | i++; | |
759 | } | |
760 | result.append(fCloseBracket); | |
761 | if (includeQuotes) { | |
762 | result.append("'"); //$NON-NLS-1$ | |
763 | } | |
764 | fSupplPatterns.add(pat.toString()); | |
765 | } | |
766 | } | |
767 | return result.toString(); | |
768 | } | |
769 | ||
770 | /** | |
771 | * Returns the unquoted characters in this pattern. | |
772 | */ | |
773 | private static String unquotePattern(String pattern) { | |
774 | boolean inQuote = false; | |
775 | int index = 0; | |
776 | StringBuilder result = new StringBuilder(); | |
777 | while (index < pattern.length()) { | |
778 | char ch = pattern.charAt(index); | |
779 | if (ch == '\'') { | |
780 | if (index + 1 < pattern.length()) { | |
781 | index++; | |
782 | ch = pattern.charAt(index); | |
783 | if (ch != '\'') { | |
784 | inQuote = !inQuote; | |
18ec12d7 | 785 | } |
f8177ba2 FC |
786 | } |
787 | } | |
081a0804 PT |
788 | if (!inQuote) { |
789 | result.append(ch); | |
790 | } | |
791 | index++; | |
f8177ba2 FC |
792 | } |
793 | return result.toString(); | |
794 | } | |
795 | ||
081a0804 PT |
796 | /** |
797 | * Returns true if the date and time pattern contains any of these chars. | |
798 | */ | |
799 | private boolean dateTimePatternContains(String chars) { | |
800 | int index = 0; | |
801 | while (index < chars.length()) { | |
802 | char ch = chars.charAt(index); | |
803 | if (fDateTimePattern.indexOf(ch) != -1) { | |
804 | return true; | |
805 | } | |
806 | index++; | |
807 | } | |
808 | return false; | |
809 | } | |
f8177ba2 | 810 | } |