1 /*******************************************************************************
2 * Copyright (c) 2012, 2015 Ericsson
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
10 * Francois Chouinard - Initial API and implementation
11 * Marc-Andre Laperle - Add time zone preference
12 * Patrick Tasse - Updated for negative value formatting and fraction of sec
13 *******************************************************************************/
15 package org
.eclipse
.tracecompass
.tmf
.core
.timestamp
;
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
;
24 import java
.util
.Locale
;
25 import java
.util
.TimeZone
;
28 * A formatting and parsing facility that can handle timestamps that span the
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.
33 * The timestamp representation is broken down into a number of optional
34 * components that can be assembled into a fairly simple way.
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:
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
46 * <tr bgcolor="#eeeeff">
48 * <td>The seconds since the epoch
49 * <td><code>0-9223372036</code>
50 * <td><code>1332170682</code>
54 * <td><code>N/A</code>
55 * <td><code>Not supported</code>
56 * <tr bgcolor="#eeeeff">
59 * <td><code>N/A</code>
60 * <td><code>Not supported</code>
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.
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.
73 * <h4>Sub-Seconds Patterns</h4>
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
83 * <td>Fraction of second
84 * <td><code>0-999999999</code>
85 * <td><code>123456789</code>
86 * <tr bgcolor="#eeeeff">
88 * <td>Microseconds in ms
89 * <td><code>0-999</code>
90 * <td><code>456</code>
93 * <td>Nanoseconds in µs
94 * <td><code>0-999</code>
95 * <td><code>789</code>
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
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.
108 * The recognized sub-second delimiters are:
110 * <li>Space ("<code> </code>")
111 * <li>Period ("<code>.</code>")
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>")
118 * <li>Single-quote ("<code>''</code>")
119 * <li>Double-quote ("<code>"</code>")
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.
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.
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
138 * <td><code>"yyyy-MM-dd HH:mm:ss.SSS.SSS.SSS"</code>
139 * <td><code>2012-03-19 11:24:42.539.677.389</code>
140 * <tr bgcolor="#eeeeff">
141 * <td><code>"yyyy-MM-dd HH:mm:ss.SSS.SSS"</code>
142 * <td><code>2012-03-19 11:24:42.539.677</code>
144 * <td><code>"yyyy-D HH:mm:ss.SSS.SSS"</code>
145 * <td><code>2012-79 11:24:42.539.677</code>
146 * <tr bgcolor="#eeeeff">
147 * <td><code>"ss,SSSS"</code>
148 * <td><code>42,5397</code>
150 * <td><code>"T.SSS SSS SSS"</code>
151 * <td><code>1332170682.539 677 389</code>
152 * <tr bgcolor="#eeeeff">
153 * <td><code>"T"</code>
154 * <td><code>1332170682</code>
158 * @author Francois Chouinard
160 public class TmfTimestampFormat
extends SimpleDateFormat
{
162 // ------------------------------------------------------------------------
164 // ------------------------------------------------------------------------
167 * This class' serialization ID
169 private static final long serialVersionUID
= 2835829763122454020L;
172 * The default timestamp pattern
174 public static final String DEFAULT_TIME_PATTERN
= "HH:mm:ss.SSS SSS SSS"; //$NON-NLS-1$
177 * The default interval pattern
179 public static final String DEFAULT_INTERVAL_PATTERN
= "TTT.SSS SSS SSS"; //$NON-NLS-1$
181 // ------------------------------------------------------------------------
183 // ------------------------------------------------------------------------
185 // The default timestamp pattern
186 private static TmfTimestampFormat fDefaultTimeFormat
= null;
188 // The default time interval format
189 private static TmfTimestampFormat fDefaultIntervalFormat
= null;
191 // The timestamp pattern
192 private String fPattern
;
194 // The index of the decimal separator in the pattern
195 private int fPatternDecimalSeparatorIndex
;
197 // The decimal separator
198 private char fDecimalSeparator
= '\0';
200 // The date and time pattern unquoted characters
201 private String fDateTimePattern
;
203 // The sub-seconds pattern
204 private String fSubSecPattern
;
206 // The list of supplementary patterns
207 private List
<String
> fSupplPatterns
= new ArrayList
<>();
210 private final Locale fLocale
;
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
218 protected String fSupplPatternLetters
= "TSCN"; //$NON-NLS-1$
220 * The sub-second pattern letters.
222 protected String fSubSecPatternChars
= "SCN"; //$NON-NLS-1$
224 * The optional sub-second delimiter characters.
226 protected String fDelimiterChars
= " .,-_:;/'\""; //$NON-NLS-1$
229 * The bracketing symbols used to mitigate the risk of a format string
230 * that contains escaped sequences that would conflict with our format
233 /** The open bracket symbol */
234 protected String fOpenBracket
= "[&"; //$NON-NLS-1$
236 /** The closing bracket symbol */
237 protected String fCloseBracket
= "&]"; //$NON-NLS-1$
239 // ------------------------------------------------------------------------
241 // ------------------------------------------------------------------------
244 * The default constructor (uses the default pattern)
246 public TmfTimestampFormat() {
247 this(TmfTimePreferences
.getTimePattern());
251 * The normal constructor
253 * @param pattern the format pattern
255 public TmfTimestampFormat(String pattern
) {
256 fLocale
= Locale
.getDefault();
257 applyPattern(pattern
);
261 * The full constructor
263 * @param pattern the format pattern
264 * @param timeZone the time zone
266 public TmfTimestampFormat(String pattern
, TimeZone timeZone
) {
267 fLocale
= Locale
.getDefault();
268 setTimeZone(timeZone
);
269 applyPattern(pattern
);
273 * The fuller constructor
275 * @param pattern the format pattern
276 * @param timeZone the time zone
277 * @param locale the locale
279 public TmfTimestampFormat(String pattern
, TimeZone timeZone
, Locale locale
) {
280 super("", locale
); //$NON-NLS-1$
282 setTimeZone(timeZone
);
283 setCalendar(Calendar
.getInstance(timeZone
, locale
));
284 applyPattern(pattern
);
288 * The copy constructor
290 * @param other the other format pattern
292 public TmfTimestampFormat(TmfTimestampFormat other
) {
293 this(other
.fPattern
, other
.getTimeZone(), other
.fLocale
);
296 // ------------------------------------------------------------------------
298 // ------------------------------------------------------------------------
301 * Update the default time and default interval formats
303 public static synchronized void updateDefaultFormats() {
304 fDefaultTimeFormat
= new TmfTimestampFormat(
305 TmfTimePreferences
.getTimePattern(),
306 TmfTimePreferences
.getTimeZone(),
307 TmfTimePreferences
.getLocale());
308 fDefaultIntervalFormat
= new TmfTimestampFormat(TmfTimePreferences
.getIntervalPattern());
312 * @return the default time format pattern
314 public static synchronized TmfTimestampFormat
getDefaulTimeFormat() {
315 if (fDefaultTimeFormat
== null) {
316 fDefaultTimeFormat
= new TmfTimestampFormat(
317 TmfTimePreferences
.getTimePattern(),
318 TmfTimePreferences
.getTimeZone(),
319 TmfTimePreferences
.getLocale());
321 return fDefaultTimeFormat
;
325 * @return the default interval format pattern
327 public static synchronized TmfTimestampFormat
getDefaulIntervalFormat() {
328 if (fDefaultIntervalFormat
== null) {
329 fDefaultIntervalFormat
= new TmfTimestampFormat(TmfTimePreferences
.getIntervalPattern());
331 return fDefaultIntervalFormat
;
335 public void applyPattern(String pattern
) {
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$
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$
347 // The super pattern is the date/time pattern, quoted and bracketed
348 super.applyPattern(quoteSpecificTags(pattern
.substring(0, fPatternDecimalSeparatorIndex
), true));
349 // The sub-seconds pattern is bracketed (but not quoted)
350 fSubSecPattern
= quoteSpecificTags(pattern
.substring(fPatternDecimalSeparatorIndex
), false);
354 public String
toPattern() {
358 // ------------------------------------------------------------------------
360 // ------------------------------------------------------------------------
363 * Format the timestamp according to its pattern.
365 * @param value the timestamp value to format (in ns)
366 * @return the formatted timestamp
368 public synchronized String
format(long value
) {
370 // Split the timestamp value into its sub-components
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
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
377 // Adjust for negative value when formatted as a date
378 if (value
< 0 && ms
+ cs
+ ns
> 0 && !super.toPattern().contains(fOpenBracket
+ "T")) { //$NON-NLS-1$
380 long nanosec
= 1000000000 - (1000000 * ms
+ 1000 * cs
+ ns
);
381 ms
= nanosec
/ 1000000;
382 cs
= (nanosec
% 1000000) / 1000;
386 // Let the base class format the date/time pattern
387 StringBuffer result
= new StringBuffer(super.format(date
));
388 // Append the sub-second pattern
389 result
.append(fSubSecPattern
);
391 int fractionDigitsPrinted
= 0;
392 // Fill in our extensions
393 for (String pattern
: fSupplPatterns
) {
394 int length
= pattern
.length();
398 // Format the proper value as per the pattern
399 switch (pattern
.charAt(0)) {
401 if (value
< 0 && sec
== 0) {
402 result
.insert(0, '-');
405 bufLength
= Math
.min(length
, 10);
408 val
= 1000000 * ms
+ 1000 * cs
+ ns
;
413 bufLength
= Math
.min(length
, 3);
417 bufLength
= Math
.min(length
, 3);
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$
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();
435 // Substitute the placeholder pattern with the formatted value
436 String ph
= new StringBuffer(fOpenBracket
+ pattern
+ fCloseBracket
).toString();
437 int loc
= result
.indexOf(ph
);
438 result
.replace(loc
, loc
+ length
+ fOpenBracket
.length() + fCloseBracket
.length(), fmtVal
);
441 return result
.toString();
445 * Parse a string according to the format pattern
447 * @param source the source string
448 * @param ref the reference (base) time (in ns)
449 * @return the parsed value (in ns)
450 * @throws ParseException if the string has an invalid format
452 public synchronized long parseValue(final String source
, final long ref
) throws ParseException
{
455 if (source
== null || source
.length() == 0) {
460 boolean isNegative
= source
.charAt(0) == '-';
461 boolean isDateTimeFormat
= true;
463 int index
= indexOfSourceDecimalSeparator(source
);
465 // Check for seconds in epoch pattern
466 for (String pattern
: fSupplPatterns
) {
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)) {
474 } else if (index
== source
.length()) {
475 return new DecimalFormat("0").parse(source
).longValue() * 1000000000; //$NON-NLS-1$
477 seconds
= new DecimalFormat("0").parse(source
.substring(0, index
)).longValue(); //$NON-NLS-1$
483 // If there was no "T" (thus not an interval), parse as a date
484 if (isDateTimeFormat
&& super.toPattern().length() > 0) {
485 Date baseDate
= super.parse(source
.substring(0, index
));
488 if (ref
!= Long
.MIN_VALUE
) {
489 Calendar baseTime
= Calendar
.getInstance(getTimeZone(), fLocale
);
490 baseTime
.setTimeInMillis(baseDate
.getTime());
491 Calendar newTime
= Calendar
.getInstance(getTimeZone(), fLocale
);
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;
498 if (setRemainingFields
|| dateTimePatternContains("M")) { //$NON-NLS-1$
499 newTime
.set(Calendar
.MONTH
, baseTime
.get(Calendar
.MONTH
));
500 setRemainingFields
= true;
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;
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;
516 if (dateTimePatternContains("Eu")) { //$NON-NLS-1$
517 newTime
.set(Calendar
.DAY_OF_WEEK
, baseTime
.get(Calendar
.DAY_OF_WEEK
));
518 setRemainingFields
= true;
520 if (setRemainingFields
|| dateTimePatternContains("aHkKh")) { //$NON-NLS-1$
521 newTime
.set(Calendar
.HOUR_OF_DAY
, baseTime
.get(Calendar
.HOUR_OF_DAY
));
522 setRemainingFields
= true;
524 if (setRemainingFields
|| dateTimePatternContains("m")) { //$NON-NLS-1$
525 newTime
.set(Calendar
.MINUTE
, baseTime
.get(Calendar
.MINUTE
));
526 setRemainingFields
= true;
528 if (setRemainingFields
|| dateTimePatternContains("s")) { //$NON-NLS-1$
529 newTime
.set(Calendar
.SECOND
, baseTime
.get(Calendar
.SECOND
));
531 newTime
.set(Calendar
.MILLISECOND
, 0);
532 seconds
= newTime
.getTimeInMillis() / 1000;
534 seconds
= baseDate
.getTime() / 1000;
536 } else if (isDateTimeFormat
&& ref
!= Long
.MIN_VALUE
) {
537 // If the date and time pattern is empty, adjust for reference
538 seconds
= ref
/ 1000000000;
541 long nanos
= parseSubSeconds(source
.substring(index
));
542 if (isNegative
&& !isDateTimeFormat
) {
545 // Compute the value in ns
546 return seconds
* 1000000000 + nanos
;
550 * Parse a string according to the format pattern
552 * @param source the source string
553 * @return the parsed value (in ns)
554 * @throws ParseException if the string has an invalid format
556 public long parseValue(final String source
) throws ParseException
{
557 long result
= parseValue(source
, Long
.MIN_VALUE
);
562 // ------------------------------------------------------------------------
564 // ------------------------------------------------------------------------
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.
571 private int indexOfPatternDecimalSeparator(String pattern
) {
572 int lastDelimiterIndex
= pattern
.length();
573 boolean inQuote
= false;
575 while (index
< pattern
.length()) {
576 char ch
= pattern
.charAt(index
);
578 if (index
+ 1 < pattern
.length()) {
580 ch
= pattern
.charAt(index
);
587 if (fSubSecPatternChars
.indexOf(ch
) != -1) {
588 if (lastDelimiterIndex
< pattern
.length()) {
589 fDecimalSeparator
= pattern
.charAt(lastDelimiterIndex
);
591 return lastDelimiterIndex
;
593 if (fDelimiterChars
.indexOf(ch
) != -1) {
594 lastDelimiterIndex
= index
;
596 lastDelimiterIndex
--;
602 return pattern
.length();
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.
610 private int indexOfSourceDecimalSeparator(String source
) {
611 String separator
= fDecimalSeparator
== '\'' ?
"''" : String
.valueOf(fDecimalSeparator
); //$NON-NLS-1$
612 int patternPos
= fPattern
.indexOf(separator
);
614 while (patternPos
!= -1 && patternPos
<= fPatternDecimalSeparatorIndex
) {
615 sourcePos
= source
.indexOf(fDecimalSeparator
, sourcePos
+ 1);
616 if (sourcePos
== -1) {
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') {
631 patternPos
= fPattern
.indexOf(separator
, patternPos
+ separator
.length());
633 if (sourcePos
== -1) {
634 sourcePos
= source
.length();
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.
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;
650 int patternIndex
= 0;
651 while (patternIndex
< pattern
.length()) {
652 char ch
= pattern
.charAt(patternIndex
);
655 if (patternIndex
< pattern
.length()) {
656 ch
= pattern
.charAt(patternIndex
);
660 } else if (inQuote
) {
665 if (fDelimiterChars
.indexOf(ch
) != -1 && !inQuote
) {
666 // delimiter is optional if not in quote
667 if (inputIndex
< input
.length() && input
.charAt(inputIndex
) == ch
) {
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
));
681 // not a digit, stop parsing digits
682 digitIndex
= digits
.length();
687 if (inputIndex
>= input
.length() || input
.charAt(inputIndex
) != ch
) {
688 throw new ParseException("Unparseable sub-seconds: \"" + input
+ '\"', inputIndex
); //$NON-NLS-1$
693 return Long
.parseLong(digits
.toString());
697 * Copy the pattern but quote (bracket with "[&" and "&]") the
698 * TmfTimestampFormat specific tags. Optionally surround tags with single
699 * quotes so these fields are treated as comments by the base class.
701 * It also keeps track of the corresponding quoted fields so they can be
702 * properly populated later on (by format()).
705 * the 'extended' pattern
706 * @param includeQuotes
707 * true to include quotes from pattern and add single quotes
709 * @return the quoted and bracketed pattern
711 private String
quoteSpecificTags(final String pattern
, boolean includeQuotes
) {
713 StringBuffer result
= new StringBuffer();
715 int length
= pattern
.length();
716 boolean inQuote
= false;
718 for (int i
= 0; i
< length
; i
++) {
719 char c
= pattern
.charAt(i
);
720 if (c
!= '\'' || includeQuotes
) {
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);
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
744 if (fSubSecPatternChars
.indexOf(c
) != -1) {
745 // do not quote sub-second pattern letters in date and time pattern
749 StringBuilder pat
= new StringBuilder();
752 result
.insert(result
.length() - 1, "'"); //$NON-NLS-1$
754 result
.insert(result
.length() - 1, fOpenBracket
);
755 while ((i
+ 1) < length
&& pattern
.charAt(i
+ 1) == c
) {
760 result
.append(fCloseBracket
);
762 result
.append("'"); //$NON-NLS-1$
764 fSupplPatterns
.add(pat
.toString());
767 return result
.toString();
771 * Returns the unquoted characters in this pattern.
773 private static String
unquotePattern(String pattern
) {
774 boolean inQuote
= false;
776 StringBuilder result
= new StringBuilder();
777 while (index
< pattern
.length()) {
778 char ch
= pattern
.charAt(index
);
780 if (index
+ 1 < pattern
.length()) {
782 ch
= pattern
.charAt(index
);
793 return result
.toString();
797 * Returns true if the date and time pattern contains any of these chars.
799 private boolean dateTimePatternContains(String chars
) {
801 while (index
< chars
.length()) {
802 char ch
= chars
.charAt(index
);
803 if (fDateTimePattern
.indexOf(ch
) != -1) {