/*
* Copyright 2001-2014 Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joda.time.format;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.joda.time.Chronology;
import org.joda.time.DateTimeConstants;
import org.joda.time.DateTimeField;
import org.joda.time.DateTimeFieldType;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.MutableDateTime;
import org.joda.time.MutableDateTime.Property;
import org.joda.time.ReadablePartial;
import org.joda.time.field.MillisDurationField;
import org.joda.time.field.PreciseDateTimeField;
/**
* Factory that creates complex instances of DateTimeFormatter via method calls.
*
* Datetime formatting is performed by the {@link DateTimeFormatter} class.
* Three classes provide factory methods to create formatters, and this is one.
* The others are {@link DateTimeFormat} and {@link ISODateTimeFormat}.
*
* DateTimeFormatterBuilder is used for constructing formatters which are then
* used to print or parse. The formatters are built by appending specific fields
* or other formatters to an instance of this builder.
*
* For example, a formatter that prints month and year, like "January 1970",
* can be constructed as follows:
*
*
* DateTimeFormatter monthAndYear = new DateTimeFormatterBuilder()
* .appendMonthOfYearText()
* .appendLiteral(' ')
* .appendYear(4, 4)
* .toFormatter();
*
*
* DateTimeFormatterBuilder itself is mutable and not thread-safe, but the
* formatters that it builds are thread-safe and immutable.
*
* @author Brian S O'Neill
* @author Stephen Colebourne
* @author Fredrik Borgh
* @since 1.0
* @see DateTimeFormat
* @see ISODateTimeFormat
*/
public class DateTimeFormatterBuilder {
/** Array of printers and parsers (alternating). */
private ArrayList iElementPairs;
/** Cache of the last returned formatter. */
private Object iFormatter;
//-----------------------------------------------------------------------
/**
* Creates a DateTimeFormatterBuilder.
*/
public DateTimeFormatterBuilder() {
super();
iElementPairs = new ArrayList();
}
//-----------------------------------------------------------------------
/**
* Constructs a DateTimeFormatter using all the appended elements.
*
* This is the main method used by applications at the end of the build
* process to create a usable formatter.
*
* Subsequent changes to this builder do not affect the returned formatter.
*
* The returned formatter may not support both printing and parsing.
* The methods {@link DateTimeFormatter#isPrinter()} and
* {@link DateTimeFormatter#isParser()} will help you determine the state
* of the formatter.
*
* @throws UnsupportedOperationException if neither printing nor parsing is supported
*/
public DateTimeFormatter toFormatter() {
Object f = getFormatter();
InternalPrinter printer = null;
if (isPrinter(f)) {
printer = (InternalPrinter) f;
}
InternalParser parser = null;
if (isParser(f)) {
parser = (InternalParser) f;
}
if (printer != null || parser != null) {
return new DateTimeFormatter(printer, parser);
}
throw new UnsupportedOperationException("Both printing and parsing not supported");
}
/**
* Internal method to create a DateTimePrinter instance using all the
* appended elements.
*
* Most applications will not use this method.
* If you want a printer in an application, call {@link #toFormatter()}
* and just use the printing API.
*
* Subsequent changes to this builder do not affect the returned printer.
*
* @throws UnsupportedOperationException if printing is not supported
*/
public DateTimePrinter toPrinter() {
Object f = getFormatter();
if (isPrinter(f)) {
InternalPrinter ip = (InternalPrinter) f;
return InternalPrinterDateTimePrinter.of(ip);
}
throw new UnsupportedOperationException("Printing is not supported");
}
/**
* Internal method to create a DateTimeParser instance using all the
* appended elements.
*
* Most applications will not use this method.
* If you want a parser in an application, call {@link #toFormatter()}
* and just use the parsing API.
*
* Subsequent changes to this builder do not affect the returned parser.
*
* @throws UnsupportedOperationException if parsing is not supported
*/
public DateTimeParser toParser() {
Object f = getFormatter();
if (isParser(f)) {
InternalParser ip = (InternalParser) f;
return InternalParserDateTimeParser.of(ip);
}
throw new UnsupportedOperationException("Parsing is not supported");
}
//-----------------------------------------------------------------------
/**
* Returns true if toFormatter can be called without throwing an
* UnsupportedOperationException.
*
* @return true if a formatter can be built
*/
public boolean canBuildFormatter() {
return isFormatter(getFormatter());
}
/**
* Returns true if toPrinter can be called without throwing an
* UnsupportedOperationException.
*
* @return true if a printer can be built
*/
public boolean canBuildPrinter() {
return isPrinter(getFormatter());
}
/**
* Returns true if toParser can be called without throwing an
* UnsupportedOperationException.
*
* @return true if a parser can be built
*/
public boolean canBuildParser() {
return isParser(getFormatter());
}
//-----------------------------------------------------------------------
/**
* Clears out all the appended elements, allowing this builder to be
* reused.
*/
public void clear() {
iFormatter = null;
iElementPairs.clear();
}
//-----------------------------------------------------------------------
/**
* Appends another formatter.
*
* This extracts the underlying printer and parser and appends them
* The printer and parser interfaces are the low-level part of the formatting API.
* Normally, instances are extracted from another formatter.
* Note however that any formatter specific information, such as the locale,
* time-zone, chronology, offset parsing or pivot/default year, will not be
* extracted by this method.
*
* @param formatter the formatter to add
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if formatter is null or of an invalid type
*/
public DateTimeFormatterBuilder append(DateTimeFormatter formatter) {
if (formatter == null) {
throw new IllegalArgumentException("No formatter supplied");
}
return append0(formatter.getPrinter0(), formatter.getParser0());
}
/**
* Appends just a printer. With no matching parser, a parser cannot be
* built from this DateTimeFormatterBuilder.
*
* The printer interface is part of the low-level part of the formatting API.
* Normally, instances are extracted from another formatter.
* Note however that any formatter specific information, such as the locale,
* time-zone, chronology, offset parsing or pivot/default year, will not be
* extracted by this method.
*
* @param printer the printer to add, not null
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if printer is null or of an invalid type
*/
public DateTimeFormatterBuilder append(DateTimePrinter printer) {
checkPrinter(printer);
return append0(DateTimePrinterInternalPrinter.of(printer), null);
}
/**
* Appends just a parser. With no matching printer, a printer cannot be
* built from this builder.
*
* The parser interface is part of the low-level part of the formatting API.
* Normally, instances are extracted from another formatter.
* Note however that any formatter specific information, such as the locale,
* time-zone, chronology, offset parsing or pivot/default year, will not be
* extracted by this method.
*
* @param parser the parser to add, not null
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if parser is null or of an invalid type
*/
public DateTimeFormatterBuilder append(DateTimeParser parser) {
checkParser(parser);
return append0(null, DateTimeParserInternalParser.of(parser));
}
/**
* Appends a printer/parser pair.
*
* The printer and parser interfaces are the low-level part of the formatting API.
* Normally, instances are extracted from another formatter.
* Note however that any formatter specific information, such as the locale,
* time-zone, chronology, offset parsing or pivot/default year, will not be
* extracted by this method.
*
* @param printer the printer to add, not null
* @param parser the parser to add, not null
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if printer or parser is null or of an invalid type
*/
public DateTimeFormatterBuilder append(DateTimePrinter printer, DateTimeParser parser) {
checkPrinter(printer);
checkParser(parser);
return append0(DateTimePrinterInternalPrinter.of(printer), DateTimeParserInternalParser.of(parser));
}
/**
* Appends a printer and a set of matching parsers. When parsing, the first
* parser in the list is selected for parsing. If it fails, the next is
* chosen, and so on. If none of these parsers succeeds, then the failed
* position of the parser that made the greatest progress is returned.
*
* Only the printer is optional. In addition, it is illegal for any but the
* last of the parser array elements to be null. If the last element is
* null, this represents the empty parser. The presence of an empty parser
* indicates that the entire array of parse formats is optional.
*
* The printer and parser interfaces are the low-level part of the formatting API.
* Normally, instances are extracted from another formatter.
* Note however that any formatter specific information, such as the locale,
* time-zone, chronology, offset parsing or pivot/default year, will not be
* extracted by this method.
*
* @param printer the printer to add
* @param parsers the parsers to add
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if any printer or parser is of an invalid type
* @throws IllegalArgumentException if any parser element but the last is null
*/
public DateTimeFormatterBuilder append(DateTimePrinter printer, DateTimeParser[] parsers) {
if (printer != null) {
checkPrinter(printer);
}
if (parsers == null) {
throw new IllegalArgumentException("No parsers supplied");
}
int length = parsers.length;
if (length == 1) {
if (parsers[0] == null) {
throw new IllegalArgumentException("No parser supplied");
}
return append0(DateTimePrinterInternalPrinter.of(printer), DateTimeParserInternalParser.of(parsers[0]));
}
InternalParser[] copyOfParsers = new InternalParser[length];
int i;
for (i = 0; i < length - 1; i++) {
if ((copyOfParsers[i] = DateTimeParserInternalParser.of(parsers[i])) == null) {
throw new IllegalArgumentException("Incomplete parser array");
}
}
copyOfParsers[i] = DateTimeParserInternalParser.of(parsers[i]);
return append0(DateTimePrinterInternalPrinter.of(printer), new MatchingParser(copyOfParsers));
}
/**
* Appends just a parser element which is optional. With no matching
* printer, a printer cannot be built from this DateTimeFormatterBuilder.
*
* The parser interface is part of the low-level part of the formatting API.
* Normally, instances are extracted from another formatter.
* Note however that any formatter specific information, such as the locale,
* time-zone, chronology, offset parsing or pivot/default year, will not be
* extracted by this method.
*
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if parser is null or of an invalid type
*/
public DateTimeFormatterBuilder appendOptional(DateTimeParser parser) {
checkParser(parser);
InternalParser[] parsers = new InternalParser[] {DateTimeParserInternalParser.of(parser), null};
return append0(null, new MatchingParser(parsers));
}
//-----------------------------------------------------------------------
/**
* Checks if the parser is non null and a provider.
*
* @param parser the parser to check
*/
private void checkParser(DateTimeParser parser) {
if (parser == null) {
throw new IllegalArgumentException("No parser supplied");
}
}
/**
* Checks if the printer is non null and a provider.
*
* @param printer the printer to check
*/
private void checkPrinter(DateTimePrinter printer) {
if (printer == null) {
throw new IllegalArgumentException("No printer supplied");
}
}
private DateTimeFormatterBuilder append0(Object element) {
iFormatter = null;
// Add the element as both a printer and parser.
iElementPairs.add(element);
iElementPairs.add(element);
return this;
}
private DateTimeFormatterBuilder append0(
InternalPrinter printer, InternalParser parser) {
iFormatter = null;
iElementPairs.add(printer);
iElementPairs.add(parser);
return this;
}
//-----------------------------------------------------------------------
/**
* Instructs the printer to emit a specific character, and the parser to
* expect it. The parser is case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendLiteral(char c) {
return append0(new CharacterLiteral(c));
}
/**
* Instructs the printer to emit specific text, and the parser to expect
* it. The parser is case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if text is null
*/
public DateTimeFormatterBuilder appendLiteral(String text) {
if (text == null) {
throw new IllegalArgumentException("Literal must not be null");
}
switch (text.length()) {
case 0:
return this;
case 1:
return append0(new CharacterLiteral(text.charAt(0)));
default:
return append0(new StringLiteral(text));
}
}
/**
* Instructs the printer to emit a field value as a decimal number, and the
* parser to expect an unsigned decimal number.
*
* @param fieldType type of field to append
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null
*/
public DateTimeFormatterBuilder appendDecimal(
DateTimeFieldType fieldType, int minDigits, int maxDigits) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
if (maxDigits < minDigits) {
maxDigits = minDigits;
}
if (minDigits < 0 || maxDigits <= 0) {
throw new IllegalArgumentException();
}
if (minDigits <= 1) {
return append0(new UnpaddedNumber(fieldType, maxDigits, false));
} else {
return append0(new PaddedNumber(fieldType, maxDigits, false, minDigits));
}
}
/**
* Instructs the printer to emit a field value as a fixed-width decimal
* number (smaller numbers will be left-padded with zeros), and the parser
* to expect an unsigned decimal number with the same fixed width.
*
* @param fieldType type of field to append
* @param numDigits the exact number of digits to parse or print, except if
* printed value requires more digits
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null or if numDigits <= 0
* @since 1.5
*/
public DateTimeFormatterBuilder appendFixedDecimal(
DateTimeFieldType fieldType, int numDigits) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
if (numDigits <= 0) {
throw new IllegalArgumentException("Illegal number of digits: " + numDigits);
}
return append0(new FixedNumber(fieldType, numDigits, false));
}
/**
* Instructs the printer to emit a field value as a decimal number, and the
* parser to expect a signed decimal number.
*
* @param fieldType type of field to append
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null
*/
public DateTimeFormatterBuilder appendSignedDecimal(
DateTimeFieldType fieldType, int minDigits, int maxDigits) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
if (maxDigits < minDigits) {
maxDigits = minDigits;
}
if (minDigits < 0 || maxDigits <= 0) {
throw new IllegalArgumentException();
}
if (minDigits <= 1) {
return append0(new UnpaddedNumber(fieldType, maxDigits, true));
} else {
return append0(new PaddedNumber(fieldType, maxDigits, true, minDigits));
}
}
/**
* Instructs the printer to emit a field value as a fixed-width decimal
* number (smaller numbers will be left-padded with zeros), and the parser
* to expect an signed decimal number with the same fixed width.
*
* @param fieldType type of field to append
* @param numDigits the exact number of digits to parse or print, except if
* printed value requires more digits
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null or if numDigits <= 0
* @since 1.5
*/
public DateTimeFormatterBuilder appendFixedSignedDecimal(
DateTimeFieldType fieldType, int numDigits) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
if (numDigits <= 0) {
throw new IllegalArgumentException("Illegal number of digits: " + numDigits);
}
return append0(new FixedNumber(fieldType, numDigits, true));
}
/**
* Instructs the printer to emit a field value as text, and the
* parser to expect text.
*
* @param fieldType type of field to append
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null
*/
public DateTimeFormatterBuilder appendText(DateTimeFieldType fieldType) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
return append0(new TextField(fieldType, false));
}
/**
* Instructs the printer to emit a field value as short text, and the
* parser to expect text.
*
* @param fieldType type of field to append
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null
*/
public DateTimeFormatterBuilder appendShortText(DateTimeFieldType fieldType) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
return append0(new TextField(fieldType, true));
}
/**
* Instructs the printer to emit a remainder of time as a decimal fraction,
* without decimal point. For example, if the field is specified as
* minuteOfHour and the time is 12:30:45, the value printed is 75. A
* decimal point is implied, so the fraction is 0.75, or three-quarters of
* a minute.
*
* @param fieldType type of field to append
* @param minDigits minimum number of digits to print.
* @param maxDigits maximum number of digits to print or parse.
* @return this DateTimeFormatterBuilder, for chaining
* @throws IllegalArgumentException if field type is null
*/
public DateTimeFormatterBuilder appendFraction(
DateTimeFieldType fieldType, int minDigits, int maxDigits) {
if (fieldType == null) {
throw new IllegalArgumentException("Field type must not be null");
}
if (maxDigits < minDigits) {
maxDigits = minDigits;
}
if (minDigits < 0 || maxDigits <= 0) {
throw new IllegalArgumentException();
}
return append0(new Fraction(fieldType, minDigits, maxDigits));
}
/**
* Appends the print/parse of a fractional second.
*
* This reliably handles the case where fractional digits are being handled
* beyond a visible decimal point. The digits parsed will always be treated
* as the most significant (numerically largest) digits.
* Thus '23' will be parsed as 230 milliseconds.
* Contrast this behaviour to {@link #appendMillisOfSecond}.
* This method does not print or parse the decimal point itself.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to print or parse
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendFractionOfSecond(int minDigits, int maxDigits) {
return appendFraction(DateTimeFieldType.secondOfDay(), minDigits, maxDigits);
}
/**
* Appends the print/parse of a fractional minute.
*
* This reliably handles the case where fractional digits are being handled
* beyond a visible decimal point. The digits parsed will always be treated
* as the most significant (numerically largest) digits.
* Thus '23' will be parsed as 0.23 minutes (converted to milliseconds).
* This method does not print or parse the decimal point itself.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to print or parse
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendFractionOfMinute(int minDigits, int maxDigits) {
return appendFraction(DateTimeFieldType.minuteOfDay(), minDigits, maxDigits);
}
/**
* Appends the print/parse of a fractional hour.
*
* This reliably handles the case where fractional digits are being handled
* beyond a visible decimal point. The digits parsed will always be treated
* as the most significant (numerically largest) digits.
* Thus '23' will be parsed as 0.23 hours (converted to milliseconds).
* This method does not print or parse the decimal point itself.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to print or parse
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendFractionOfHour(int minDigits, int maxDigits) {
return appendFraction(DateTimeFieldType.hourOfDay(), minDigits, maxDigits);
}
/**
* Appends the print/parse of a fractional day.
*
* This reliably handles the case where fractional digits are being handled
* beyond a visible decimal point. The digits parsed will always be treated
* as the most significant (numerically largest) digits.
* Thus '23' will be parsed as 0.23 days (converted to milliseconds).
* This method does not print or parse the decimal point itself.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to print or parse
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendFractionOfDay(int minDigits, int maxDigits) {
return appendFraction(DateTimeFieldType.dayOfYear(), minDigits, maxDigits);
}
/**
* Instructs the printer to emit a numeric millisOfSecond field.
*
* This method will append a field that prints a three digit value.
* During parsing the value that is parsed is assumed to be three digits.
* If less than three digits are present then they will be counted as the
* smallest parts of the millisecond. This is probably not what you want
* if you are using the field as a fraction. Instead, a fractional
* millisecond should be produced using {@link #appendFractionOfSecond}.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMillisOfSecond(int minDigits) {
return appendDecimal(DateTimeFieldType.millisOfSecond(), minDigits, 3);
}
/**
* Instructs the printer to emit a numeric millisOfDay field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMillisOfDay(int minDigits) {
return appendDecimal(DateTimeFieldType.millisOfDay(), minDigits, 8);
}
/**
* Instructs the printer to emit a numeric secondOfMinute field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendSecondOfMinute(int minDigits) {
return appendDecimal(DateTimeFieldType.secondOfMinute(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric secondOfDay field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendSecondOfDay(int minDigits) {
return appendDecimal(DateTimeFieldType.secondOfDay(), minDigits, 5);
}
/**
* Instructs the printer to emit a numeric minuteOfHour field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMinuteOfHour(int minDigits) {
return appendDecimal(DateTimeFieldType.minuteOfHour(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric minuteOfDay field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMinuteOfDay(int minDigits) {
return appendDecimal(DateTimeFieldType.minuteOfDay(), minDigits, 4);
}
/**
* Instructs the printer to emit a numeric hourOfDay field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendHourOfDay(int minDigits) {
return appendDecimal(DateTimeFieldType.hourOfDay(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric clockhourOfDay field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendClockhourOfDay(int minDigits) {
return appendDecimal(DateTimeFieldType.clockhourOfDay(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric hourOfHalfday field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendHourOfHalfday(int minDigits) {
return appendDecimal(DateTimeFieldType.hourOfHalfday(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric clockhourOfHalfday field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendClockhourOfHalfday(int minDigits) {
return appendDecimal(DateTimeFieldType.clockhourOfHalfday(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric dayOfWeek field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendDayOfWeek(int minDigits) {
return appendDecimal(DateTimeFieldType.dayOfWeek(), minDigits, 1);
}
/**
* Instructs the printer to emit a numeric dayOfMonth field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendDayOfMonth(int minDigits) {
return appendDecimal(DateTimeFieldType.dayOfMonth(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric dayOfYear field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendDayOfYear(int minDigits) {
return appendDecimal(DateTimeFieldType.dayOfYear(), minDigits, 3);
}
/**
* Instructs the printer to emit a numeric weekOfWeekyear field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendWeekOfWeekyear(int minDigits) {
return appendDecimal(DateTimeFieldType.weekOfWeekyear(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric weekyear field.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendWeekyear(int minDigits, int maxDigits) {
return appendSignedDecimal(DateTimeFieldType.weekyear(), minDigits, maxDigits);
}
/**
* Instructs the printer to emit a numeric monthOfYear field.
*
* @param minDigits minimum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMonthOfYear(int minDigits) {
return appendDecimal(DateTimeFieldType.monthOfYear(), minDigits, 2);
}
/**
* Instructs the printer to emit a numeric year field.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendYear(int minDigits, int maxDigits) {
return appendSignedDecimal(DateTimeFieldType.year(), minDigits, maxDigits);
}
/**
* Instructs the printer to emit a numeric year field which always prints
* and parses two digits. A pivot year is used during parsing to determine
* the range of supported years as (pivot - 50) .. (pivot + 49)
.
*
*
* pivot supported range 00 is 20 is 40 is 60 is 80 is
* ---------------------------------------------------------------
* 1950 1900..1999 1900 1920 1940 1960 1980
* 1975 1925..2024 2000 2020 1940 1960 1980
* 2000 1950..2049 2000 2020 2040 1960 1980
* 2025 1975..2074 2000 2020 2040 2060 1980
* 2050 2000..2099 2000 2020 2040 2060 2080
*
*
* @param pivot pivot year to use when parsing
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTwoDigitYear(int pivot) {
return appendTwoDigitYear(pivot, false);
}
/**
* Instructs the printer to emit a numeric year field which always prints
* two digits. A pivot year is used during parsing to determine the range
* of supported years as (pivot - 50) .. (pivot + 49)
. If
* parse is instructed to be lenient and the digit count is not two, it is
* treated as an absolute year. With lenient parsing, specifying a positive
* or negative sign before the year also makes it absolute.
*
* @param pivot pivot year to use when parsing
* @param lenientParse when true, if digit count is not two, it is treated
* as an absolute year
* @return this DateTimeFormatterBuilder, for chaining
* @since 1.1
*/
public DateTimeFormatterBuilder appendTwoDigitYear(int pivot, boolean lenientParse) {
return append0(new TwoDigitYear(DateTimeFieldType.year(), pivot, lenientParse));
}
/**
* Instructs the printer to emit a numeric weekyear field which always prints
* and parses two digits. A pivot year is used during parsing to determine
* the range of supported years as (pivot - 50) .. (pivot + 49)
.
*
*
* pivot supported range 00 is 20 is 40 is 60 is 80 is
* ---------------------------------------------------------------
* 1950 1900..1999 1900 1920 1940 1960 1980
* 1975 1925..2024 2000 2020 1940 1960 1980
* 2000 1950..2049 2000 2020 2040 1960 1980
* 2025 1975..2074 2000 2020 2040 2060 1980
* 2050 2000..2099 2000 2020 2040 2060 2080
*
*
* @param pivot pivot weekyear to use when parsing
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTwoDigitWeekyear(int pivot) {
return appendTwoDigitWeekyear(pivot, false);
}
/**
* Instructs the printer to emit a numeric weekyear field which always prints
* two digits. A pivot year is used during parsing to determine the range
* of supported years as (pivot - 50) .. (pivot + 49)
. If
* parse is instructed to be lenient and the digit count is not two, it is
* treated as an absolute weekyear. With lenient parsing, specifying a positive
* or negative sign before the weekyear also makes it absolute.
*
* @param pivot pivot weekyear to use when parsing
* @param lenientParse when true, if digit count is not two, it is treated
* as an absolute weekyear
* @return this DateTimeFormatterBuilder, for chaining
* @since 1.1
*/
public DateTimeFormatterBuilder appendTwoDigitWeekyear(int pivot, boolean lenientParse) {
return append0(new TwoDigitYear(DateTimeFieldType.weekyear(), pivot, lenientParse));
}
/**
* Instructs the printer to emit a numeric yearOfEra field.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendYearOfEra(int minDigits, int maxDigits) {
return appendDecimal(DateTimeFieldType.yearOfEra(), minDigits, maxDigits);
}
/**
* Instructs the printer to emit a numeric year of century field.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendYearOfCentury(int minDigits, int maxDigits) {
return appendDecimal(DateTimeFieldType.yearOfCentury(), minDigits, maxDigits);
}
/**
* Instructs the printer to emit a numeric century of era field.
*
* @param minDigits minimum number of digits to print
* @param maxDigits maximum number of digits to parse , or the estimated
* maximum number of digits to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendCenturyOfEra(int minDigits, int maxDigits) {
return appendSignedDecimal(DateTimeFieldType.centuryOfEra(), minDigits, maxDigits);
}
/**
* Instructs the printer to emit a locale-specific AM/PM text, and the
* parser to expect it. The parser is case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendHalfdayOfDayText() {
return appendText(DateTimeFieldType.halfdayOfDay());
}
/**
* Instructs the printer to emit a locale-specific dayOfWeek text. The
* parser will accept a long or short dayOfWeek text, case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendDayOfWeekText() {
return appendText(DateTimeFieldType.dayOfWeek());
}
/**
* Instructs the printer to emit a short locale-specific dayOfWeek
* text. The parser will accept a long or short dayOfWeek text,
* case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendDayOfWeekShortText() {
return appendShortText(DateTimeFieldType.dayOfWeek());
}
/**
* Instructs the printer to emit a short locale-specific monthOfYear
* text. The parser will accept a long or short monthOfYear text,
* case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMonthOfYearText() {
return appendText(DateTimeFieldType.monthOfYear());
}
/**
* Instructs the printer to emit a locale-specific monthOfYear text. The
* parser will accept a long or short monthOfYear text, case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendMonthOfYearShortText() {
return appendShortText(DateTimeFieldType.monthOfYear());
}
/**
* Instructs the printer to emit a locale-specific era text (BC/AD), and
* the parser to expect it. The parser is case-insensitive.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendEraText() {
return appendText(DateTimeFieldType.era());
}
/**
* Instructs the printer to emit a locale-specific time zone name.
* Using this method prevents parsing, because time zone names are not unique.
* See {@link #appendTimeZoneName(Map)}.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTimeZoneName() {
return append0(new TimeZoneName(TimeZoneName.LONG_NAME, null), null);
}
/**
* Instructs the printer to emit a locale-specific time zone name, providing a lookup for parsing.
* Time zone names are not unique, thus the API forces you to supply the lookup.
* The names are searched in the order of the map, thus it is strongly recommended
* to use a {@code LinkedHashMap} or similar.
*
* @param parseLookup the table of names, not null
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTimeZoneName(Map parseLookup) {
TimeZoneName pp = new TimeZoneName(TimeZoneName.LONG_NAME, parseLookup);
return append0(pp, pp);
}
/**
* Instructs the printer to emit a short locale-specific time zone name.
* Using this method prevents parsing, because time zone names are not unique.
* See {@link #appendTimeZoneShortName(Map)}.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTimeZoneShortName() {
return append0(new TimeZoneName(TimeZoneName.SHORT_NAME, null), null);
}
/**
* Instructs the printer to emit a short locale-specific time zone
* name, providing a lookup for parsing.
* Time zone names are not unique, thus the API forces you to supply the lookup.
* The names are searched in the order of the map, thus it is strongly recommended
* to use a {@code LinkedHashMap} or similar.
*
* @param parseLookup the table of names, null to use the {@link DateTimeUtils#getDefaultTimeZoneNames() default names}
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTimeZoneShortName(Map parseLookup) {
TimeZoneName pp = new TimeZoneName(TimeZoneName.SHORT_NAME, parseLookup);
return append0(pp, pp);
}
/**
* Instructs the printer to emit the identifier of the time zone.
* From version 2.0, this field can be parsed.
*
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTimeZoneId() {
return append0(TimeZoneId.INSTANCE, TimeZoneId.INSTANCE);
}
/**
* Instructs the printer to emit text and numbers to display time zone
* offset from UTC. A parser will use the parsed time zone offset to adjust
* the datetime.
*
* If zero offset text is supplied, then it will be printed when the zone is zero.
* During parsing, either the zero offset text, or the offset will be parsed.
*
* @param zeroOffsetText the text to use if time zone offset is zero. If
* null, offset is always shown.
* @param showSeparators if true, prints ':' separator before minute and
* second field and prints '.' separator before fraction field.
* @param minFields minimum number of fields to print, stopping when no
* more precision is required. 1=hours, 2=minutes, 3=seconds, 4=fraction
* @param maxFields maximum number of fields to print
* @return this DateTimeFormatterBuilder, for chaining
*/
public DateTimeFormatterBuilder appendTimeZoneOffset(
String zeroOffsetText, boolean showSeparators,
int minFields, int maxFields) {
return append0(new TimeZoneOffset
(zeroOffsetText, zeroOffsetText, showSeparators, minFields, maxFields));
}
/**
* Instructs the printer to emit text and numbers to display time zone
* offset from UTC. A parser will use the parsed time zone offset to adjust
* the datetime.
*
* If zero offset print text is supplied, then it will be printed when the zone is zero.
* If zero offset parse text is supplied, then either it or the offset will be parsed.
*
* @param zeroOffsetPrintText the text to print if time zone offset is zero. If
* null, offset is always shown.
* @param zeroOffsetParseText the text to optionally parse to indicate that the time
* zone offset is zero. If null, then always use the offset.
* @param showSeparators if true, prints ':' separator before minute and
* second field and prints '.' separator before fraction field.
* @param minFields minimum number of fields to print, stopping when no
* more precision is required. 1=hours, 2=minutes, 3=seconds, 4=fraction
* @param maxFields maximum number of fields to print
* @return this DateTimeFormatterBuilder, for chaining
* @since 2.0
*/
public DateTimeFormatterBuilder appendTimeZoneOffset(
String zeroOffsetPrintText, String zeroOffsetParseText, boolean showSeparators,
int minFields, int maxFields) {
return append0(new TimeZoneOffset
(zeroOffsetPrintText, zeroOffsetParseText, showSeparators, minFields, maxFields));
}
//-----------------------------------------------------------------------
/**
* Calls upon {@link DateTimeFormat} to parse the pattern and append the
* results into this builder.
*
* @param pattern pattern specification
* @throws IllegalArgumentException if the pattern is invalid
* @see DateTimeFormat
*/
public DateTimeFormatterBuilder appendPattern(String pattern) {
DateTimeFormat.appendPatternTo(this, pattern);
return this;
}
//-----------------------------------------------------------------------
private Object getFormatter() {
Object f = iFormatter;
if (f == null) {
if (iElementPairs.size() == 2) {
Object printer = iElementPairs.get(0);
Object parser = iElementPairs.get(1);
if (printer != null) {
if (printer == parser || parser == null) {
f = printer;
}
} else {
f = parser;
}
}
if (f == null) {
f = new Composite(iElementPairs);
}
iFormatter = f;
}
return f;
}
private boolean isPrinter(Object f) {
if (f instanceof InternalPrinter) {
if (f instanceof Composite) {
return ((Composite)f).isPrinter();
}
return true;
}
return false;
}
private boolean isParser(Object f) {
if (f instanceof InternalParser) {
if (f instanceof Composite) {
return ((Composite)f).isParser();
}
return true;
}
return false;
}
private boolean isFormatter(Object f) {
return (isPrinter(f) || isParser(f));
}
static void appendUnknownString(Appendable appendable, int len) throws IOException {
for (int i = len; --i >= 0;) {
appendable.append('\ufffd');
}
}
//-----------------------------------------------------------------------
static class CharacterLiteral
implements InternalPrinter, InternalParser {
private final char iValue;
CharacterLiteral(char value) {
super();
iValue = value;
}
public int estimatePrintedLength() {
return 1;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
appendable.append(iValue);
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
appendable.append(iValue);
}
public int estimateParsedLength() {
return 1;
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
if (position >= text.length()) {
return ~position;
}
char a = text.charAt(position);
char b = iValue;
if (a != b) {
a = Character.toUpperCase(a);
b = Character.toUpperCase(b);
if (a != b) {
a = Character.toLowerCase(a);
b = Character.toLowerCase(b);
if (a != b) {
return ~position;
}
}
}
return position + 1;
}
}
//-----------------------------------------------------------------------
static class StringLiteral
implements InternalPrinter, InternalParser {
private final String iValue;
StringLiteral(String value) {
super();
iValue = value;
}
public int estimatePrintedLength() {
return iValue.length();
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
appendable.append(iValue);
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
appendable.append(iValue);
}
public int estimateParsedLength() {
return iValue.length();
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
if (csStartsWithIgnoreCase(text, position, iValue)) {
return position + iValue.length();
}
return ~position;
}
}
//-----------------------------------------------------------------------
static abstract class NumberFormatter
implements InternalPrinter, InternalParser {
protected final DateTimeFieldType iFieldType;
protected final int iMaxParsedDigits;
protected final boolean iSigned;
NumberFormatter(DateTimeFieldType fieldType,
int maxParsedDigits, boolean signed) {
super();
iFieldType = fieldType;
iMaxParsedDigits = maxParsedDigits;
iSigned = signed;
}
public int estimateParsedLength() {
return iMaxParsedDigits;
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
int limit = Math.min(iMaxParsedDigits, text.length() - position);
boolean negative = false;
boolean positive = false;
int length = 0;
while (length < limit) {
char c = text.charAt(position + length);
if (length == 0 && (c == '-' || c == '+') && iSigned) {
negative = c == '-';
positive = c == '+';
// Next character must be a digit.
if (length + 1 >= limit ||
(c = text.charAt(position + length + 1)) < '0' || c > '9') {
break;
}
length++;
// Expand the limit to disregard the sign character.
limit = Math.min(limit + 1, text.length() - position);
continue;
}
if (c < '0' || c > '9') {
break;
}
length++;
}
if (length == 0) {
return ~position;
}
int value;
if (length >= 9) {
// Since value may exceed integer limits, use stock parser
// which checks for this.
if (positive) {
value = Integer.parseInt(text.subSequence(position + 1, position += length).toString());
} else {
value = Integer.parseInt(text.subSequence(position, position += length).toString());
}
// value = Integer.parseInt(text.subSequence(position, position += length).toString());
} else {
int i = position;
if (negative || positive) {
i++;
}
try {
value = text.charAt(i++) - '0';
} catch (StringIndexOutOfBoundsException e) {
return ~position;
}
position += length;
while (i < position) {
value = ((value << 3) + (value << 1)) + text.charAt(i++) - '0';
}
if (negative) {
value = -value;
}
}
bucket.saveField(iFieldType, value);
return position;
}
}
//-----------------------------------------------------------------------
static class UnpaddedNumber extends NumberFormatter {
protected UnpaddedNumber(DateTimeFieldType fieldType,
int maxParsedDigits, boolean signed)
{
super(fieldType, maxParsedDigits, signed);
}
public int estimatePrintedLength() {
return iMaxParsedDigits;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
try {
DateTimeField field = iFieldType.getField(chrono);
FormatUtils.appendUnpaddedInteger(appendable, field.get(instant));
} catch (RuntimeException e) {
appendable.append('\ufffd');
}
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
if (partial.isSupported(iFieldType)) {
try {
FormatUtils.appendUnpaddedInteger(appendable, partial.get(iFieldType));
} catch (RuntimeException e) {
appendable.append('\ufffd');
}
} else {
appendable.append('\ufffd');
}
}
}
//-----------------------------------------------------------------------
static class PaddedNumber extends NumberFormatter {
protected final int iMinPrintedDigits;
protected PaddedNumber(DateTimeFieldType fieldType, int maxParsedDigits,
boolean signed, int minPrintedDigits)
{
super(fieldType, maxParsedDigits, signed);
iMinPrintedDigits = minPrintedDigits;
}
public int estimatePrintedLength() {
return iMaxParsedDigits;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
try {
DateTimeField field = iFieldType.getField(chrono);
FormatUtils.appendPaddedInteger(appendable, field.get(instant), iMinPrintedDigits);
} catch (RuntimeException e) {
appendUnknownString(appendable, iMinPrintedDigits);
}
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
if (partial.isSupported(iFieldType)) {
try {
FormatUtils.appendPaddedInteger(appendable, partial.get(iFieldType), iMinPrintedDigits);
} catch (RuntimeException e) {
appendUnknownString(appendable, iMinPrintedDigits);
}
} else {
appendUnknownString(appendable, iMinPrintedDigits);
}
}
}
//-----------------------------------------------------------------------
static class FixedNumber extends PaddedNumber {
protected FixedNumber(DateTimeFieldType fieldType, int numDigits, boolean signed) {
super(fieldType, numDigits, signed, numDigits);
}
@Override
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
int newPos = super.parseInto(bucket, text, position);
if (newPos < 0) {
return newPos;
}
int expectedPos = position + iMaxParsedDigits;
if (newPos != expectedPos) {
if (iSigned) {
char c = text.charAt(position);
if (c == '-' || c == '+') {
expectedPos++;
}
}
if (newPos > expectedPos) {
// The failure is at the position of the first extra digit.
return ~(expectedPos + 1);
} else if (newPos < expectedPos) {
// The failure is at the position where the next digit should be.
return ~newPos;
}
}
return newPos;
}
}
//-----------------------------------------------------------------------
static class TwoDigitYear
implements InternalPrinter, InternalParser {
/** The field to print/parse. */
private final DateTimeFieldType iType;
/** The pivot year. */
private final int iPivot;
private final boolean iLenientParse;
TwoDigitYear(DateTimeFieldType type, int pivot, boolean lenientParse) {
super();
iType = type;
iPivot = pivot;
iLenientParse = lenientParse;
}
public int estimateParsedLength() {
return iLenientParse ? 4 : 2;
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
int limit = text.length() - position;
if (!iLenientParse) {
limit = Math.min(2, limit);
if (limit < 2) {
return ~position;
}
} else {
boolean hasSignChar = false;
boolean negative = false;
int length = 0;
while (length < limit) {
char c = text.charAt(position + length);
if (length == 0 && (c == '-' || c == '+')) {
hasSignChar = true;
negative = c == '-';
if (negative) {
length++;
} else {
// Skip the '+' for parseInt to succeed.
position++;
limit--;
}
continue;
}
if (c < '0' || c > '9') {
break;
}
length++;
}
if (length == 0) {
return ~position;
}
if (hasSignChar || length != 2) {
int value;
if (length >= 9) {
// Since value may exceed integer limits, use stock
// parser which checks for this.
value = Integer.parseInt(text.subSequence(position, position += length).toString());
} else {
int i = position;
if (negative) {
i++;
}
try {
value = text.charAt(i++) - '0';
} catch (StringIndexOutOfBoundsException e) {
return ~position;
}
position += length;
while (i < position) {
value = ((value << 3) + (value << 1)) + text.charAt(i++) - '0';
}
if (negative) {
value = -value;
}
}
bucket.saveField(iType, value);
return position;
}
}
int year;
char c = text.charAt(position);
if (c < '0' || c > '9') {
return ~position;
}
year = c - '0';
c = text.charAt(position + 1);
if (c < '0' || c > '9') {
return ~position;
}
year = ((year << 3) + (year << 1)) + c - '0';
int pivot = iPivot;
// If the bucket pivot year is non-null, use that when parsing
if (bucket.getPivotYear() != null) {
pivot = bucket.getPivotYear().intValue();
}
int low = pivot - 50;
int t;
if (low >= 0) {
t = low % 100;
} else {
t = 99 + ((low + 1) % 100);
}
year += low + ((year < t) ? 100 : 0) - t;
bucket.saveField(iType, year);
return position + 2;
}
public int estimatePrintedLength() {
return 2;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
int year = getTwoDigitYear(instant, chrono);
if (year < 0) {
appendable.append('\ufffd');
appendable.append('\ufffd');
} else {
FormatUtils.appendPaddedInteger(appendable, year, 2);
}
}
private int getTwoDigitYear(long instant, Chronology chrono) {
try {
int year = iType.getField(chrono).get(instant);
if (year < 0) {
year = -year;
}
return year % 100;
} catch (RuntimeException e) {
return -1;
}
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
int year = getTwoDigitYear(partial);
if (year < 0) {
appendable.append('\ufffd');
appendable.append('\ufffd');
} else {
FormatUtils.appendPaddedInteger(appendable, year, 2);
}
}
private int getTwoDigitYear(ReadablePartial partial) {
if (partial.isSupported(iType)) {
try {
int year = partial.get(iType);
if (year < 0) {
year = -year;
}
return year % 100;
} catch (RuntimeException e) {}
}
return -1;
}
}
//-----------------------------------------------------------------------
static class TextField
implements InternalPrinter, InternalParser {
private static Map> cParseCache =
new ConcurrentHashMap>();
private final DateTimeFieldType iFieldType;
private final boolean iShort;
TextField(DateTimeFieldType fieldType, boolean isShort) {
super();
iFieldType = fieldType;
iShort = isShort;
}
public int estimatePrintedLength() {
return iShort ? 6 : 20;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
try {
appendable.append(print(instant, chrono, locale));
} catch (RuntimeException e) {
appendable.append('\ufffd');
}
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
try {
appendable.append(print(partial, locale));
} catch (RuntimeException e) {
appendable.append('\ufffd');
}
}
private String print(long instant, Chronology chrono, Locale locale) {
DateTimeField field = iFieldType.getField(chrono);
if (iShort) {
return field.getAsShortText(instant, locale);
} else {
return field.getAsText(instant, locale);
}
}
private String print(ReadablePartial partial, Locale locale) {
if (partial.isSupported(iFieldType)) {
DateTimeField field = iFieldType.getField(partial.getChronology());
if (iShort) {
return field.getAsShortText(partial, locale);
} else {
return field.getAsText(partial, locale);
}
} else {
return "\ufffd";
}
}
public int estimateParsedLength() {
return estimatePrintedLength();
}
@SuppressWarnings("unchecked")
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
Locale locale = bucket.getLocale();
// handle languages which might have non ASCII A-Z or punctuation
// bug 1788282
Map validValues = null;
int maxLength = 0;
Map innerMap = cParseCache.get(locale);
if (innerMap == null) {
innerMap = new ConcurrentHashMap();
cParseCache.put(locale, innerMap);
}
Object[] array = innerMap.get(iFieldType);
if (array == null) {
validValues = new ConcurrentHashMap(32); // use map as no concurrent Set
MutableDateTime dt = new MutableDateTime(0L, DateTimeZone.UTC);
Property property = dt.property(iFieldType);
int min = property.getMinimumValueOverall();
int max = property.getMaximumValueOverall();
if (max - min > 32) { // protect against invalid fields
return ~position;
}
maxLength = property.getMaximumTextLength(locale);
for (int i = min; i <= max; i++) {
property.set(i);
validValues.put(property.getAsShortText(locale), Boolean.TRUE);
validValues.put(property.getAsShortText(locale).toLowerCase(locale), Boolean.TRUE);
validValues.put(property.getAsShortText(locale).toUpperCase(locale), Boolean.TRUE);
validValues.put(property.getAsText(locale), Boolean.TRUE);
validValues.put(property.getAsText(locale).toLowerCase(locale), Boolean.TRUE);
validValues.put(property.getAsText(locale).toUpperCase(locale), Boolean.TRUE);
}
if ("en".equals(locale.getLanguage()) && iFieldType == DateTimeFieldType.era()) {
// hack to support for parsing "BCE" and "CE" if the language is English
validValues.put("BCE", Boolean.TRUE);
validValues.put("bce", Boolean.TRUE);
validValues.put("CE", Boolean.TRUE);
validValues.put("ce", Boolean.TRUE);
maxLength = 3;
}
array = new Object[] {validValues, Integer.valueOf(maxLength)};
innerMap.put(iFieldType, array);
} else {
validValues = (Map) array[0];
maxLength = ((Integer) array[1]).intValue();
}
// match the longest string first using our knowledge of the max length
int limit = Math.min(text.length(), position + maxLength);
for (int i = limit; i > position; i--) {
String match = text.subSequence(position, i).toString();
if (validValues.containsKey(match)) {
bucket.saveField(iFieldType, match, locale);
return i;
}
}
return ~position;
}
}
//-----------------------------------------------------------------------
static class Fraction
implements InternalPrinter, InternalParser {
private final DateTimeFieldType iFieldType;
protected int iMinDigits;
protected int iMaxDigits;
protected Fraction(DateTimeFieldType fieldType, int minDigits, int maxDigits) {
super();
iFieldType = fieldType;
// Limit the precision requirements.
if (maxDigits > 18) {
maxDigits = 18;
}
iMinDigits = minDigits;
iMaxDigits = maxDigits;
}
public int estimatePrintedLength() {
return iMaxDigits;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
printTo(appendable, instant, chrono);
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
// removed check whether field is supported, as input field is typically
// secondOfDay which is unsupported by TimeOfDay
long millis = partial.getChronology().set(partial, 0L);
printTo(appendable, millis, partial.getChronology());
}
protected void printTo(Appendable appendable, long instant, Chronology chrono)
throws IOException
{
DateTimeField field = iFieldType.getField(chrono);
int minDigits = iMinDigits;
long fraction;
try {
fraction = field.remainder(instant);
} catch (RuntimeException e) {
appendUnknownString(appendable, minDigits);
return;
}
if (fraction == 0) {
while (--minDigits >= 0) {
appendable.append('0');
}
return;
}
String str;
long[] fractionData = getFractionData(fraction, field);
long scaled = fractionData[0];
int maxDigits = (int) fractionData[1];
if ((scaled & 0x7fffffff) == scaled) {
str = Integer.toString((int) scaled);
} else {
str = Long.toString(scaled);
}
int length = str.length();
int digits = maxDigits;
while (length < digits) {
appendable.append('0');
minDigits--;
digits--;
}
if (minDigits < digits) {
// Chop off as many trailing zero digits as necessary.
while (minDigits < digits) {
if (length <= 1 || str.charAt(length - 1) != '0') {
break;
}
digits--;
length--;
}
if (length < str.length()) {
for (int i=0; i '9') {
break;
}
length++;
long nn = n / 10;
value += (c - '0') * nn;
n = nn;
}
value /= 10;
if (length == 0) {
return ~position;
}
if (value > Integer.MAX_VALUE) {
return ~position;
}
DateTimeField parseField = new PreciseDateTimeField(
DateTimeFieldType.millisOfSecond(),
MillisDurationField.INSTANCE,
field.getDurationField());
bucket.saveField(parseField, (int) value);
return position + length;
}
}
//-----------------------------------------------------------------------
static class TimeZoneOffset
implements InternalPrinter, InternalParser {
private final String iZeroOffsetPrintText;
private final String iZeroOffsetParseText;
private final boolean iShowSeparators;
private final int iMinFields;
private final int iMaxFields;
TimeZoneOffset(String zeroOffsetPrintText, String zeroOffsetParseText,
boolean showSeparators,
int minFields, int maxFields)
{
super();
iZeroOffsetPrintText = zeroOffsetPrintText;
iZeroOffsetParseText = zeroOffsetParseText;
iShowSeparators = showSeparators;
if (minFields <= 0 || maxFields < minFields) {
throw new IllegalArgumentException();
}
if (minFields > 4) {
minFields = 4;
maxFields = 4;
}
iMinFields = minFields;
iMaxFields = maxFields;
}
public int estimatePrintedLength() {
int est = 1 + iMinFields << 1;
if (iShowSeparators) {
est += iMinFields - 1;
}
if (iZeroOffsetPrintText != null && iZeroOffsetPrintText.length() > est) {
est = iZeroOffsetPrintText.length();
}
return est;
}
public void printTo(
Appendable buf, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
if (displayZone == null) {
return; // no zone
}
if (displayOffset == 0 && iZeroOffsetPrintText != null) {
buf.append(iZeroOffsetPrintText);
return;
}
if (displayOffset >= 0) {
buf.append('+');
} else {
buf.append('-');
displayOffset = -displayOffset;
}
int hours = displayOffset / DateTimeConstants.MILLIS_PER_HOUR;
FormatUtils.appendPaddedInteger(buf, hours, 2);
if (iMaxFields == 1) {
return;
}
displayOffset -= hours * (int)DateTimeConstants.MILLIS_PER_HOUR;
if (displayOffset == 0 && iMinFields <= 1) {
return;
}
int minutes = displayOffset / DateTimeConstants.MILLIS_PER_MINUTE;
if (iShowSeparators) {
buf.append(':');
}
FormatUtils.appendPaddedInteger(buf, minutes, 2);
if (iMaxFields == 2) {
return;
}
displayOffset -= minutes * DateTimeConstants.MILLIS_PER_MINUTE;
if (displayOffset == 0 && iMinFields <= 2) {
return;
}
int seconds = displayOffset / DateTimeConstants.MILLIS_PER_SECOND;
if (iShowSeparators) {
buf.append(':');
}
FormatUtils.appendPaddedInteger(buf, seconds, 2);
if (iMaxFields == 3) {
return;
}
displayOffset -= seconds * DateTimeConstants.MILLIS_PER_SECOND;
if (displayOffset == 0 && iMinFields <= 3) {
return;
}
if (iShowSeparators) {
buf.append('.');
}
FormatUtils.appendPaddedInteger(buf, displayOffset, 3);
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
// no zone info
}
public int estimateParsedLength() {
return estimatePrintedLength();
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
int limit = text.length() - position;
zeroOffset:
if (iZeroOffsetParseText != null) {
if (iZeroOffsetParseText.length() == 0) {
// Peek ahead, looking for sign character.
if (limit > 0) {
char c = text.charAt(position);
if (c == '-' || c == '+') {
break zeroOffset;
}
}
bucket.setOffset(Integer.valueOf(0));
return position;
}
if (csStartsWithIgnoreCase(text, position, iZeroOffsetParseText)) {
bucket.setOffset(Integer.valueOf(0));
return position + iZeroOffsetParseText.length();
}
}
// Format to expect is sign character followed by at least one digit.
if (limit <= 1) {
return ~position;
}
boolean negative;
char c = text.charAt(position);
if (c == '-') {
negative = true;
} else if (c == '+') {
negative = false;
} else {
return ~position;
}
limit--;
position++;
// Format following sign is one of:
//
// hh
// hhmm
// hhmmss
// hhmmssSSS
// hh:mm
// hh:mm:ss
// hh:mm:ss.SSS
// First parse hours.
if (digitCount(text, position, 2) < 2) {
// Need two digits for hour.
return ~position;
}
int offset;
int hours = FormatUtils.parseTwoDigits(text, position);
if (hours > 23) {
return ~position;
}
offset = hours * DateTimeConstants.MILLIS_PER_HOUR;
limit -= 2;
position += 2;
parse: {
// Need to decide now if separators are expected or parsing
// stops at hour field.
if (limit <= 0) {
break parse;
}
boolean expectSeparators;
c = text.charAt(position);
if (c == ':') {
expectSeparators = true;
limit--;
position++;
} else if (c >= '0' && c <= '9') {
expectSeparators = false;
} else {
break parse;
}
// Proceed to parse minutes.
int count = digitCount(text, position, 2);
if (count == 0 && !expectSeparators) {
break parse;
} else if (count < 2) {
// Need two digits for minute.
return ~position;
}
int minutes = FormatUtils.parseTwoDigits(text, position);
if (minutes > 59) {
return ~position;
}
offset += minutes * DateTimeConstants.MILLIS_PER_MINUTE;
limit -= 2;
position += 2;
// Proceed to parse seconds.
if (limit <= 0) {
break parse;
}
if (expectSeparators) {
if (text.charAt(position) != ':') {
break parse;
}
limit--;
position++;
}
count = digitCount(text, position, 2);
if (count == 0 && !expectSeparators) {
break parse;
} else if (count < 2) {
// Need two digits for second.
return ~position;
}
int seconds = FormatUtils.parseTwoDigits(text, position);
if (seconds > 59) {
return ~position;
}
offset += seconds * DateTimeConstants.MILLIS_PER_SECOND;
limit -= 2;
position += 2;
// Proceed to parse fraction of second.
if (limit <= 0) {
break parse;
}
if (expectSeparators) {
if (text.charAt(position) != '.' && text.charAt(position) != ',') {
break parse;
}
limit--;
position++;
}
count = digitCount(text, position, 3);
if (count == 0 && !expectSeparators) {
break parse;
} else if (count < 1) {
// Need at least one digit for fraction of second.
return ~position;
}
offset += (text.charAt(position++) - '0') * 100;
if (count > 1) {
offset += (text.charAt(position++) - '0') * 10;
if (count > 2) {
offset += text.charAt(position++) - '0';
}
}
}
bucket.setOffset(Integer.valueOf(negative ? -offset : offset));
return position;
}
/**
* Returns actual amount of digits to parse, but no more than original
* 'amount' parameter.
*/
private int digitCount(CharSequence text, int position, int amount) {
int limit = Math.min(text.length() - position, amount);
amount = 0;
for (; limit > 0; limit--) {
char c = text.charAt(position + amount);
if (c < '0' || c > '9') {
break;
}
amount++;
}
return amount;
}
}
//-----------------------------------------------------------------------
static class TimeZoneName
implements InternalPrinter, InternalParser {
static final int LONG_NAME = 0;
static final int SHORT_NAME = 1;
private final Map iParseLookup;
private final int iType;
TimeZoneName(int type, Map parseLookup) {
super();
iType = type;
iParseLookup = parseLookup;
}
public int estimatePrintedLength() {
return (iType == SHORT_NAME ? 4 : 20);
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
appendable.append(print(instant - displayOffset, displayZone, locale));
}
private String print(long instant, DateTimeZone displayZone, Locale locale) {
if (displayZone == null) {
return ""; // no zone
}
switch (iType) {
case LONG_NAME:
return displayZone.getName(instant, locale);
case SHORT_NAME:
return displayZone.getShortName(instant, locale);
}
return "";
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
// no zone info
}
public int estimateParsedLength() {
return (iType == SHORT_NAME ? 4 : 20);
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
Map parseLookup = iParseLookup;
parseLookup = (parseLookup != null ? parseLookup : DateTimeUtils.getDefaultTimeZoneNames());
String matched = null;
for (String name : parseLookup.keySet()) {
if (csStartsWith(text, position, name)) {
if (matched == null || name.length() > matched.length()) {
matched = name;
}
}
}
if (matched != null) {
bucket.setZone(parseLookup.get(matched));
return position + matched.length();
}
return ~position;
}
}
//-----------------------------------------------------------------------
static enum TimeZoneId
implements InternalPrinter, InternalParser {
INSTANCE;
private static final List ALL_IDS;
// groups are "Europe/A", "Europe/B", "Europe/C", etc
// group of "" is for zones that do not have a "/" in the name
private static final Map> GROUPED_IDS;
private static final List BASE_GROUPED_IDS = new ArrayList();
static final int MAX_LENGTH;
static final int MAX_PREFIX_LENGTH;
static {
ALL_IDS = new ArrayList(DateTimeZone.getAvailableIDs());
Collections.sort(ALL_IDS);
GROUPED_IDS = new HashMap>();
int max = 0;
int maxPrefix = 0;
for (String id : ALL_IDS) {
int pos = id.indexOf('/');
if (pos >= 0) {
if (pos < id.length()) {
pos++;
}
maxPrefix = Math.max(maxPrefix, pos);
String prefix = id.substring(0, pos + 1);
String suffix = id.substring(pos);
if (!GROUPED_IDS.containsKey(prefix)) {
GROUPED_IDS.put(prefix, new ArrayList());
}
GROUPED_IDS.get(prefix).add(suffix);
} else {
BASE_GROUPED_IDS.add(id);
}
max = Math.max(max, id.length());
}
MAX_LENGTH = max;
MAX_PREFIX_LENGTH = maxPrefix;
}
public int estimatePrintedLength() {
return MAX_LENGTH;
}
public void printTo(
Appendable appendable, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
appendable.append(displayZone != null ? displayZone.getID() : "");
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
// no zone info
}
public int estimateParsedLength() {
return MAX_LENGTH;
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
// select the base set of identifiers that do not have a slash
List suffixSet = BASE_GROUPED_IDS;
// hunt for a slash only as far as the max prefix length
int textLen = text.length();
int matchLen = Math.min(textLen, position + MAX_PREFIX_LENGTH);
int pos = position;
String prefix = "";
for (int i = pos; i < matchLen; i++) {
if (text.charAt(i) == '/') {
// when a slash is found, determine the prefix, such as "Europe/A" and lookup to get suffixes
prefix = text.subSequence(pos, i + 1).toString();
pos += prefix.length();
String prefixLookup = prefix;
if (i < textLen) {
prefixLookup += text.charAt(i + 1);
}
suffixSet = GROUPED_IDS.get(prefixLookup);
if (suffixSet == null) {
return ~position;
}
break;
}
}
// search all suffixes, hopefully a relatively small number due to prefix search
String best = null;
for (int i = 0; i < suffixSet.size(); i++) {
String suffix = suffixSet.get(i);
if (csStartsWith(text, pos, suffix)) {
if (best == null || suffix.length() > best.length()) {
best = suffix;
}
}
}
// if found then store, else fail
if (best != null) {
bucket.setZone(DateTimeZone.forID(prefix + best));
return pos + best.length();
}
return ~position;
}
}
//-----------------------------------------------------------------------
static class Composite
implements InternalPrinter, InternalParser {
private final InternalPrinter[] iPrinters;
private final InternalParser[] iParsers;
private final int iPrintedLengthEstimate;
private final int iParsedLengthEstimate;
Composite(List elementPairs) {
super();
List printerList = new ArrayList();
List parserList = new ArrayList();
decompose(elementPairs, printerList, parserList);
if (printerList.contains(null) || printerList.isEmpty()) {
iPrinters = null;
iPrintedLengthEstimate = 0;
} else {
int size = printerList.size();
iPrinters = new InternalPrinter[size];
int printEst = 0;
for (int i=0; i= 0; i++) {
position = elements[i].parseInto(bucket, text, position);
}
return position;
}
boolean isPrinter() {
return iPrinters != null;
}
boolean isParser() {
return iParsers != null;
}
/**
* Processes the element pairs, putting results into the given printer
* and parser lists.
*/
private void decompose(List elementPairs, List printerList, List parserList) {
int size = elementPairs.size();
for (int i=0; i list, Object[] array) {
if (array != null) {
for (int i=0; i=0 ;) {
InternalParser parser = parsers[i];
if (parser != null) {
int len = parser.estimateParsedLength();
if (len > est) {
est = len;
}
}
}
iParsedLengthEstimate = est;
}
public int estimateParsedLength() {
return iParsedLengthEstimate;
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
InternalParser[] parsers = iParsers;
int length = parsers.length;
final Object originalState = bucket.saveState();
boolean isOptional = false;
int bestValidPos = position;
Object bestValidState = null;
int bestInvalidPos = position;
for (int i=0; i= position) {
if (parsePos > bestValidPos) {
if (parsePos >= text.length() ||
(i + 1) >= length || parsers[i + 1] == null) {
// Completely parsed text or no more parsers to
// check. Skip the rest.
return parsePos;
}
bestValidPos = parsePos;
bestValidState = bucket.saveState();
}
} else {
if (parsePos < 0) {
parsePos = ~parsePos;
if (parsePos > bestInvalidPos) {
bestInvalidPos = parsePos;
}
}
}
bucket.restoreState(originalState);
}
if (bestValidPos > position || (bestValidPos == position && isOptional)) {
// Restore the state to the best valid parse.
if (bestValidState != null) {
bucket.restoreState(bestValidState);
}
return bestValidPos;
}
return ~bestInvalidPos;
}
}
static boolean csStartsWith(CharSequence text, int position, String search) {
int searchLen = search.length();
if ((text.length() - position) < searchLen) {
return false;
}
for (int i = 0; i < searchLen; i++) {
if (text.charAt(position + i) != search.charAt(i)) {
return false;
}
}
return true;
}
static boolean csStartsWithIgnoreCase(CharSequence text, int position, String search) {
int searchLen = search.length();
if ((text.length() - position) < searchLen) {
return false;
}
for (int i = 0; i < searchLen; i++) {
char ch1 = text.charAt(position + i);
char ch2 = search.charAt(i);
if (ch1 != ch2) {
char u1 = Character.toUpperCase(ch1);
char u2 = Character.toUpperCase(ch2);
if (u1 != u2 && Character.toLowerCase(u1) != Character.toLowerCase(u2)) {
return false;
}
}
}
return true;
}
}