001    /*
002     *  Copyright 2001-2005 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.time.format;
017    
018    import java.io.IOException;
019    import java.io.Writer;
020    import java.util.ArrayList;
021    import java.util.HashMap;
022    import java.util.HashSet;
023    import java.util.List;
024    import java.util.Locale;
025    import java.util.Map;
026    import java.util.Set;
027    
028    import org.joda.time.Chronology;
029    import org.joda.time.DateTimeConstants;
030    import org.joda.time.DateTimeField;
031    import org.joda.time.DateTimeFieldType;
032    import org.joda.time.DateTimeZone;
033    import org.joda.time.MutableDateTime;
034    import org.joda.time.ReadablePartial;
035    import org.joda.time.MutableDateTime.Property;
036    import org.joda.time.field.MillisDurationField;
037    import org.joda.time.field.PreciseDateTimeField;
038    
039    /**
040     * Factory that creates complex instances of DateTimeFormatter via method calls.
041     * <p>
042     * Datetime formatting is performed by the {@link DateTimeFormatter} class.
043     * Three classes provide factory methods to create formatters, and this is one.
044     * The others are {@link DateTimeFormat} and {@link ISODateTimeFormat}.
045     * <p>
046     * DateTimeFormatterBuilder is used for constructing formatters which are then
047     * used to print or parse. The formatters are built by appending specific fields
048     * or other formatters to an instance of this builder.
049     * <p>
050     * For example, a formatter that prints month and year, like "January 1970",
051     * can be constructed as follows:
052     * <p>
053     * <pre>
054     * DateTimeFormatter monthAndYear = new DateTimeFormatterBuilder()
055     *     .appendMonthOfYearText()
056     *     .appendLiteral(' ')
057     *     .appendYear(4, 4)
058     *     .toFormatter();
059     * </pre>
060     * <p>
061     * DateTimeFormatterBuilder itself is mutable and not thread-safe, but the
062     * formatters that it builds are thread-safe and immutable.
063     *
064     * @author Brian S O'Neill
065     * @author Stephen Colebourne
066     * @author Fredrik Borgh
067     * @since 1.0
068     * @see DateTimeFormat
069     * @see ISODateTimeFormat
070     */
071    public class DateTimeFormatterBuilder {
072    
073        /** Array of printers and parsers (alternating). */
074        private ArrayList iElementPairs;
075        /** Cache of the last returned formatter. */
076        private Object iFormatter;
077    
078        //-----------------------------------------------------------------------
079        /**
080         * Creates a DateTimeFormatterBuilder.
081         */
082        public DateTimeFormatterBuilder() {
083            super();
084            iElementPairs = new ArrayList();
085        }
086    
087        //-----------------------------------------------------------------------
088        /**
089         * Constructs a DateTimeFormatter using all the appended elements.
090         * <p>
091         * This is the main method used by applications at the end of the build
092         * process to create a usable formatter.
093         * <p>
094         * Subsequent changes to this builder do not affect the returned formatter.
095         * <p>
096         * The returned formatter may not support both printing and parsing.
097         * The methods {@link DateTimeFormatter#isPrinter()} and
098         * {@link DateTimeFormatter#isParser()} will help you determine the state
099         * of the formatter.
100         *
101         * @throws UnsupportedOperationException if neither printing nor parsing is supported
102         */
103        public DateTimeFormatter toFormatter() {
104            Object f = getFormatter();
105            DateTimePrinter printer = null;
106            if (isPrinter(f)) {
107                printer = (DateTimePrinter) f;
108            }
109            DateTimeParser parser = null;
110            if (isParser(f)) {
111                parser = (DateTimeParser) f;
112            }
113            if (printer != null || parser != null) {
114                return new DateTimeFormatter(printer, parser);
115            }
116            throw new UnsupportedOperationException("Both printing and parsing not supported");
117        }
118    
119        /**
120         * Internal method to create a DateTimePrinter instance using all the
121         * appended elements.
122         * <p>
123         * Most applications will not use this method.
124         * If you want a printer in an application, call {@link #toFormatter()}
125         * and just use the printing API.
126         * <p>
127         * Subsequent changes to this builder do not affect the returned printer.
128         *
129         * @throws UnsupportedOperationException if printing is not supported
130         */
131        public DateTimePrinter toPrinter() {
132            Object f = getFormatter();
133            if (isPrinter(f)) {
134                return (DateTimePrinter) f;
135            }
136            throw new UnsupportedOperationException("Printing is not supported");
137        }
138    
139        /**
140         * Internal method to create a DateTimeParser instance using all the
141         * appended elements.
142         * <p>
143         * Most applications will not use this method.
144         * If you want a parser in an application, call {@link #toFormatter()}
145         * and just use the parsing API.
146         * <p>
147         * Subsequent changes to this builder do not affect the returned parser.
148         *
149         * @throws UnsupportedOperationException if parsing is not supported
150         */
151        public DateTimeParser toParser() {
152            Object f = getFormatter();
153            if (isParser(f)) {
154                return (DateTimeParser) f;
155            }
156            throw new UnsupportedOperationException("Parsing is not supported");
157        }
158    
159        //-----------------------------------------------------------------------
160        /**
161         * Returns true if toFormatter can be called without throwing an
162         * UnsupportedOperationException.
163         * 
164         * @return true if a formatter can be built
165         */
166        public boolean canBuildFormatter() {
167            return isFormatter(getFormatter());
168        }
169    
170        /**
171         * Returns true if toPrinter can be called without throwing an
172         * UnsupportedOperationException.
173         * 
174         * @return true if a printer can be built
175         */
176        public boolean canBuildPrinter() {
177            return isPrinter(getFormatter());
178        }
179    
180        /**
181         * Returns true if toParser can be called without throwing an
182         * UnsupportedOperationException.
183         * 
184         * @return true if a parser can be built
185         */
186        public boolean canBuildParser() {
187            return isParser(getFormatter());
188        }
189    
190        //-----------------------------------------------------------------------
191        /**
192         * Clears out all the appended elements, allowing this builder to be
193         * reused.
194         */
195        public void clear() {
196            iFormatter = null;
197            iElementPairs.clear();
198        }
199    
200        //-----------------------------------------------------------------------
201        /**
202         * Appends another formatter.
203         *
204         * @param formatter  the formatter to add
205         * @return this DateTimeFormatterBuilder
206         * @throws IllegalArgumentException if formatter is null or of an invalid type
207         */
208        public DateTimeFormatterBuilder append(DateTimeFormatter formatter) {
209            if (formatter == null) {
210                throw new IllegalArgumentException("No formatter supplied");
211            }
212            return append0(formatter.getPrinter(), formatter.getParser());
213        }
214    
215        /**
216         * Appends just a printer. With no matching parser, a parser cannot be
217         * built from this DateTimeFormatterBuilder.
218         *
219         * @param printer  the printer to add
220         * @return this DateTimeFormatterBuilder
221         * @throws IllegalArgumentException if printer is null or of an invalid type
222         */
223        public DateTimeFormatterBuilder append(DateTimePrinter printer) {
224            checkPrinter(printer);
225            return append0(printer, null);
226        }
227    
228        /**
229         * Appends just a parser. With no matching printer, a printer cannot be
230         * built from this builder.
231         *
232         * @param parser  the parser to add
233         * @return this DateTimeFormatterBuilder
234         * @throws IllegalArgumentException if parser is null or of an invalid type
235         */
236        public DateTimeFormatterBuilder append(DateTimeParser parser) {
237            checkParser(parser);
238            return append0(null, parser);
239        }
240    
241        /**
242         * Appends a printer/parser pair.
243         *
244         * @param printer  the printer to add
245         * @param parser  the parser to add
246         * @return this DateTimeFormatterBuilder
247         * @throws IllegalArgumentException if printer or parser is null or of an invalid type
248         */
249        public DateTimeFormatterBuilder append(DateTimePrinter printer, DateTimeParser parser) {
250            checkPrinter(printer);
251            checkParser(parser);
252            return append0(printer, parser);
253        }
254    
255        /**
256         * Appends a printer and a set of matching parsers. When parsing, the first
257         * parser in the list is selected for parsing. If it fails, the next is
258         * chosen, and so on. If none of these parsers succeeds, then the failed
259         * position of the parser that made the greatest progress is returned.
260         * <p>
261         * Only the printer is optional. In addtion, it is illegal for any but the
262         * last of the parser array elements to be null. If the last element is
263         * null, this represents the empty parser. The presence of an empty parser
264         * indicates that the entire array of parse formats is optional.
265         *
266         * @param printer  the printer to add
267         * @param parsers  the parsers to add
268         * @return this DateTimeFormatterBuilder
269         * @throws IllegalArgumentException if any printer or parser is of an invalid type
270         * @throws IllegalArgumentException if any parser element but the last is null
271         */
272        public DateTimeFormatterBuilder append(DateTimePrinter printer, DateTimeParser[] parsers) {
273            if (printer != null) {
274                checkPrinter(printer);
275            }
276            if (parsers == null) {
277                throw new IllegalArgumentException("No parsers supplied");
278            }
279            int length = parsers.length;
280            if (length == 1) {
281                if (parsers[0] == null) {
282                    throw new IllegalArgumentException("No parser supplied");
283                }
284                return append0(printer, parsers[0]);
285            }
286    
287            DateTimeParser[] copyOfParsers = new DateTimeParser[length];
288            int i;
289            for (i = 0; i < length - 1; i++) {
290                if ((copyOfParsers[i] = parsers[i]) == null) {
291                    throw new IllegalArgumentException("Incomplete parser array");
292                }
293            }
294            copyOfParsers[i] = parsers[i];
295    
296            return append0(printer, new MatchingParser(copyOfParsers));
297        }
298    
299        /**
300         * Appends just a parser element which is optional. With no matching
301         * printer, a printer cannot be built from this DateTimeFormatterBuilder.
302         *
303         * @return this DateTimeFormatterBuilder
304         * @throws IllegalArgumentException if parser is null or of an invalid type
305         */
306        public DateTimeFormatterBuilder appendOptional(DateTimeParser parser) {
307            checkParser(parser);
308            DateTimeParser[] parsers = new DateTimeParser[] {parser, null};
309            return append0(null, new MatchingParser(parsers));
310        }
311    
312        //-----------------------------------------------------------------------
313        /**
314         * Checks if the parser is non null and a provider.
315         * 
316         * @param parser  the parser to check
317         */
318        private void checkParser(DateTimeParser parser) {
319            if (parser == null) {
320                throw new IllegalArgumentException("No parser supplied");
321            }
322        }
323    
324        /**
325         * Checks if the printer is non null and a provider.
326         * 
327         * @param printer  the printer to check
328         */
329        private void checkPrinter(DateTimePrinter printer) {
330            if (printer == null) {
331                throw new IllegalArgumentException("No printer supplied");
332            }
333        }
334    
335        private DateTimeFormatterBuilder append0(Object element) {
336            iFormatter = null;
337            // Add the element as both a printer and parser.
338            iElementPairs.add(element);
339            iElementPairs.add(element);
340            return this;
341        }
342    
343        private DateTimeFormatterBuilder append0(
344                DateTimePrinter printer, DateTimeParser parser) {
345            iFormatter = null;
346            iElementPairs.add(printer);
347            iElementPairs.add(parser);
348            return this;
349        }
350    
351        //-----------------------------------------------------------------------
352        /**
353         * Instructs the printer to emit a specific character, and the parser to
354         * expect it. The parser is case-insensitive.
355         *
356         * @return this DateTimeFormatterBuilder
357         */
358        public DateTimeFormatterBuilder appendLiteral(char c) {
359            return append0(new CharacterLiteral(c));
360        }
361    
362        /**
363         * Instructs the printer to emit specific text, and the parser to expect
364         * it. The parser is case-insensitive.
365         *
366         * @return this DateTimeFormatterBuilder
367         * @throws IllegalArgumentException if text is null
368         */
369        public DateTimeFormatterBuilder appendLiteral(String text) {
370            if (text == null) {
371                throw new IllegalArgumentException("Literal must not be null");
372            }
373            switch (text.length()) {
374                case 0:
375                    return this;
376                case 1:
377                    return append0(new CharacterLiteral(text.charAt(0)));
378                default:
379                    return append0(new StringLiteral(text));
380            }
381        }
382    
383        /**
384         * Instructs the printer to emit a field value as a decimal number, and the
385         * parser to expect an unsigned decimal number.
386         *
387         * @param fieldType type of field to append
388         * @param minDigits minumum number of digits to <i>print</i>
389         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
390         * maximum number of digits to print
391         * @return this DateTimeFormatterBuilder
392         * @throws IllegalArgumentException if field type is null
393         */
394        public DateTimeFormatterBuilder appendDecimal(
395                DateTimeFieldType fieldType, int minDigits, int maxDigits) {
396            if (fieldType == null) {
397                throw new IllegalArgumentException("Field type must not be null");
398            }
399            if (maxDigits < minDigits) {
400                maxDigits = minDigits;
401            }
402            if (minDigits < 0 || maxDigits <= 0) {
403                throw new IllegalArgumentException();
404            }
405            if (minDigits <= 1) {
406                return append0(new UnpaddedNumber(fieldType, maxDigits, false));
407            } else {
408                return append0(new PaddedNumber(fieldType, maxDigits, false, minDigits));
409            }
410        }
411    
412        /**
413         * Instructs the printer to emit a field value as a fixed-width decimal
414         * number (smaller numbers will be left-padded with zeros), and the parser
415         * to expect an unsigned decimal number with the same fixed width.
416         * 
417         * @param fieldType type of field to append
418         * @param numDigits the exact number of digits to parse or print, except if
419         * printed value requires more digits
420         * @return this DateTimeFormatterBuilder
421         * @throws IllegalArgumentException if field type is null or if <code>numDigits <= 0</code>
422         * @since 1.5
423         */
424        public DateTimeFormatterBuilder appendFixedDecimal(
425                DateTimeFieldType fieldType, int numDigits) {
426            if (fieldType == null) {
427                throw new IllegalArgumentException("Field type must not be null");
428            }
429            if (numDigits <= 0) {
430                throw new IllegalArgumentException("Illegal number of digits: " + numDigits);
431            }
432            return append0(new FixedNumber(fieldType, numDigits, false));
433        }
434    
435        /**
436         * Instructs the printer to emit a field value as a decimal number, and the
437         * parser to expect a signed decimal number.
438         *
439         * @param fieldType type of field to append
440         * @param minDigits minumum number of digits to <i>print</i>
441         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
442         * maximum number of digits to print
443         * @return this DateTimeFormatterBuilder
444         * @throws IllegalArgumentException if field type is null
445         */
446        public DateTimeFormatterBuilder appendSignedDecimal(
447                DateTimeFieldType fieldType, int minDigits, int maxDigits) {
448            if (fieldType == null) {
449                throw new IllegalArgumentException("Field type must not be null");
450            }
451            if (maxDigits < minDigits) {
452                maxDigits = minDigits;
453            }
454            if (minDigits < 0 || maxDigits <= 0) {
455                throw new IllegalArgumentException();
456            }
457            if (minDigits <= 1) {
458                return append0(new UnpaddedNumber(fieldType, maxDigits, true));
459            } else {
460                return append0(new PaddedNumber(fieldType, maxDigits, true, minDigits));
461            }
462        }
463    
464        /**
465         * Instructs the printer to emit a field value as a fixed-width decimal
466         * number (smaller numbers will be left-padded with zeros), and the parser
467         * to expect an signed decimal number with the same fixed width.
468         * 
469         * @param fieldType type of field to append
470         * @param numDigits the exact number of digits to parse or print, except if
471         * printed value requires more digits
472         * @return this DateTimeFormatterBuilder
473         * @throws IllegalArgumentException if field type is null or if <code>numDigits <= 0</code>
474         * @since 1.5
475         */
476        public DateTimeFormatterBuilder appendFixedSignedDecimal(
477                DateTimeFieldType fieldType, int numDigits) {
478            if (fieldType == null) {
479                throw new IllegalArgumentException("Field type must not be null");
480            }
481            if (numDigits <= 0) {
482                throw new IllegalArgumentException("Illegal number of digits: " + numDigits);
483            }
484            return append0(new FixedNumber(fieldType, numDigits, true));
485        }
486    
487        /**
488         * Instructs the printer to emit a field value as text, and the
489         * parser to expect text.
490         *
491         * @param fieldType type of field to append
492         * @return this DateTimeFormatterBuilder
493         * @throws IllegalArgumentException if field type is null
494         */
495        public DateTimeFormatterBuilder appendText(DateTimeFieldType fieldType) {
496            if (fieldType == null) {
497                throw new IllegalArgumentException("Field type must not be null");
498            }
499            return append0(new TextField(fieldType, false));
500        }
501    
502        /**
503         * Instructs the printer to emit a field value as short text, and the
504         * parser to expect text.
505         *
506         * @param fieldType type of field to append
507         * @return this DateTimeFormatterBuilder
508         * @throws IllegalArgumentException if field type is null
509         */
510        public DateTimeFormatterBuilder appendShortText(DateTimeFieldType fieldType) {
511            if (fieldType == null) {
512                throw new IllegalArgumentException("Field type must not be null");
513            }
514            return append0(new TextField(fieldType, true));
515        }
516    
517        /**
518         * Instructs the printer to emit a remainder of time as a decimal fraction,
519         * sans decimal point. For example, if the field is specified as
520         * minuteOfHour and the time is 12:30:45, the value printed is 75. A
521         * decimal point is implied, so the fraction is 0.75, or three-quarters of
522         * a minute.
523         *
524         * @param fieldType type of field to append
525         * @param minDigits minumum number of digits to print.
526         * @param maxDigits maximum number of digits to print or parse.
527         * @return this DateTimeFormatterBuilder
528         * @throws IllegalArgumentException if field type is null
529         */
530        public DateTimeFormatterBuilder appendFraction(
531                DateTimeFieldType fieldType, int minDigits, int maxDigits) {
532            if (fieldType == null) {
533                throw new IllegalArgumentException("Field type must not be null");
534            }
535            if (maxDigits < minDigits) {
536                maxDigits = minDigits;
537            }
538            if (minDigits < 0 || maxDigits <= 0) {
539                throw new IllegalArgumentException();
540            }
541            return append0(new Fraction(fieldType, minDigits, maxDigits));
542        }
543    
544        /**
545         * @param minDigits minumum number of digits to print
546         * @param maxDigits maximum number of digits to print or parse
547         * @return this DateTimeFormatterBuilder
548         */
549        public DateTimeFormatterBuilder appendFractionOfSecond(int minDigits, int maxDigits) {
550            return appendFraction(DateTimeFieldType.secondOfDay(), minDigits, maxDigits);
551        }
552    
553        /**
554         * @param minDigits minumum number of digits to print
555         * @param maxDigits maximum number of digits to print or parse
556         * @return this DateTimeFormatterBuilder
557         */
558        public DateTimeFormatterBuilder appendFractionOfMinute(int minDigits, int maxDigits) {
559            return appendFraction(DateTimeFieldType.minuteOfDay(), minDigits, maxDigits);
560        }
561    
562        /**
563         * @param minDigits minumum number of digits to print
564         * @param maxDigits maximum number of digits to print or parse
565         * @return this DateTimeFormatterBuilder
566         */
567        public DateTimeFormatterBuilder appendFractionOfHour(int minDigits, int maxDigits) {
568            return appendFraction(DateTimeFieldType.hourOfDay(), minDigits, maxDigits);
569        }
570    
571        /**
572         * @param minDigits minumum number of digits to print
573         * @param maxDigits maximum number of digits to print or parse
574         * @return this DateTimeFormatterBuilder
575         */
576        public DateTimeFormatterBuilder appendFractionOfDay(int minDigits, int maxDigits) {
577            return appendFraction(DateTimeFieldType.dayOfYear(), minDigits, maxDigits);
578        }
579    
580        /**
581         * Instructs the printer to emit a numeric millisOfSecond field.
582         * <p>
583         * This method will append a field that prints a three digit value.
584         * During parsing the value that is parsed is assumed to be three digits.
585         * If less than three digits are present then they will be counted as the
586         * smallest parts of the millisecond. This is probably not what you want
587         * if you are using the field as a fraction. Instead, a fractional
588         * millisecond should be produced using {@link #appendFractionOfSecond}.
589         *
590         * @param minDigits minumum number of digits to print
591         * @return this DateTimeFormatterBuilder
592         */
593        public DateTimeFormatterBuilder appendMillisOfSecond(int minDigits) {
594            return appendDecimal(DateTimeFieldType.millisOfSecond(), minDigits, 3);
595        }
596    
597        /**
598         * Instructs the printer to emit a numeric millisOfDay field.
599         *
600         * @param minDigits minumum number of digits to print
601         * @return this DateTimeFormatterBuilder
602         */
603        public DateTimeFormatterBuilder appendMillisOfDay(int minDigits) {
604            return appendDecimal(DateTimeFieldType.millisOfDay(), minDigits, 8);
605        }
606    
607        /**
608         * Instructs the printer to emit a numeric secondOfMinute field.
609         *
610         * @param minDigits minumum number of digits to print
611         * @return this DateTimeFormatterBuilder
612         */
613        public DateTimeFormatterBuilder appendSecondOfMinute(int minDigits) {
614            return appendDecimal(DateTimeFieldType.secondOfMinute(), minDigits, 2);
615        }
616    
617        /**
618         * Instructs the printer to emit a numeric secondOfDay field.
619         *
620         * @param minDigits minumum number of digits to print
621         * @return this DateTimeFormatterBuilder
622         */
623        public DateTimeFormatterBuilder appendSecondOfDay(int minDigits) {
624            return appendDecimal(DateTimeFieldType.secondOfDay(), minDigits, 5);
625        }
626    
627        /**
628         * Instructs the printer to emit a numeric minuteOfHour field.
629         *
630         * @param minDigits minumum number of digits to print
631         * @return this DateTimeFormatterBuilder
632         */
633        public DateTimeFormatterBuilder appendMinuteOfHour(int minDigits) {
634            return appendDecimal(DateTimeFieldType.minuteOfHour(), minDigits, 2);
635        }
636    
637        /**
638         * Instructs the printer to emit a numeric minuteOfDay field.
639         *
640         * @param minDigits minumum number of digits to print
641         * @return this DateTimeFormatterBuilder
642         */
643        public DateTimeFormatterBuilder appendMinuteOfDay(int minDigits) {
644            return appendDecimal(DateTimeFieldType.minuteOfDay(), minDigits, 4);
645        }
646    
647        /**
648         * Instructs the printer to emit a numeric hourOfDay field.
649         *
650         * @param minDigits minumum number of digits to print
651         * @return this DateTimeFormatterBuilder
652         */
653        public DateTimeFormatterBuilder appendHourOfDay(int minDigits) {
654            return appendDecimal(DateTimeFieldType.hourOfDay(), minDigits, 2);
655        }
656    
657        /**
658         * Instructs the printer to emit a numeric clockhourOfDay field.
659         *
660         * @param minDigits minumum number of digits to print
661         * @return this DateTimeFormatterBuilder
662         */
663        public DateTimeFormatterBuilder appendClockhourOfDay(int minDigits) {
664            return appendDecimal(DateTimeFieldType.clockhourOfDay(), minDigits, 2);
665        }
666    
667        /**
668         * Instructs the printer to emit a numeric hourOfHalfday field.
669         *
670         * @param minDigits minumum number of digits to print
671         * @return this DateTimeFormatterBuilder
672         */
673        public DateTimeFormatterBuilder appendHourOfHalfday(int minDigits) {
674            return appendDecimal(DateTimeFieldType.hourOfHalfday(), minDigits, 2);
675        }
676    
677        /**
678         * Instructs the printer to emit a numeric clockhourOfHalfday field.
679         *
680         * @param minDigits minumum number of digits to print
681         * @return this DateTimeFormatterBuilder
682         */
683        public DateTimeFormatterBuilder appendClockhourOfHalfday(int minDigits) {
684            return appendDecimal(DateTimeFieldType.clockhourOfHalfday(), minDigits, 2);
685        }
686    
687        /**
688         * Instructs the printer to emit a numeric dayOfWeek field.
689         *
690         * @param minDigits minumum number of digits to print
691         * @return this DateTimeFormatterBuilder
692         */
693        public DateTimeFormatterBuilder appendDayOfWeek(int minDigits) {
694            return appendDecimal(DateTimeFieldType.dayOfWeek(), minDigits, 1);
695        }
696    
697        /**
698         * Instructs the printer to emit a numeric dayOfMonth field.
699         *
700         * @param minDigits minumum number of digits to print
701         * @return this DateTimeFormatterBuilder
702         */
703        public DateTimeFormatterBuilder appendDayOfMonth(int minDigits) {
704            return appendDecimal(DateTimeFieldType.dayOfMonth(), minDigits, 2);
705        }
706    
707        /**
708         * Instructs the printer to emit a numeric dayOfYear field.
709         *
710         * @param minDigits minumum number of digits to print
711         * @return this DateTimeFormatterBuilder
712         */
713        public DateTimeFormatterBuilder appendDayOfYear(int minDigits) {
714            return appendDecimal(DateTimeFieldType.dayOfYear(), minDigits, 3);
715        }
716    
717        /**
718         * Instructs the printer to emit a numeric weekOfWeekyear field.
719         *
720         * @param minDigits minumum number of digits to print
721         * @return this DateTimeFormatterBuilder
722         */
723        public DateTimeFormatterBuilder appendWeekOfWeekyear(int minDigits) {
724            return appendDecimal(DateTimeFieldType.weekOfWeekyear(), minDigits, 2);
725        }
726    
727        /**
728         * Instructs the printer to emit a numeric weekyear field.
729         *
730         * @param minDigits minumum number of digits to <i>print</i>
731         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
732         * maximum number of digits to print
733         * @return this DateTimeFormatterBuilder
734         */
735        public DateTimeFormatterBuilder appendWeekyear(int minDigits, int maxDigits) {
736            return appendSignedDecimal(DateTimeFieldType.weekyear(), minDigits, maxDigits);
737        }
738    
739        /**
740         * Instructs the printer to emit a numeric monthOfYear field.
741         *
742         * @param minDigits minumum number of digits to print
743         * @return this DateTimeFormatterBuilder
744         */
745        public DateTimeFormatterBuilder appendMonthOfYear(int minDigits) {
746            return appendDecimal(DateTimeFieldType.monthOfYear(), minDigits, 2);
747        }
748    
749        /**
750         * Instructs the printer to emit a numeric year field.
751         *
752         * @param minDigits minumum number of digits to <i>print</i>
753         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
754         * maximum number of digits to print
755         * @return this DateTimeFormatterBuilder
756         */
757        public DateTimeFormatterBuilder appendYear(int minDigits, int maxDigits) {
758            return appendSignedDecimal(DateTimeFieldType.year(), minDigits, maxDigits);
759        }
760    
761        /**
762         * Instructs the printer to emit a numeric year field which always prints
763         * and parses two digits. A pivot year is used during parsing to determine
764         * the range of supported years as <code>(pivot - 50) .. (pivot + 49)</code>.
765         *
766         * <pre>
767         * pivot   supported range   00 is   20 is   40 is   60 is   80 is
768         * ---------------------------------------------------------------
769         * 1950      1900..1999      1900    1920    1940    1960    1980
770         * 1975      1925..2024      2000    2020    1940    1960    1980
771         * 2000      1950..2049      2000    2020    2040    1960    1980
772         * 2025      1975..2074      2000    2020    2040    2060    1980
773         * 2050      2000..2099      2000    2020    2040    2060    2080
774         * </pre>
775         *
776         * @param pivot  pivot year to use when parsing
777         * @return this DateTimeFormatterBuilder
778         */
779        public DateTimeFormatterBuilder appendTwoDigitYear(int pivot) {
780            return appendTwoDigitYear(pivot, false);
781        }
782    
783        /**
784         * Instructs the printer to emit a numeric year field which always prints
785         * two digits. A pivot year is used during parsing to determine the range
786         * of supported years as <code>(pivot - 50) .. (pivot + 49)</code>. If
787         * parse is instructed to be lenient and the digit count is not two, it is
788         * treated as an absolute year. With lenient parsing, specifying a positive
789         * or negative sign before the year also makes it absolute.
790         *
791         * @param pivot  pivot year to use when parsing
792         * @param lenientParse  when true, if digit count is not two, it is treated
793         * as an absolute year
794         * @return this DateTimeFormatterBuilder
795         * @since 1.1
796         */
797        public DateTimeFormatterBuilder appendTwoDigitYear(int pivot, boolean lenientParse) {
798            return append0(new TwoDigitYear(DateTimeFieldType.year(), pivot, lenientParse));
799        }
800    
801        /**
802         * Instructs the printer to emit a numeric weekyear field which always prints
803         * and parses two digits. A pivot year is used during parsing to determine
804         * the range of supported years as <code>(pivot - 50) .. (pivot + 49)</code>.
805         *
806         * <pre>
807         * pivot   supported range   00 is   20 is   40 is   60 is   80 is
808         * ---------------------------------------------------------------
809         * 1950      1900..1999      1900    1920    1940    1960    1980
810         * 1975      1925..2024      2000    2020    1940    1960    1980
811         * 2000      1950..2049      2000    2020    2040    1960    1980
812         * 2025      1975..2074      2000    2020    2040    2060    1980
813         * 2050      2000..2099      2000    2020    2040    2060    2080
814         * </pre>
815         *
816         * @param pivot  pivot weekyear to use when parsing
817         * @return this DateTimeFormatterBuilder
818         */
819        public DateTimeFormatterBuilder appendTwoDigitWeekyear(int pivot) {
820            return appendTwoDigitWeekyear(pivot, false);
821        }
822    
823        /**
824         * Instructs the printer to emit a numeric weekyear field which always prints
825         * two digits. A pivot year is used during parsing to determine the range
826         * of supported years as <code>(pivot - 50) .. (pivot + 49)</code>. If
827         * parse is instructed to be lenient and the digit count is not two, it is
828         * treated as an absolute weekyear. With lenient parsing, specifying a positive
829         * or negative sign before the weekyear also makes it absolute.
830         *
831         * @param pivot  pivot weekyear to use when parsing
832         * @param lenientParse  when true, if digit count is not two, it is treated
833         * as an absolute weekyear
834         * @return this DateTimeFormatterBuilder
835         * @since 1.1
836         */
837        public DateTimeFormatterBuilder appendTwoDigitWeekyear(int pivot, boolean lenientParse) {
838            return append0(new TwoDigitYear(DateTimeFieldType.weekyear(), pivot, lenientParse));
839        }
840    
841        /**
842         * Instructs the printer to emit a numeric yearOfEra field.
843         *
844         * @param minDigits minumum number of digits to <i>print</i>
845         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
846         * maximum number of digits to print
847         * @return this DateTimeFormatterBuilder
848         */
849        public DateTimeFormatterBuilder appendYearOfEra(int minDigits, int maxDigits) {
850            return appendDecimal(DateTimeFieldType.yearOfEra(), minDigits, maxDigits);
851        }
852    
853        /**
854         * Instructs the printer to emit a numeric year of century field.
855         *
856         * @param minDigits minumum number of digits to print
857         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
858         * maximum number of digits to print
859         * @return this DateTimeFormatterBuilder
860         */
861        public DateTimeFormatterBuilder appendYearOfCentury(int minDigits, int maxDigits) {
862            return appendDecimal(DateTimeFieldType.yearOfCentury(), minDigits, maxDigits);
863        }
864    
865        /**
866         * Instructs the printer to emit a numeric century of era field.
867         *
868         * @param minDigits minumum number of digits to print
869         * @param maxDigits maximum number of digits to <i>parse</i>, or the estimated
870         * maximum number of digits to print
871         * @return this DateTimeFormatterBuilder
872         */
873        public DateTimeFormatterBuilder appendCenturyOfEra(int minDigits, int maxDigits) {
874            return appendSignedDecimal(DateTimeFieldType.centuryOfEra(), minDigits, maxDigits);
875        }
876    
877        /**
878         * Instructs the printer to emit a locale-specific AM/PM text, and the
879         * parser to expect it. The parser is case-insensitive.
880         *
881         * @return this DateTimeFormatterBuilder
882         */
883        public DateTimeFormatterBuilder appendHalfdayOfDayText() {
884            return appendText(DateTimeFieldType.halfdayOfDay());
885        }
886    
887        /**
888         * Instructs the printer to emit a locale-specific dayOfWeek text. The
889         * parser will accept a long or short dayOfWeek text, case-insensitive.
890         *
891         * @return this DateTimeFormatterBuilder
892         */
893        public DateTimeFormatterBuilder appendDayOfWeekText() {
894            return appendText(DateTimeFieldType.dayOfWeek());
895        }
896    
897        /**
898         * Instructs the printer to emit a short locale-specific dayOfWeek
899         * text. The parser will accept a long or short dayOfWeek text,
900         * case-insensitive.
901         *
902         * @return this DateTimeFormatterBuilder
903         */
904        public DateTimeFormatterBuilder appendDayOfWeekShortText() {
905            return appendShortText(DateTimeFieldType.dayOfWeek());
906        }
907    
908        /**
909         * Instructs the printer to emit a short locale-specific monthOfYear
910         * text. The parser will accept a long or short monthOfYear text,
911         * case-insensitive.
912         *
913         * @return this DateTimeFormatterBuilder
914         */
915        public DateTimeFormatterBuilder appendMonthOfYearText() { 
916            return appendText(DateTimeFieldType.monthOfYear());
917        }
918    
919        /**
920         * Instructs the printer to emit a locale-specific monthOfYear text. The
921         * parser will accept a long or short monthOfYear text, case-insensitive.
922         *
923         * @return this DateTimeFormatterBuilder
924         */
925        public DateTimeFormatterBuilder appendMonthOfYearShortText() {
926            return appendShortText(DateTimeFieldType.monthOfYear());
927        }
928    
929        /**
930         * Instructs the printer to emit a locale-specific era text (BC/AD), and
931         * the parser to expect it. The parser is case-insensitive.
932         *
933         * @return this DateTimeFormatterBuilder
934         */
935        public DateTimeFormatterBuilder appendEraText() {
936            return appendText(DateTimeFieldType.era());
937        }
938    
939        /**
940         * Instructs the printer to emit a locale-specific time zone name. A
941         * parser cannot be created from this builder if a time zone name is
942         * appended.
943         *
944         * @return this DateTimeFormatterBuilder
945         */
946        public DateTimeFormatterBuilder appendTimeZoneName() {
947            return append0(new TimeZoneName(TimeZoneName.LONG_NAME), null);
948        }
949    
950        /**
951         * Instructs the printer to emit a short locale-specific time zone
952         * name. A parser cannot be created from this builder if time zone
953         * name is appended.
954         *
955         * @return this DateTimeFormatterBuilder
956         */
957        public DateTimeFormatterBuilder appendTimeZoneShortName() {
958            return append0(new TimeZoneName(TimeZoneName.SHORT_NAME), null);
959        }
960    
961        /**
962         * Instructs the printer to emit the identifier of the time zone.
963         * This field cannot currently be parsed.
964         *
965         * @return this DateTimeFormatterBuilder
966         */
967        public DateTimeFormatterBuilder appendTimeZoneId() {
968            return append0(new TimeZoneName(TimeZoneName.ID), null);
969        }
970    
971        /**
972         * Instructs the printer to emit text and numbers to display time zone
973         * offset from UTC. A parser will use the parsed time zone offset to adjust
974         * the datetime.
975         *
976         * @param zeroOffsetText Text to use if time zone offset is zero. If
977         * null, offset is always shown.
978         * @param showSeparators If true, prints ':' separator before minute and
979         * second field and prints '.' separator before fraction field.
980         * @param minFields minimum number of fields to print, stopping when no
981         * more precision is required. 1=hours, 2=minutes, 3=seconds, 4=fraction
982         * @param maxFields maximum number of fields to print
983         * @return this DateTimeFormatterBuilder
984         */
985        public DateTimeFormatterBuilder appendTimeZoneOffset(
986                String zeroOffsetText, boolean showSeparators,
987                int minFields, int maxFields) {
988            return append0(new TimeZoneOffset
989                           (zeroOffsetText, showSeparators, minFields, maxFields));
990        }
991    
992        //-----------------------------------------------------------------------
993        /**
994         * Calls upon {@link DateTimeFormat} to parse the pattern and append the
995         * results into this builder.
996         *
997         * @param pattern  pattern specification
998         * @throws IllegalArgumentException if the pattern is invalid
999         * @see DateTimeFormat
1000         */
1001        public DateTimeFormatterBuilder appendPattern(String pattern) {
1002            DateTimeFormat.appendPatternTo(this, pattern);
1003            return this;
1004        }
1005    
1006        //-----------------------------------------------------------------------
1007        private Object getFormatter() {
1008            Object f = iFormatter;
1009    
1010            if (f == null) {
1011                if (iElementPairs.size() == 2) {
1012                    Object printer = iElementPairs.get(0);
1013                    Object parser = iElementPairs.get(1);
1014    
1015                    if (printer != null) {
1016                        if (printer == parser || parser == null) {
1017                            f = printer;
1018                        }
1019                    } else {
1020                        f = parser;
1021                    }
1022                }
1023    
1024                if (f == null) {
1025                    f = new Composite(iElementPairs);
1026                }
1027    
1028                iFormatter = f;
1029            }
1030    
1031            return f;
1032        }
1033    
1034        private boolean isPrinter(Object f) {
1035            if (f instanceof DateTimePrinter) {
1036                if (f instanceof Composite) {
1037                    return ((Composite)f).isPrinter();
1038                }
1039                return true;
1040            }
1041            return false;
1042        }
1043    
1044        private boolean isParser(Object f) {
1045            if (f instanceof DateTimeParser) {
1046                if (f instanceof Composite) {
1047                    return ((Composite)f).isParser();
1048                }
1049                return true;
1050            }
1051            return false;
1052        }
1053    
1054        private boolean isFormatter(Object f) {
1055            return (isPrinter(f) || isParser(f));
1056        }
1057    
1058        static void appendUnknownString(StringBuffer buf, int len) {
1059            for (int i = len; --i >= 0;) {
1060                buf.append('\ufffd');
1061            }
1062        }
1063    
1064        static void printUnknownString(Writer out, int len) throws IOException {
1065            for (int i = len; --i >= 0;) {
1066                out.write('\ufffd');
1067            }
1068        }
1069    
1070        //-----------------------------------------------------------------------
1071        static class CharacterLiteral
1072                implements DateTimePrinter, DateTimeParser {
1073    
1074            private final char iValue;
1075    
1076            CharacterLiteral(char value) {
1077                super();
1078                iValue = value;
1079            }
1080    
1081            public int estimatePrintedLength() {
1082                return 1;
1083            }
1084    
1085            public void printTo(
1086                    StringBuffer buf, long instant, Chronology chrono,
1087                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1088                buf.append(iValue);
1089            }
1090    
1091            public void printTo(
1092                    Writer out, long instant, Chronology chrono,
1093                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1094                out.write(iValue);
1095            }
1096    
1097            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1098                buf.append(iValue);
1099            }
1100    
1101            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1102                out.write(iValue);
1103            }
1104    
1105            public int estimateParsedLength() {
1106                return 1;
1107            }
1108    
1109            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1110                if (position >= text.length()) {
1111                    return ~position;
1112                }
1113    
1114                char a = text.charAt(position);
1115                char b = iValue;
1116    
1117                if (a != b) {
1118                    a = Character.toUpperCase(a);
1119                    b = Character.toUpperCase(b);
1120                    if (a != b) {
1121                        a = Character.toLowerCase(a);
1122                        b = Character.toLowerCase(b);
1123                        if (a != b) {
1124                            return ~position;
1125                        }
1126                    }
1127                }
1128    
1129                return position + 1;
1130            }
1131        }
1132    
1133        //-----------------------------------------------------------------------
1134        static class StringLiteral
1135                implements DateTimePrinter, DateTimeParser {
1136    
1137            private final String iValue;
1138    
1139            StringLiteral(String value) {
1140                super();
1141                iValue = value;
1142            }
1143    
1144            public int estimatePrintedLength() {
1145                return iValue.length();
1146            }
1147    
1148            public void printTo(
1149                    StringBuffer buf, long instant, Chronology chrono,
1150                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1151                buf.append(iValue);
1152            }
1153    
1154            public void printTo(
1155                    Writer out, long instant, Chronology chrono,
1156                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1157                out.write(iValue);
1158            }
1159    
1160            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1161                buf.append(iValue);
1162            }
1163    
1164            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1165                out.write(iValue);
1166            }
1167    
1168            public int estimateParsedLength() {
1169                return iValue.length();
1170            }
1171    
1172            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1173                if (text.regionMatches(true, position, iValue, 0, iValue.length())) {
1174                    return position + iValue.length();
1175                }
1176                return ~position;
1177            }
1178        }
1179    
1180        //-----------------------------------------------------------------------
1181        static abstract class NumberFormatter
1182                implements DateTimePrinter, DateTimeParser {
1183            protected final DateTimeFieldType iFieldType;
1184            protected final int iMaxParsedDigits;
1185            protected final boolean iSigned;
1186    
1187            NumberFormatter(DateTimeFieldType fieldType,
1188                    int maxParsedDigits, boolean signed) {
1189                super();
1190                iFieldType = fieldType;
1191                iMaxParsedDigits = maxParsedDigits;
1192                iSigned = signed;
1193            }
1194    
1195            public int estimateParsedLength() {
1196                return iMaxParsedDigits;
1197            }
1198    
1199            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1200                int limit = Math.min(iMaxParsedDigits, text.length() - position);
1201    
1202                boolean negative = false;
1203                int length = 0;
1204                while (length < limit) {
1205                    char c = text.charAt(position + length);
1206                    if (length == 0 && (c == '-' || c == '+') && iSigned) {
1207                        negative = c == '-';
1208    
1209                        // Next character must be a digit.
1210                        if (length + 1 >= limit || 
1211                            (c = text.charAt(position + length + 1)) < '0' || c > '9')
1212                        {
1213                            break;
1214                        }
1215    
1216                        if (negative) {
1217                            length++;
1218                        } else {
1219                            // Skip the '+' for parseInt to succeed.
1220                            position++;
1221                        }
1222                        // Expand the limit to disregard the sign character.
1223                        limit = Math.min(limit + 1, text.length() - position);
1224                        continue;
1225                    }
1226                    if (c < '0' || c > '9') {
1227                        break;
1228                    }
1229                    length++;
1230                }
1231    
1232                if (length == 0) {
1233                    return ~position;
1234                }
1235    
1236                int value;
1237                if (length >= 9) {
1238                    // Since value may exceed integer limits, use stock parser
1239                    // which checks for this.
1240                    value = Integer.parseInt(text.substring(position, position += length));
1241                } else {
1242                    int i = position;
1243                    if (negative) {
1244                        i++;
1245                    }
1246                    try {
1247                        value = text.charAt(i++) - '0';
1248                    } catch (StringIndexOutOfBoundsException e) {
1249                        return ~position;
1250                    }
1251                    position += length;
1252                    while (i < position) {
1253                        value = ((value << 3) + (value << 1)) + text.charAt(i++) - '0';
1254                    }
1255                    if (negative) {
1256                        value = -value;
1257                    }
1258                }
1259    
1260                bucket.saveField(iFieldType, value);
1261                return position;
1262            }
1263        }
1264    
1265        //-----------------------------------------------------------------------
1266        static class UnpaddedNumber extends NumberFormatter {
1267    
1268            protected UnpaddedNumber(DateTimeFieldType fieldType,
1269                           int maxParsedDigits, boolean signed)
1270            {
1271                super(fieldType, maxParsedDigits, signed);
1272            }
1273    
1274            public int estimatePrintedLength() {
1275                return iMaxParsedDigits;
1276            }
1277    
1278            public void printTo(
1279                    StringBuffer buf, long instant, Chronology chrono,
1280                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1281                try {
1282                    DateTimeField field = iFieldType.getField(chrono);
1283                    FormatUtils.appendUnpaddedInteger(buf, field.get(instant));
1284                } catch (RuntimeException e) {
1285                    buf.append('\ufffd');
1286                }
1287            }
1288    
1289            public void printTo(
1290                    Writer out, long instant, Chronology chrono,
1291                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1292                try {
1293                    DateTimeField field = iFieldType.getField(chrono);
1294                    FormatUtils.writeUnpaddedInteger(out, field.get(instant));
1295                } catch (RuntimeException e) {
1296                    out.write('\ufffd');
1297                }
1298            }
1299    
1300            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1301                if (partial.isSupported(iFieldType)) {
1302                    try {
1303                        FormatUtils.appendUnpaddedInteger(buf, partial.get(iFieldType));
1304                    } catch (RuntimeException e) {
1305                        buf.append('\ufffd');
1306                    }
1307                } else {
1308                    buf.append('\ufffd');
1309                }
1310            }
1311    
1312            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1313                if (partial.isSupported(iFieldType)) {
1314                    try {
1315                        FormatUtils.writeUnpaddedInteger(out, partial.get(iFieldType));
1316                    } catch (RuntimeException e) {
1317                        out.write('\ufffd');
1318                    }
1319                } else {
1320                    out.write('\ufffd');
1321                }
1322            }
1323        }
1324    
1325        //-----------------------------------------------------------------------
1326        static class PaddedNumber extends NumberFormatter {
1327    
1328            protected final int iMinPrintedDigits;
1329    
1330            protected PaddedNumber(DateTimeFieldType fieldType, int maxParsedDigits,
1331                         boolean signed, int minPrintedDigits)
1332            {
1333                super(fieldType, maxParsedDigits, signed);
1334                iMinPrintedDigits = minPrintedDigits;
1335            }
1336    
1337            public int estimatePrintedLength() {
1338                return iMaxParsedDigits;
1339            }
1340    
1341            public void printTo(
1342                    StringBuffer buf, long instant, Chronology chrono,
1343                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1344                try {
1345                    DateTimeField field = iFieldType.getField(chrono);
1346                    FormatUtils.appendPaddedInteger(buf, field.get(instant), iMinPrintedDigits);
1347                } catch (RuntimeException e) {
1348                    appendUnknownString(buf, iMinPrintedDigits);
1349                }
1350            }
1351    
1352            public void printTo(
1353                    Writer out, long instant, Chronology chrono,
1354                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1355                try {
1356                    DateTimeField field = iFieldType.getField(chrono);
1357                    FormatUtils.writePaddedInteger(out, field.get(instant), iMinPrintedDigits);
1358                } catch (RuntimeException e) {
1359                    printUnknownString(out, iMinPrintedDigits);
1360                }
1361            }
1362    
1363            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1364                if (partial.isSupported(iFieldType)) {
1365                    try {
1366                        FormatUtils.appendPaddedInteger(buf, partial.get(iFieldType), iMinPrintedDigits);
1367                    } catch (RuntimeException e) {
1368                        appendUnknownString(buf, iMinPrintedDigits);
1369                    }
1370                } else {
1371                    appendUnknownString(buf, iMinPrintedDigits);
1372                }
1373            }
1374    
1375            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1376                if (partial.isSupported(iFieldType)) {
1377                    try {
1378                        FormatUtils.writePaddedInteger(out, partial.get(iFieldType), iMinPrintedDigits);
1379                    } catch (RuntimeException e) {
1380                        printUnknownString(out, iMinPrintedDigits);
1381                    }
1382                } else {
1383                    printUnknownString(out, iMinPrintedDigits);
1384                }
1385            }
1386        }
1387    
1388        //-----------------------------------------------------------------------
1389        static class FixedNumber extends PaddedNumber {
1390    
1391            protected FixedNumber(DateTimeFieldType fieldType, int numDigits, boolean signed) {
1392                super(fieldType, numDigits, signed, numDigits);
1393            }
1394    
1395            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1396                int newPos = super.parseInto(bucket, text, position);
1397                if (newPos < 0) {
1398                    return newPos;
1399                }
1400                int expectedPos = position + iMaxParsedDigits;
1401                if (newPos != expectedPos) {
1402                    if (iSigned) {
1403                        char c = text.charAt(position);
1404                        if (c == '-' || c == '+') {
1405                            expectedPos++;
1406                        }
1407                    }
1408                    if (newPos > expectedPos) {
1409                        // The failure is at the position of the first extra digit.
1410                        return ~(expectedPos + 1);
1411                    } else if (newPos < expectedPos) {
1412                        // The failure is at the position where the next digit should be.
1413                        return ~newPos;
1414                    }
1415                }
1416                return newPos;
1417            }
1418        }
1419    
1420        //-----------------------------------------------------------------------
1421        static class TwoDigitYear
1422                implements DateTimePrinter, DateTimeParser {
1423    
1424            /** The field to print/parse. */
1425            private final DateTimeFieldType iType;
1426            /** The pivot year. */
1427            private final int iPivot;
1428            private final boolean iLenientParse;
1429    
1430            TwoDigitYear(DateTimeFieldType type, int pivot, boolean lenientParse) {
1431                super();
1432                iType = type;
1433                iPivot = pivot;
1434                iLenientParse = lenientParse;
1435            }
1436    
1437            public int estimateParsedLength() {
1438                return iLenientParse ? 4 : 2;
1439            }
1440    
1441            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1442                int limit = text.length() - position;
1443    
1444                if (!iLenientParse) {
1445                    limit = Math.min(2, limit);
1446                    if (limit < 2) {
1447                        return ~position;
1448                    }
1449                } else {
1450                    boolean hasSignChar = false;
1451                    boolean negative = false;
1452                    int length = 0;
1453                    while (length < limit) {
1454                        char c = text.charAt(position + length);
1455                        if (length == 0 && (c == '-' || c == '+')) {
1456                            hasSignChar = true;
1457                            negative = c == '-';
1458                            if (negative) {
1459                                length++;
1460                            } else {
1461                                // Skip the '+' for parseInt to succeed.
1462                                position++;
1463                                limit--;
1464                            }
1465                            continue;
1466                        }
1467                        if (c < '0' || c > '9') {
1468                            break;
1469                        }
1470                        length++;
1471                    }
1472                    
1473                    if (length == 0) {
1474                        return ~position;
1475                    }
1476    
1477                    if (hasSignChar || length != 2) {
1478                        int value;
1479                        if (length >= 9) {
1480                            // Since value may exceed integer limits, use stock
1481                            // parser which checks for this.
1482                            value = Integer.parseInt(text.substring(position, position += length));
1483                        } else {
1484                            int i = position;
1485                            if (negative) {
1486                                i++;
1487                            }
1488                            try {
1489                                value = text.charAt(i++) - '0';
1490                            } catch (StringIndexOutOfBoundsException e) {
1491                                return ~position;
1492                            }
1493                            position += length;
1494                            while (i < position) {
1495                                value = ((value << 3) + (value << 1)) + text.charAt(i++) - '0';
1496                            }
1497                            if (negative) {
1498                                value = -value;
1499                            }
1500                        }
1501                        
1502                        bucket.saveField(iType, value);
1503                        return position;
1504                    }
1505                }
1506    
1507                int year;
1508                char c = text.charAt(position);
1509                if (c < '0' || c > '9') {
1510                    return ~position;
1511                }
1512                year = c - '0';
1513                c = text.charAt(position + 1);
1514                if (c < '0' || c > '9') {
1515                    return ~position;
1516                }
1517                year = ((year << 3) + (year << 1)) + c - '0';
1518    
1519                int pivot = iPivot;
1520                // If the bucket pivot year is non-null, use that when parsing
1521                if (bucket.getPivotYear() != null) {
1522                    pivot = bucket.getPivotYear().intValue();
1523                }
1524    
1525                int low = pivot - 50;
1526    
1527                int t;
1528                if (low >= 0) {
1529                    t = low % 100;
1530                } else {
1531                    t = 99 + ((low + 1) % 100);
1532                }
1533    
1534                year += low + ((year < t) ? 100 : 0) - t;
1535    
1536                bucket.saveField(iType, year);
1537                return position + 2;
1538            }
1539            
1540            public int estimatePrintedLength() {
1541                return 2;
1542            }
1543    
1544            public void printTo(
1545                    StringBuffer buf, long instant, Chronology chrono,
1546                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1547                int year = getTwoDigitYear(instant, chrono);
1548                if (year < 0) {
1549                    buf.append('\ufffd');
1550                    buf.append('\ufffd');
1551                } else {
1552                    FormatUtils.appendPaddedInteger(buf, year, 2);
1553                }
1554            }
1555    
1556            public void printTo(
1557                    Writer out, long instant, Chronology chrono,
1558                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1559                int year = getTwoDigitYear(instant, chrono);
1560                if (year < 0) {
1561                    out.write('\ufffd');
1562                    out.write('\ufffd');
1563                } else {
1564                    FormatUtils.writePaddedInteger(out, year, 2);
1565                }
1566            }
1567    
1568            private int getTwoDigitYear(long instant, Chronology chrono) {
1569                try {
1570                    int year = iType.getField(chrono).get(instant);
1571                    if (year < 0) {
1572                        year = -year;
1573                    }
1574                    return year % 100;
1575                } catch (RuntimeException e) {
1576                    return -1;
1577                }
1578            }
1579    
1580            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1581                int year = getTwoDigitYear(partial);
1582                if (year < 0) {
1583                    buf.append('\ufffd');
1584                    buf.append('\ufffd');
1585                } else {
1586                    FormatUtils.appendPaddedInteger(buf, year, 2);
1587                }
1588            }
1589    
1590            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1591                int year = getTwoDigitYear(partial);
1592                if (year < 0) {
1593                    out.write('\ufffd');
1594                    out.write('\ufffd');
1595                } else {
1596                    FormatUtils.writePaddedInteger(out, year, 2);
1597                }
1598            }
1599    
1600            private int getTwoDigitYear(ReadablePartial partial) {
1601                if (partial.isSupported(iType)) {
1602                    try {
1603                        int year = partial.get(iType);
1604                        if (year < 0) {
1605                            year = -year;
1606                        }
1607                        return year % 100;
1608                    } catch (RuntimeException e) {}
1609                } 
1610                return -1;
1611            }
1612        }
1613    
1614        //-----------------------------------------------------------------------
1615        static class TextField
1616                implements DateTimePrinter, DateTimeParser {
1617    
1618            private static Map cParseCache = new HashMap();
1619            private final DateTimeFieldType iFieldType;
1620            private final boolean iShort;
1621    
1622            TextField(DateTimeFieldType fieldType, boolean isShort) {
1623                super();
1624                iFieldType = fieldType;
1625                iShort = isShort;
1626            }
1627    
1628            public int estimatePrintedLength() {
1629                return iShort ? 6 : 20;
1630            }
1631    
1632            public void printTo(
1633                    StringBuffer buf, long instant, Chronology chrono,
1634                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1635                try {
1636                    buf.append(print(instant, chrono, locale));
1637                } catch (RuntimeException e) {
1638                    buf.append('\ufffd');
1639                }
1640            }
1641    
1642            public void printTo(
1643                    Writer out, long instant, Chronology chrono,
1644                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1645                try {
1646                    out.write(print(instant, chrono, locale));
1647                } catch (RuntimeException e) {
1648                    out.write('\ufffd');
1649                }
1650            }
1651    
1652            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1653                try {
1654                    buf.append(print(partial, locale));
1655                } catch (RuntimeException e) {
1656                    buf.append('\ufffd');
1657                }
1658            }
1659    
1660            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1661                try {
1662                    out.write(print(partial, locale));
1663                } catch (RuntimeException e) {
1664                    out.write('\ufffd');
1665                }
1666            }
1667    
1668            private String print(long instant, Chronology chrono, Locale locale) {
1669                DateTimeField field = iFieldType.getField(chrono);
1670                if (iShort) {
1671                    return field.getAsShortText(instant, locale);
1672                } else {
1673                    return field.getAsText(instant, locale);
1674                }
1675            }
1676    
1677            private String print(ReadablePartial partial, Locale locale) {
1678                if (partial.isSupported(iFieldType)) {
1679                    DateTimeField field = iFieldType.getField(partial.getChronology());
1680                    if (iShort) {
1681                        return field.getAsShortText(partial, locale);
1682                    } else {
1683                        return field.getAsText(partial, locale);
1684                    }
1685                } else {
1686                    return "\ufffd";
1687                }
1688            }
1689    
1690            public int estimateParsedLength() {
1691                return estimatePrintedLength();
1692            }
1693    
1694            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1695                Locale locale = bucket.getLocale();
1696                // handle languages which might have non ASCII A-Z or punctuation
1697                // bug 1788282
1698                Set validValues = null;
1699                int maxLength = 0;
1700                synchronized (cParseCache) {
1701                    Map innerMap = (Map) cParseCache.get(locale);
1702                    if (innerMap == null) {
1703                        innerMap = new HashMap();
1704                        cParseCache.put(locale, innerMap);
1705                    }
1706                    Object[] array = (Object[]) innerMap.get(iFieldType);
1707                    if (array == null) {
1708                        validValues = new HashSet(32);
1709                        MutableDateTime dt = new MutableDateTime(0L, DateTimeZone.UTC);
1710                        Property property = dt.property(iFieldType);
1711                        int min = property.getMinimumValueOverall();
1712                        int max = property.getMaximumValueOverall();
1713                        if (max - min > 32) {  // protect against invalid fields
1714                            return ~position;
1715                        }
1716                        maxLength = property.getMaximumTextLength(locale);
1717                        for (int i = min; i <= max; i++) {
1718                            property.set(i);
1719                            validValues.add(property.getAsShortText(locale));
1720                            validValues.add(property.getAsShortText(locale).toLowerCase(locale));
1721                            validValues.add(property.getAsShortText(locale).toUpperCase(locale));
1722                            validValues.add(property.getAsText(locale));
1723                            validValues.add(property.getAsText(locale).toLowerCase(locale));
1724                            validValues.add(property.getAsText(locale).toUpperCase(locale));
1725                        }
1726                        if ("en".equals(locale.getLanguage()) && iFieldType == DateTimeFieldType.era()) {
1727                            // hack to support for parsing "BCE" and "CE" if the language is English
1728                            validValues.add("BCE");
1729                            validValues.add("bce");
1730                            validValues.add("CE");
1731                            validValues.add("ce");
1732                            maxLength = 3;
1733                        }
1734                        array = new Object[] {validValues, new Integer(maxLength)};
1735                        innerMap.put(iFieldType, array);
1736                    } else {
1737                        validValues = (Set) array[0];
1738                        maxLength = ((Integer) array[1]).intValue();
1739                    }
1740                }
1741                // match the longest string first using our knowledge of the max length
1742                int limit = Math.min(text.length(), position + maxLength);
1743                for (int i = limit; i > position; i--) {
1744                    String match = text.substring(position, i);
1745                    if (validValues.contains(match)) {
1746                        bucket.saveField(iFieldType, match, locale);
1747                        return i;
1748                    }
1749                }
1750                return ~position;
1751            }
1752        }
1753    
1754        //-----------------------------------------------------------------------
1755        static class Fraction
1756                implements DateTimePrinter, DateTimeParser {
1757    
1758            private final DateTimeFieldType iFieldType;
1759            protected int iMinDigits;
1760            protected int iMaxDigits;
1761    
1762            protected Fraction(DateTimeFieldType fieldType, int minDigits, int maxDigits) {
1763                super();
1764                iFieldType = fieldType;
1765                // Limit the precision requirements.
1766                if (maxDigits > 18) {
1767                    maxDigits = 18;
1768                }
1769                iMinDigits = minDigits;
1770                iMaxDigits = maxDigits;
1771            }
1772    
1773            public int estimatePrintedLength() {
1774                return iMaxDigits;
1775            }
1776    
1777            public void printTo(
1778                    StringBuffer buf, long instant, Chronology chrono,
1779                    int displayOffset, DateTimeZone displayZone, Locale locale) {
1780                try {
1781                    printTo(buf, null, instant, chrono);
1782                } catch (IOException e) {
1783                    // Not gonna happen.
1784                }
1785            }
1786    
1787            public void printTo(
1788                    Writer out, long instant, Chronology chrono,
1789                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
1790                printTo(null, out, instant, chrono);
1791            }
1792    
1793            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
1794                // removed check whether field is supported, as input field is typically
1795                // secondOfDay which is unsupported by TimeOfDay
1796                long millis = partial.getChronology().set(partial, 0L);
1797                try {
1798                    printTo(buf, null, millis, partial.getChronology());
1799                } catch (IOException e) {
1800                    // Not gonna happen.
1801                }
1802            }
1803    
1804            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
1805                // removed check whether field is supported, as input field is typically
1806                // secondOfDay which is unsupported by TimeOfDay
1807                long millis = partial.getChronology().set(partial, 0L);
1808                printTo(null, out, millis, partial.getChronology());
1809            }
1810    
1811            protected void printTo(StringBuffer buf, Writer out, long instant, Chronology chrono)
1812                throws IOException
1813            {
1814                DateTimeField field = iFieldType.getField(chrono);
1815                int minDigits = iMinDigits;
1816    
1817                long fraction;
1818                try {
1819                    fraction = field.remainder(instant);
1820                } catch (RuntimeException e) {
1821                    if (buf != null) {
1822                        appendUnknownString(buf, minDigits);
1823                    } else {
1824                        printUnknownString(out, minDigits);
1825                    }
1826                    return;
1827                }
1828    
1829                if (fraction == 0) {
1830                    if (buf != null) {
1831                        while (--minDigits >= 0) {
1832                            buf.append('0');
1833                        }
1834                    } else {
1835                        while (--minDigits >= 0) {
1836                            out.write('0');
1837                        }
1838                    }
1839                    return;
1840                }
1841    
1842                String str;
1843                long[] fractionData = getFractionData(fraction, field);
1844                long scaled = fractionData[0];
1845                int maxDigits = (int) fractionData[1];
1846                
1847                if ((scaled & 0x7fffffff) == scaled) {
1848                    str = Integer.toString((int) scaled);
1849                } else {
1850                    str = Long.toString(scaled);
1851                }
1852    
1853                int length = str.length();
1854                int digits = maxDigits;
1855                while (length < digits) {
1856                    if (buf != null) {
1857                        buf.append('0');
1858                    } else {
1859                        out.write('0');
1860                    }
1861                    minDigits--;
1862                    digits--;
1863                }
1864    
1865                if (minDigits < digits) {
1866                    // Chop off as many trailing zero digits as necessary.
1867                    while (minDigits < digits) {
1868                        if (length <= 1 || str.charAt(length - 1) != '0') {
1869                            break;
1870                        }
1871                        digits--;
1872                        length--;
1873                    }
1874                    if (length < str.length()) {
1875                        if (buf != null) {
1876                            for (int i=0; i<length; i++) {
1877                                buf.append(str.charAt(i));
1878                            }
1879                        } else {
1880                            for (int i=0; i<length; i++) {
1881                                out.write(str.charAt(i));
1882                            }
1883                        }
1884                        return;
1885                    }
1886                }
1887    
1888                if (buf != null) {
1889                    buf.append(str);
1890                } else {
1891                    out.write(str);
1892                }
1893            }
1894            
1895            private long[] getFractionData(long fraction, DateTimeField field) {
1896                long rangeMillis = field.getDurationField().getUnitMillis();
1897                long scalar;
1898                int maxDigits = iMaxDigits;
1899                while (true) {
1900                    switch (maxDigits) {
1901                    default: scalar = 1L; break;
1902                    case 1:  scalar = 10L; break;
1903                    case 2:  scalar = 100L; break;
1904                    case 3:  scalar = 1000L; break;
1905                    case 4:  scalar = 10000L; break;
1906                    case 5:  scalar = 100000L; break;
1907                    case 6:  scalar = 1000000L; break;
1908                    case 7:  scalar = 10000000L; break;
1909                    case 8:  scalar = 100000000L; break;
1910                    case 9:  scalar = 1000000000L; break;
1911                    case 10: scalar = 10000000000L; break;
1912                    case 11: scalar = 100000000000L; break;
1913                    case 12: scalar = 1000000000000L; break;
1914                    case 13: scalar = 10000000000000L; break;
1915                    case 14: scalar = 100000000000000L; break;
1916                    case 15: scalar = 1000000000000000L; break;
1917                    case 16: scalar = 10000000000000000L; break;
1918                    case 17: scalar = 100000000000000000L; break;
1919                    case 18: scalar = 1000000000000000000L; break;
1920                    }
1921                    if (((rangeMillis * scalar) / scalar) == rangeMillis) {
1922                        break;
1923                    }
1924                    // Overflowed: scale down.
1925                    maxDigits--;
1926                }
1927                
1928                return new long[] {fraction * scalar / rangeMillis, maxDigits};
1929            }
1930    
1931            public int estimateParsedLength() {
1932                return iMaxDigits;
1933            }
1934    
1935            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
1936                DateTimeField field = iFieldType.getField(bucket.getChronology());
1937                
1938                int limit = Math.min(iMaxDigits, text.length() - position);
1939    
1940                long value = 0;
1941                long n = field.getDurationField().getUnitMillis() * 10;
1942                int length = 0;
1943                while (length < limit) {
1944                    char c = text.charAt(position + length);
1945                    if (c < '0' || c > '9') {
1946                        break;
1947                    }
1948                    length++;
1949                    long nn = n / 10;
1950                    value += (c - '0') * nn;
1951                    n = nn;
1952                }
1953    
1954                value /= 10;
1955    
1956                if (length == 0) {
1957                    return ~position;
1958                }
1959    
1960                if (value > Integer.MAX_VALUE) {
1961                    return ~position;
1962                }
1963    
1964                DateTimeField parseField = new PreciseDateTimeField(
1965                    DateTimeFieldType.millisOfSecond(),
1966                    MillisDurationField.INSTANCE,
1967                    field.getDurationField());
1968    
1969                bucket.saveField(parseField, (int) value);
1970    
1971                return position + length;
1972            }
1973        }
1974    
1975        //-----------------------------------------------------------------------
1976        static class TimeZoneOffset
1977                implements DateTimePrinter, DateTimeParser {
1978    
1979            private final String iZeroOffsetText;
1980            private final boolean iShowSeparators;
1981            private final int iMinFields;
1982            private final int iMaxFields;
1983    
1984            TimeZoneOffset(String zeroOffsetText,
1985                                    boolean showSeparators,
1986                                    int minFields, int maxFields)
1987            {
1988                super();
1989                iZeroOffsetText = zeroOffsetText;
1990                iShowSeparators = showSeparators;
1991                if (minFields <= 0 || maxFields < minFields) {
1992                    throw new IllegalArgumentException();
1993                }
1994                if (minFields > 4) {
1995                    minFields = 4;
1996                    maxFields = 4;
1997                }
1998                iMinFields = minFields;
1999                iMaxFields = maxFields;
2000            }
2001                
2002            public int estimatePrintedLength() {
2003                int est = 1 + iMinFields << 1;
2004                if (iShowSeparators) {
2005                    est += iMinFields - 1;
2006                }
2007                if (iZeroOffsetText != null && iZeroOffsetText.length() > est) {
2008                    est = iZeroOffsetText.length();
2009                }
2010                return est;
2011            }
2012            
2013            public void printTo(
2014                    StringBuffer buf, long instant, Chronology chrono,
2015                    int displayOffset, DateTimeZone displayZone, Locale locale) {
2016                if (displayZone == null) {
2017                    return;  // no zone
2018                }
2019                if (displayOffset == 0 && iZeroOffsetText != null) {
2020                    buf.append(iZeroOffsetText);
2021                    return;
2022                }
2023                if (displayOffset >= 0) {
2024                    buf.append('+');
2025                } else {
2026                    buf.append('-');
2027                    displayOffset = -displayOffset;
2028                }
2029    
2030                int hours = displayOffset / DateTimeConstants.MILLIS_PER_HOUR;
2031                FormatUtils.appendPaddedInteger(buf, hours, 2);
2032                if (iMaxFields == 1) {
2033                    return;
2034                }
2035                displayOffset -= hours * (int)DateTimeConstants.MILLIS_PER_HOUR;
2036                if (displayOffset == 0 && iMinFields <= 1) {
2037                    return;
2038                }
2039    
2040                int minutes = displayOffset / DateTimeConstants.MILLIS_PER_MINUTE;
2041                if (iShowSeparators) {
2042                    buf.append(':');
2043                }
2044                FormatUtils.appendPaddedInteger(buf, minutes, 2);
2045                if (iMaxFields == 2) {
2046                    return;
2047                }
2048                displayOffset -= minutes * DateTimeConstants.MILLIS_PER_MINUTE;
2049                if (displayOffset == 0 && iMinFields <= 2) {
2050                    return;
2051                }
2052    
2053                int seconds = displayOffset / DateTimeConstants.MILLIS_PER_SECOND;
2054                if (iShowSeparators) {
2055                    buf.append(':');
2056                }
2057                FormatUtils.appendPaddedInteger(buf, seconds, 2);
2058                if (iMaxFields == 3) {
2059                    return;
2060                }
2061                displayOffset -= seconds * DateTimeConstants.MILLIS_PER_SECOND;
2062                if (displayOffset == 0 && iMinFields <= 3) {
2063                    return;
2064                }
2065    
2066                if (iShowSeparators) {
2067                    buf.append('.');
2068                }
2069                FormatUtils.appendPaddedInteger(buf, displayOffset, 3);
2070            }
2071            
2072            public void printTo(
2073                    Writer out, long instant, Chronology chrono,
2074                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
2075                if (displayZone == null) {
2076                    return;  // no zone
2077                }
2078                if (displayOffset == 0 && iZeroOffsetText != null) {
2079                    out.write(iZeroOffsetText);
2080                    return;
2081                }
2082                if (displayOffset >= 0) {
2083                    out.write('+');
2084                } else {
2085                    out.write('-');
2086                    displayOffset = -displayOffset;
2087                }
2088    
2089                int hours = displayOffset / DateTimeConstants.MILLIS_PER_HOUR;
2090                FormatUtils.writePaddedInteger(out, hours, 2);
2091                if (iMaxFields == 1) {
2092                    return;
2093                }
2094                displayOffset -= hours * (int)DateTimeConstants.MILLIS_PER_HOUR;
2095                if (displayOffset == 0 && iMinFields == 1) {
2096                    return;
2097                }
2098    
2099                int minutes = displayOffset / DateTimeConstants.MILLIS_PER_MINUTE;
2100                if (iShowSeparators) {
2101                    out.write(':');
2102                }
2103                FormatUtils.writePaddedInteger(out, minutes, 2);
2104                if (iMaxFields == 2) {
2105                    return;
2106                }
2107                displayOffset -= minutes * DateTimeConstants.MILLIS_PER_MINUTE;
2108                if (displayOffset == 0 && iMinFields == 2) {
2109                    return;
2110                }
2111    
2112                int seconds = displayOffset / DateTimeConstants.MILLIS_PER_SECOND;
2113                if (iShowSeparators) {
2114                    out.write(':');
2115                }
2116                FormatUtils.writePaddedInteger(out, seconds, 2);
2117                if (iMaxFields == 3) {
2118                    return;
2119                }
2120                displayOffset -= seconds * DateTimeConstants.MILLIS_PER_SECOND;
2121                if (displayOffset == 0 && iMinFields == 3) {
2122                    return;
2123                }
2124    
2125                if (iShowSeparators) {
2126                    out.write('.');
2127                }
2128                FormatUtils.writePaddedInteger(out, displayOffset, 3);
2129            }
2130    
2131            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
2132                // no zone info
2133            }
2134    
2135            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
2136                // no zone info
2137            }
2138    
2139            public int estimateParsedLength() {
2140                return estimatePrintedLength();
2141            }
2142    
2143            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
2144                int limit = text.length() - position;
2145    
2146                zeroOffset:
2147                if (iZeroOffsetText != null) {
2148                    if (iZeroOffsetText.length() == 0) {
2149                        // Peek ahead, looking for sign character.
2150                        if (limit > 0) {
2151                            char c = text.charAt(position);
2152                            if (c == '-' || c == '+') {
2153                                break zeroOffset;
2154                            }
2155                        }
2156                        bucket.setOffset(0);
2157                        return position;
2158                    }
2159                    if (text.regionMatches(true, position, iZeroOffsetText, 0,
2160                                           iZeroOffsetText.length())) {
2161                        bucket.setOffset(0);
2162                        return position + iZeroOffsetText.length();
2163                    }
2164                }
2165    
2166                // Format to expect is sign character followed by at least one digit.
2167    
2168                if (limit <= 1) {
2169                    return ~position;
2170                }
2171    
2172                boolean negative;
2173                char c = text.charAt(position);
2174                if (c == '-') {
2175                    negative = true;
2176                } else if (c == '+') {
2177                    negative = false;
2178                } else {
2179                    return ~position;
2180                }
2181    
2182                limit--;
2183                position++;
2184    
2185                // Format following sign is one of:
2186                //
2187                // hh
2188                // hhmm
2189                // hhmmss
2190                // hhmmssSSS
2191                // hh:mm
2192                // hh:mm:ss
2193                // hh:mm:ss.SSS
2194    
2195                // First parse hours.
2196    
2197                if (digitCount(text, position, 2) < 2) {
2198                    // Need two digits for hour.
2199                    return ~position;
2200                }
2201    
2202                int offset;
2203    
2204                int hours = FormatUtils.parseTwoDigits(text, position);
2205                if (hours > 23) {
2206                    return ~position;
2207                }
2208                offset = hours * DateTimeConstants.MILLIS_PER_HOUR;
2209                limit -= 2;
2210                position += 2;
2211    
2212                parse: {
2213                    // Need to decide now if separators are expected or parsing
2214                    // stops at hour field.
2215    
2216                    if (limit <= 0) {
2217                        break parse;
2218                    }
2219    
2220                    boolean expectSeparators;
2221                    c = text.charAt(position);
2222                    if (c == ':') {
2223                        expectSeparators = true;
2224                        limit--;
2225                        position++;
2226                    } else if (c >= '0' && c <= '9') {
2227                        expectSeparators = false;
2228                    } else {
2229                        break parse;
2230                    }
2231    
2232                    // Proceed to parse minutes.
2233    
2234                    int count = digitCount(text, position, 2);
2235                    if (count == 0 && !expectSeparators) {
2236                        break parse;
2237                    } else if (count < 2) {
2238                        // Need two digits for minute.
2239                        return ~position;
2240                    }
2241    
2242                    int minutes = FormatUtils.parseTwoDigits(text, position);
2243                    if (minutes > 59) {
2244                        return ~position;
2245                    }
2246                    offset += minutes * DateTimeConstants.MILLIS_PER_MINUTE;
2247                    limit -= 2;
2248                    position += 2;
2249    
2250                    // Proceed to parse seconds.
2251    
2252                    if (limit <= 0) {
2253                        break parse;
2254                    }
2255    
2256                    if (expectSeparators) {
2257                        if (text.charAt(position) != ':') {
2258                            break parse;
2259                        }
2260                        limit--;
2261                        position++;
2262                    }
2263    
2264                    count = digitCount(text, position, 2);
2265                    if (count == 0 && !expectSeparators) {
2266                        break parse;
2267                    } else if (count < 2) {
2268                        // Need two digits for second.
2269                        return ~position;
2270                    }
2271    
2272                    int seconds = FormatUtils.parseTwoDigits(text, position);
2273                    if (seconds > 59) {
2274                        return ~position;
2275                    }
2276                    offset += seconds * DateTimeConstants.MILLIS_PER_SECOND;
2277                    limit -= 2;
2278                    position += 2;
2279    
2280                    // Proceed to parse fraction of second.
2281    
2282                    if (limit <= 0) {
2283                        break parse;
2284                    }
2285    
2286                    if (expectSeparators) {
2287                        if (text.charAt(position) != '.' && text.charAt(position) != ',') {
2288                            break parse;
2289                        }
2290                        limit--;
2291                        position++;
2292                    }
2293                    
2294                    count = digitCount(text, position, 3);
2295                    if (count == 0 && !expectSeparators) {
2296                        break parse;
2297                    } else if (count < 1) {
2298                        // Need at least one digit for fraction of second.
2299                        return ~position;
2300                    }
2301    
2302                    offset += (text.charAt(position++) - '0') * 100;
2303                    if (count > 1) {
2304                        offset += (text.charAt(position++) - '0') * 10;
2305                        if (count > 2) {
2306                            offset += text.charAt(position++) - '0';
2307                        }
2308                    }
2309                }
2310    
2311                bucket.setOffset(negative ? -offset : offset);
2312                return position;
2313            }
2314    
2315            /**
2316             * Returns actual amount of digits to parse, but no more than original
2317             * 'amount' parameter.
2318             */
2319            private int digitCount(String text, int position, int amount) {
2320                int limit = Math.min(text.length() - position, amount);
2321                amount = 0;
2322                for (; limit > 0; limit--) {
2323                    char c = text.charAt(position + amount);
2324                    if (c < '0' || c > '9') {
2325                        break;
2326                    }
2327                    amount++;
2328                }
2329                return amount;
2330            }
2331        }
2332    
2333        //-----------------------------------------------------------------------
2334        static class TimeZoneName
2335                implements DateTimePrinter {
2336    
2337            static final int LONG_NAME = 0;
2338            static final int SHORT_NAME = 1;
2339            static final int ID = 2;
2340    
2341            private final int iType;
2342    
2343            TimeZoneName(int type) {
2344                super();
2345                iType = type;
2346            }
2347    
2348            public int estimatePrintedLength() {
2349                return (iType == SHORT_NAME ? 4 : 20);
2350            }
2351    
2352            public void printTo(
2353                    StringBuffer buf, long instant, Chronology chrono,
2354                    int displayOffset, DateTimeZone displayZone, Locale locale) {
2355                buf.append(print(instant - displayOffset, displayZone, locale));
2356            }
2357    
2358            public void printTo(
2359                    Writer out, long instant, Chronology chrono,
2360                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
2361                out.write(print(instant - displayOffset, displayZone, locale));
2362            }
2363    
2364            private String print(long instant, DateTimeZone displayZone, Locale locale) {
2365                if (displayZone == null) {
2366                    return "";  // no zone
2367                }
2368                switch (iType) {
2369                    case LONG_NAME:
2370                        return displayZone.getName(instant, locale);
2371                    case SHORT_NAME:
2372                        return displayZone.getShortName(instant, locale);
2373                    case ID:
2374                        return displayZone.getID();
2375                }
2376                return "";
2377            }
2378    
2379            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
2380                // no zone info
2381            }
2382    
2383            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
2384                // no zone info
2385            }
2386        }
2387    
2388        //-----------------------------------------------------------------------
2389        static class Composite
2390                implements DateTimePrinter, DateTimeParser {
2391    
2392            private final DateTimePrinter[] iPrinters;
2393            private final DateTimeParser[] iParsers;
2394    
2395            private final int iPrintedLengthEstimate;
2396            private final int iParsedLengthEstimate;
2397    
2398            Composite(List elementPairs) {
2399                super();
2400    
2401                List printerList = new ArrayList();
2402                List parserList = new ArrayList();
2403    
2404                decompose(elementPairs, printerList, parserList);
2405    
2406                if (printerList.size() <= 0) {
2407                    iPrinters = null;
2408                    iPrintedLengthEstimate = 0;
2409                } else {
2410                    int size = printerList.size();
2411                    iPrinters = new DateTimePrinter[size];
2412                    int printEst = 0;
2413                    for (int i=0; i<size; i++) {
2414                        DateTimePrinter printer = (DateTimePrinter) printerList.get(i);
2415                        printEst += printer.estimatePrintedLength();
2416                        iPrinters[i] = printer;
2417                    }
2418                    iPrintedLengthEstimate = printEst;
2419                }
2420    
2421                if (parserList.size() <= 0) {
2422                    iParsers = null;
2423                    iParsedLengthEstimate = 0;
2424                } else {
2425                    int size = parserList.size();
2426                    iParsers = new DateTimeParser[size];
2427                    int parseEst = 0;
2428                    for (int i=0; i<size; i++) {
2429                        DateTimeParser parser = (DateTimeParser) parserList.get(i);
2430                        parseEst += parser.estimateParsedLength();
2431                        iParsers[i] = parser;
2432                    }
2433                    iParsedLengthEstimate = parseEst;
2434                }
2435            }
2436    
2437            public int estimatePrintedLength() {
2438                return iPrintedLengthEstimate;
2439            }
2440    
2441            public void printTo(
2442                    StringBuffer buf, long instant, Chronology chrono,
2443                    int displayOffset, DateTimeZone displayZone, Locale locale) {
2444                DateTimePrinter[] elements = iPrinters;
2445                if (elements == null) {
2446                    throw new UnsupportedOperationException();
2447                }
2448    
2449                if (locale == null) {
2450                    // Guard against default locale changing concurrently.
2451                    locale = Locale.getDefault();
2452                }
2453    
2454                int len = elements.length;
2455                for (int i = 0; i < len; i++) {
2456                    elements[i].printTo(buf, instant, chrono, displayOffset, displayZone, locale);
2457                }
2458            }
2459    
2460            public void printTo(
2461                    Writer out, long instant, Chronology chrono,
2462                    int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
2463                DateTimePrinter[] elements = iPrinters;
2464                if (elements == null) {
2465                    throw new UnsupportedOperationException();
2466                }
2467    
2468                if (locale == null) {
2469                    // Guard against default locale changing concurrently.
2470                    locale = Locale.getDefault();
2471                }
2472    
2473                int len = elements.length;
2474                for (int i = 0; i < len; i++) {
2475                    elements[i].printTo(out, instant, chrono, displayOffset, displayZone, locale);
2476                }
2477            }
2478    
2479            public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
2480                DateTimePrinter[] elements = iPrinters;
2481                if (elements == null) {
2482                    throw new UnsupportedOperationException();
2483                }
2484    
2485                if (locale == null) {
2486                    // Guard against default locale changing concurrently.
2487                    locale = Locale.getDefault();
2488                }
2489    
2490                int len = elements.length;
2491                for (int i=0; i<len; i++) {
2492                    elements[i].printTo(buf, partial, locale);
2493                }
2494            }
2495    
2496            public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
2497                DateTimePrinter[] elements = iPrinters;
2498                if (elements == null) {
2499                    throw new UnsupportedOperationException();
2500                }
2501    
2502                if (locale == null) {
2503                    // Guard against default locale changing concurrently.
2504                    locale = Locale.getDefault();
2505                }
2506    
2507                int len = elements.length;
2508                for (int i=0; i<len; i++) {
2509                    elements[i].printTo(out, partial, locale);
2510                }
2511            }
2512    
2513            public int estimateParsedLength() {
2514                return iParsedLengthEstimate;
2515            }
2516    
2517            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
2518                DateTimeParser[] elements = iParsers;
2519                if (elements == null) {
2520                    throw new UnsupportedOperationException();
2521                }
2522    
2523                int len = elements.length;
2524                for (int i=0; i<len && position >= 0; i++) {
2525                    position = elements[i].parseInto(bucket, text, position);
2526                }
2527                return position;
2528            }
2529    
2530            boolean isPrinter() {
2531                return iPrinters != null;
2532            }
2533    
2534            boolean isParser() {
2535                return iParsers != null;
2536            }
2537    
2538            /**
2539             * Processes the element pairs, putting results into the given printer
2540             * and parser lists.
2541             */
2542            private void decompose(List elementPairs, List printerList, List parserList) {
2543                int size = elementPairs.size();
2544                for (int i=0; i<size; i+=2) {
2545                    Object element = elementPairs.get(i);
2546                    if (element instanceof DateTimePrinter) {
2547                        if (element instanceof Composite) {
2548                            addArrayToList(printerList, ((Composite)element).iPrinters);
2549                        } else {
2550                            printerList.add(element);
2551                        }
2552                    }
2553    
2554                    element = elementPairs.get(i + 1);
2555                    if (element instanceof DateTimeParser) {
2556                        if (element instanceof Composite) {
2557                            addArrayToList(parserList, ((Composite)element).iParsers);
2558                        } else {
2559                            parserList.add(element);
2560                        }
2561                    }
2562                }
2563            }
2564    
2565            private void addArrayToList(List list, Object[] array) {
2566                if (array != null) {
2567                    for (int i=0; i<array.length; i++) {
2568                        list.add(array[i]);
2569                    }
2570                }
2571            }
2572        }
2573    
2574        //-----------------------------------------------------------------------
2575        static class MatchingParser
2576                implements DateTimeParser {
2577    
2578            private final DateTimeParser[] iParsers;
2579            private final int iParsedLengthEstimate;
2580    
2581            MatchingParser(DateTimeParser[] parsers) {
2582                super();
2583                iParsers = parsers;
2584                int est = 0;
2585                for (int i=parsers.length; --i>=0 ;) {
2586                    DateTimeParser parser = parsers[i];
2587                    if (parser != null) {
2588                        int len = parser.estimateParsedLength();
2589                        if (len > est) {
2590                            est = len;
2591                        }
2592                    }
2593                }
2594                iParsedLengthEstimate = est;
2595            }
2596    
2597            public int estimateParsedLength() {
2598                return iParsedLengthEstimate;
2599            }
2600    
2601            public int parseInto(DateTimeParserBucket bucket, String text, int position) {
2602                DateTimeParser[] parsers = iParsers;
2603                int length = parsers.length;
2604    
2605                final Object originalState = bucket.saveState();
2606                boolean isOptional = false;
2607    
2608                int bestValidPos = position;
2609                Object bestValidState = null;
2610    
2611                int bestInvalidPos = position;
2612    
2613                for (int i=0; i<length; i++) {
2614                    DateTimeParser parser = parsers[i];
2615                    if (parser == null) {
2616                        // The empty parser wins only if nothing is better.
2617                        if (bestValidPos <= position) {
2618                            return position;
2619                        }
2620                        isOptional = true;
2621                        break;
2622                    }
2623                    int parsePos = parser.parseInto(bucket, text, position);
2624                    if (parsePos >= position) {
2625                        if (parsePos > bestValidPos) {
2626                            if (parsePos >= text.length() ||
2627                                (i + 1) >= length || parsers[i + 1] == null) {
2628    
2629                                // Completely parsed text or no more parsers to
2630                                // check. Skip the rest.
2631                                return parsePos;
2632                            }
2633                            bestValidPos = parsePos;
2634                            bestValidState = bucket.saveState();
2635                        }
2636                    } else {
2637                        if (parsePos < 0) {
2638                            parsePos = ~parsePos;
2639                            if (parsePos > bestInvalidPos) {
2640                                bestInvalidPos = parsePos;
2641                            }
2642                        }
2643                    }
2644                    bucket.restoreState(originalState);
2645                }
2646    
2647                if (bestValidPos > position || (bestValidPos == position && isOptional)) {
2648                    // Restore the state to the best valid parse.
2649                    if (bestValidState != null) {
2650                        bucket.restoreState(bestValidState);
2651                    }
2652                    return bestValidPos;
2653                }
2654    
2655                return ~bestInvalidPos;
2656            }
2657        }
2658    
2659    }