diff options
Diffstat (limited to 'src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java')
-rw-r--r-- | src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java | 1573 |
1 files changed, 1573 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java new file mode 100644 index 000000000..d86b25e32 --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java @@ -0,0 +1,1573 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.lang3.time; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.FieldPosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.LocaleUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; + +/** + * FastDatePrinter is a fast and thread-safe version of + * {@link java.text.SimpleDateFormat}. + * + * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} + * or another variation of the factory methods of {@link FastDateFormat}.</p> + * + * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p> + * <code> + * private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd"); + * </code> + * + * <p>This class can be used as a direct replacement to + * {@link SimpleDateFormat} in most formatting situations. + * This class is especially useful in multi-threaded server environments. + * {@link SimpleDateFormat} is not thread-safe in any JDK version, + * nor will it be as Sun have closed the bug/RFE. + * </p> + * + * <p>Only formatting is supported by this class, but all patterns are compatible with + * SimpleDateFormat (except time zones and some year patterns - see below).</p> + * + * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent + * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}). + * This pattern letter can be used here (on all JDK versions).</p> + * + * <p>In addition, the pattern {@code 'ZZ'} has been made to represent + * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}). + * This introduces a minor incompatibility with Java 1.4, but at a gain of + * useful functionality.</p> + * + * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}. + * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using + * one of the {@code 'X'} formats is recommended. + * + * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of + * pattern letters is 2, the year is truncated to 2 digits; otherwise it is + * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or + * 'YYY' will be formatted as '2003', while it was '03' in former Java + * versions. FastDatePrinter implements the behavior of Java 7.</p> + * + * @since 3.2 + * @see FastDateParser + */ +public class FastDatePrinter implements DatePrinter, Serializable { + // A lot of the speed in this class comes from caching, but some comes + // from the special int to StringBuffer conversion. + // + // The following produces a padded 2-digit number: + // buffer.append((char)(value / 10 + '0')); + // buffer.append((char)(value % 10 + '0')); + // + // Note that the fastest append to StringBuffer is a single char (used here). + // Note that Integer.toString() is not called, the conversion is simply + // taking the value and adding (mathematically) the ASCII value for '0'. + // So, don't change this code! It works and is very fast. + + /** Empty array. */ + private static final Rule[] EMPTY_RULE_ARRAY = {}; + + /** + * Required for serialization support. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 1L; + + /** + * FULL locale dependent date or time style. + */ + public static final int FULL = DateFormat.FULL; + /** + * LONG locale dependent date or time style. + */ + public static final int LONG = DateFormat.LONG; + /** + * MEDIUM locale dependent date or time style. + */ + public static final int MEDIUM = DateFormat.MEDIUM; + /** + * SHORT locale dependent date or time style. + */ + public static final int SHORT = DateFormat.SHORT; + + /** + * The pattern. + */ + private final String pattern; + /** + * The time zone. + */ + private final TimeZone timeZone; + /** + * The locale. + */ + private final Locale locale; + /** + * The parsed rules. + */ + private transient Rule[] rules; + /** + * The estimated maximum length. + */ + private transient int maxLengthEstimate; + + // Constructor + /** + * Constructs a new FastDatePrinter. + * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the + * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance. + * + * @param pattern {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone non-null time zone to use + * @param locale non-null locale to use + * @throws NullPointerException if pattern, timeZone, or locale is null. + */ + protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { + this.pattern = pattern; + this.timeZone = timeZone; + this.locale = LocaleUtils.toLocale(locale); + init(); + } + + /** + * Initializes the instance for first use. + */ + private void init() { + final List<Rule> rulesList = parsePattern(); + rules = rulesList.toArray(EMPTY_RULE_ARRAY); + + int len = 0; + for (int i = rules.length; --i >= 0;) { + len += rules[i].estimateLength(); + } + + maxLengthEstimate = len; + } + + // Parse the pattern + /** + * Returns a list of Rules given a pattern. + * + * @return a {@link List} of Rule objects + * @throws IllegalArgumentException if pattern is invalid + */ + protected List<Rule> parsePattern() { + final DateFormatSymbols symbols = new DateFormatSymbols(locale); + final List<Rule> rules = new ArrayList<>(); + + final String[] ERAs = symbols.getEras(); + final String[] months = symbols.getMonths(); + final String[] shortMonths = symbols.getShortMonths(); + final String[] weekdays = symbols.getWeekdays(); + final String[] shortWeekdays = symbols.getShortWeekdays(); + final String[] AmPmStrings = symbols.getAmPmStrings(); + + final int length = pattern.length(); + final int[] indexRef = new int[1]; + + for (int i = 0; i < length; i++) { + indexRef[0] = i; + final String token = parseToken(pattern, indexRef); + i = indexRef[0]; + + final int tokenLen = token.length(); + if (tokenLen == 0) { + break; + } + + Rule rule; + final char c = token.charAt(0); + + switch (c) { + case 'G': // era designator (text) + rule = new TextField(Calendar.ERA, ERAs); + break; + case 'y': // year (number) + case 'Y': // week year + if (tokenLen == 2) { + rule = TwoDigitYearField.INSTANCE; + } else { + rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4)); + } + if (c == 'Y') { + rule = new WeekYear((NumberRule) rule); + } + break; + case 'M': // month in year (text and number) + if (tokenLen >= 4) { + rule = new TextField(Calendar.MONTH, months); + } else if (tokenLen == 3) { + rule = new TextField(Calendar.MONTH, shortMonths); + } else if (tokenLen == 2) { + rule = TwoDigitMonthField.INSTANCE; + } else { + rule = UnpaddedMonthField.INSTANCE; + } + break; + case 'L': // month in year (text and number) + if (tokenLen >= 4) { + rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneLongMonthNames()); + } else if (tokenLen == 3) { + rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneShortMonthNames()); + } else if (tokenLen == 2) { + rule = TwoDigitMonthField.INSTANCE; + } else { + rule = UnpaddedMonthField.INSTANCE; + } + break; + case 'd': // day in month (number) + rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); + break; + case 'h': // hour in am/pm (number, 1..12) + rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); + break; + case 'H': // hour in day (number, 0..23) + rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); + break; + case 'm': // minute in hour (number) + rule = selectNumberRule(Calendar.MINUTE, tokenLen); + break; + case 's': // second in minute (number) + rule = selectNumberRule(Calendar.SECOND, tokenLen); + break; + case 'S': // millisecond (number) + rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); + break; + case 'E': // day in week (text) + rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); + break; + case 'u': // day in week (number) + rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen)); + break; + case 'D': // day in year (number) + rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); + break; + case 'F': // day of week in month (number) + rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); + break; + case 'w': // week in year (number) + rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); + break; + case 'W': // week in month (number) + rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); + break; + case 'a': // am/pm marker (text) + rule = new TextField(Calendar.AM_PM, AmPmStrings); + break; + case 'k': // hour in day (1..24) + rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); + break; + case 'K': // hour in am/pm (0..11) + rule = selectNumberRule(Calendar.HOUR, tokenLen); + break; + case 'X': // ISO 8601 + rule = Iso8601_Rule.getRule(tokenLen); + break; + case 'z': // time zone (text) + if (tokenLen >= 4) { + rule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG); + } else { + rule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT); + } + break; + case 'Z': // time zone (value) + if (tokenLen == 1) { + rule = TimeZoneNumberRule.INSTANCE_NO_COLON; + } else if (tokenLen == 2) { + rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; + } else { + rule = TimeZoneNumberRule.INSTANCE_COLON; + } + break; + case '\'': // literal text + final String sub = token.substring(1); + if (sub.length() == 1) { + rule = new CharacterLiteral(sub.charAt(0)); + } else { + rule = new StringLiteral(sub); + } + break; + default: + throw new IllegalArgumentException("Illegal pattern component: " + token); + } + + rules.add(rule); + } + + return rules; + } + + /** + * Performs the parsing of tokens. + * + * @param pattern the pattern + * @param indexRef index references + * @return parsed token + */ + protected String parseToken(final String pattern, final int[] indexRef) { + final StringBuilder buf = new StringBuilder(); + + int i = indexRef[0]; + final int length = pattern.length(); + + char c = pattern.charAt(i); + if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { + // Scan a run of the same character, which indicates a time + // pattern. + buf.append(c); + + while (i + 1 < length) { + final char peek = pattern.charAt(i + 1); + if (peek != c) { + break; + } + buf.append(c); + i++; + } + } else { + // This will identify token as text. + buf.append('\''); + + boolean inLiteral = false; + + for (; i < length; i++) { + c = pattern.charAt(i); + + if (c == '\'') { + if (i + 1 < length && pattern.charAt(i + 1) == '\'') { + // '' is treated as escaped ' + i++; + buf.append(c); + } else { + inLiteral = !inLiteral; + } + } else if (!inLiteral && + (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { + i--; + break; + } else { + buf.append(c); + } + } + } + + indexRef[0] = i; + return buf.toString(); + } + + /** + * Gets an appropriate rule for the padding required. + * + * @param field the field to get a rule for + * @param padding the padding required + * @return a new rule with the correct padding + */ + protected NumberRule selectNumberRule(final int field, final int padding) { + switch (padding) { + case 1: + return new UnpaddedNumberField(field); + case 2: + return new TwoDigitNumberField(field); + default: + return new PaddedNumberField(field, padding); + } + } + + // Format methods + /** + * Formats a {@link Date}, {@link Calendar} or + * {@link Long} (milliseconds) object. + * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}. + * @param obj the object to format + * @param toAppendTo the buffer to append to + * @param pos the position - ignored + * @return the buffer passed in + */ + @Deprecated + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + if (obj instanceof Date) { + return format((Date) obj, toAppendTo); + } + if (obj instanceof Calendar) { + return format((Calendar) obj, toAppendTo); + } + if (obj instanceof Long) { + return format(((Long) obj).longValue(), toAppendTo); + } + throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>")); + } + + /** + * Formats a {@link Date}, {@link Calendar} or + * {@link Long} (milliseconds) object. + * @since 3.5 + * @param obj the object to format + * @return The formatted value. + */ + String format(final Object obj) { + if (obj instanceof Date) { + return format((Date) obj); + } + if (obj instanceof Calendar) { + return format((Calendar) obj); + } + if (obj instanceof Long) { + return format(((Long) obj).longValue()); + } + throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>")); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(long) + */ + @Override + public String format(final long millis) { + final Calendar c = newCalendar(); + c.setTimeInMillis(millis); + return applyRulesToString(c); + } + + /** + * Creates a String representation of the given Calendar by applying the rules of this printer to it. + * @param c the Calendar to apply the rules to. + * @return a String representation of the given Calendar. + */ + private String applyRulesToString(final Calendar c) { + return applyRules(c, new StringBuilder(maxLengthEstimate)).toString(); + } + + /** + * Creates a new Calendar instance. + * @return a new Calendar instance. + */ + private Calendar newCalendar() { + return Calendar.getInstance(timeZone, locale); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date) + */ + @Override + public String format(final Date date) { + final Calendar c = newCalendar(); + c.setTime(date); + return applyRulesToString(c); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar) + */ + @Override + public String format(final Calendar calendar) { + return format(calendar, new StringBuilder(maxLengthEstimate)).toString(); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(long, StringBuffer) + */ + @Override + public StringBuffer format(final long millis, final StringBuffer buf) { + final Calendar c = newCalendar(); + c.setTimeInMillis(millis); + return (StringBuffer) applyRules(c, (Appendable) buf); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, StringBuffer) + */ + @Override + public StringBuffer format(final Date date, final StringBuffer buf) { + final Calendar c = newCalendar(); + c.setTime(date); + return (StringBuffer) applyRules(c, (Appendable) buf); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, StringBuffer) + */ + @Override + public StringBuffer format(final Calendar calendar, final StringBuffer buf) { + // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored + return format(calendar.getTime(), buf); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(long, Appendable) + */ + @Override + public <B extends Appendable> B format(final long millis, final B buf) { + final Calendar c = newCalendar(); + c.setTimeInMillis(millis); + return applyRules(c, buf); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, Appendable) + */ + @Override + public <B extends Appendable> B format(final Date date, final B buf) { + final Calendar c = newCalendar(); + c.setTime(date); + return applyRules(c, buf); + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, Appendable) + */ + @Override + public <B extends Appendable> B format(Calendar calendar, final B buf) { + // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored + if (!calendar.getTimeZone().equals(timeZone)) { + calendar = (Calendar) calendar.clone(); + calendar.setTimeZone(timeZone); + } + return applyRules(calendar, buf); + } + + /** + * Performs the formatting by applying the rules to the + * specified calendar. + * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @return the specified string buffer + * + * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)} + */ + @Deprecated + protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) { + return (StringBuffer) applyRules(calendar, (Appendable) buf); + } + + /** + * Performs the formatting by applying the rules to the + * specified calendar. + * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @param <B> the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) { + try { + for (final Rule rule : rules) { + rule.appendTo(buf, calendar); + } + } catch (final IOException ioe) { + ExceptionUtils.rethrow(ioe); + } + return buf; + } + + // Accessors + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#getPattern() + */ + @Override + public String getPattern() { + return pattern; + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone() + */ + @Override + public TimeZone getTimeZone() { + return timeZone; + } + + /* (non-Javadoc) + * @see org.apache.commons.lang3.time.DatePrinter#getLocale() + */ + @Override + public Locale getLocale() { + return locale; + } + + /** + * Gets an estimate for the maximum string length that the + * formatter will produce. + * + * <p>The actual formatted length will almost always be less than or + * equal to this amount.</p> + * + * @return the maximum formatted length + */ + public int getMaxLengthEstimate() { + return maxLengthEstimate; + } + + // Basics + /** + * Compares two objects for equality. + * + * @param obj the object to compare to + * @return {@code true} if equal + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof FastDatePrinter)) { + return false; + } + final FastDatePrinter other = (FastDatePrinter) obj; + return pattern.equals(other.pattern) + && timeZone.equals(other.timeZone) + && locale.equals(other.locale); + } + + /** + * Returns a hash code compatible with equals. + * + * @return a hash code compatible with equals + */ + @Override + public int hashCode() { + return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); + } + + /** + * Gets a debugging string version of this formatter. + * + * @return a debugging string + */ + @Override + public String toString() { + return "FastDatePrinter[" + pattern + "," + locale + "," + timeZone.getID() + "]"; + } + + // Serializing + /** + * Create the object after serialization. This implementation reinitializes the + * transient properties. + * + * @param in ObjectInputStream from which the object is being deserialized. + * @throws IOException if there is an IO issue. + * @throws ClassNotFoundException if a class cannot be found. + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + init(); + } + + /** + * Appends two digits to the given buffer. + * + * @param buffer the buffer to append to. + * @param value the value to append digits from. + * @throws IOException If an I/O error occurs + */ + private static void appendDigits(final Appendable buffer, final int value) throws IOException { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } + + private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3 + + /** + * Appends all digits to the given buffer. + * + * @param buffer the buffer to append to. + * @param value the value to append digits from. + * @param minFieldWidth Minimum field width. + * @throws IOException If an I/O error occurs + */ + private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException { + // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array + // see LANG-1248 + if (value < 10000) { + // less memory allocation path works for four digits or less + + int nDigits = 4; + if (value < 1000) { + --nDigits; + if (value < 100) { + --nDigits; + if (value < 10) { + --nDigits; + } + } + } + // left zero pad + for (int i = minFieldWidth - nDigits; i > 0; --i) { + buffer.append('0'); + } + + switch (nDigits) { + case 4: + buffer.append((char) (value / 1000 + '0')); + value %= 1000; + case 3: + if (value >= 100) { + buffer.append((char) (value / 100 + '0')); + value %= 100; + } else { + buffer.append('0'); + } + case 2: + if (value >= 10) { + buffer.append((char) (value / 10 + '0')); + value %= 10; + } else { + buffer.append('0'); + } + case 1: + buffer.append((char) (value + '0')); + } + } else { + // more memory allocation path works for any digits + + // build up decimal representation in reverse + final char[] work = new char[MAX_DIGITS]; + int digit = 0; + while (value != 0) { + work[digit++] = (char) (value % 10 + '0'); + value = value / 10; + } + + // pad with zeros + while (digit < minFieldWidth) { + buffer.append('0'); + --minFieldWidth; + } + + // reverse + while (--digit >= 0) { + buffer.append(work[digit]); + } + } + } + + // Rules + /** + * Inner class defining a rule. + */ + private interface Rule { + /** + * Returns the estimated length of the result. + * + * @return the estimated length + */ + int estimateLength(); + + /** + * Appends the value of the specified calendar to the output buffer based on the rule implementation. + * + * @param buf the output buffer + * @param calendar calendar to be appended + * @throws IOException if an I/O error occurs. + */ + void appendTo(Appendable buf, Calendar calendar) throws IOException; + } + + /** + * Inner class defining a numeric rule. + */ + private interface NumberRule extends Rule { + /** + * Appends the specified value to the output buffer based on the rule implementation. + * + * @param buffer the output buffer + * @param value the value to be appended + * @throws IOException if an I/O error occurs. + */ + void appendTo(Appendable buffer, int value) throws IOException; + } + + /** + * Inner class to output a constant single character. + */ + private static class CharacterLiteral implements Rule { + private final char mValue; + + /** + * Constructs a new instance of {@link CharacterLiteral} + * to hold the specified value. + * + * @param value the character literal + */ + CharacterLiteral(final char value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 1; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValue); + } + } + + /** + * Inner class to output a constant string. + */ + private static class StringLiteral implements Rule { + private final String mValue; + + /** + * Constructs a new instance of {@link StringLiteral} + * to hold the specified value. + * + * @param value the string literal + */ + StringLiteral(final String value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mValue.length(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValue); + } + } + + /** + * Inner class to output one of a set of values. + */ + private static class TextField implements Rule { + private final int mField; + private final String[] mValues; + + /** + * Constructs an instance of {@link TextField} + * with the specified field and values. + * + * @param field the field + * @param values the field values + */ + TextField(final int field, final String[] values) { + mField = field; + mValues = values; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + int max = 0; + for (int i=mValues.length; --i >= 0; ) { + final int len = mValues[i].length(); + if (len > max) { + max = len; + } + } + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValues[calendar.get(mField)]); + } + } + + /** + * Inner class to output an unpadded number. + */ + private static class UnpaddedNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@link UnpaddedNumberField} with the specified field. + * + * @param field the field + */ + UnpaddedNumberField(final int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 4; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + if (value < 10) { + buffer.append((char) (value + '0')); + } else if (value < 100) { + appendDigits(buffer, value); + } else { + appendFullDigits(buffer, value, 1); + } + } + } + + /** + * Inner class to output an unpadded month. + */ + private static class UnpaddedMonthField implements NumberRule { + static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); + + /** + * Constructs an instance of {@link UnpaddedMonthField}. + * + */ + UnpaddedMonthField() { + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + if (value < 10) { + buffer.append((char) (value + '0')); + } else { + appendDigits(buffer, value); + } + } + } + + /** + * Inner class to output a padded number. + */ + private static class PaddedNumberField implements NumberRule { + private final int mField; + private final int mSize; + + /** + * Constructs an instance of {@link PaddedNumberField}. + * + * @param field the field + * @param size size of the output field + */ + PaddedNumberField(final int field, final int size) { + if (size < 3) { + // Should use UnpaddedNumberField or TwoDigitNumberField. + throw new IllegalArgumentException(); + } + mField = field; + mSize = size; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mSize; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + appendFullDigits(buffer, value, mSize); + } + } + + /** + * Inner class to output a two digit number. + */ + private static class TwoDigitNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@link TwoDigitNumberField} with the specified field. + * + * @param field the field + */ + TwoDigitNumberField(final int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + if (value < 100) { + appendDigits(buffer, value); + } else { + appendFullDigits(buffer, value, 2); + } + } + } + + /** + * Inner class to output a two digit year. + */ + private static class TwoDigitYearField implements NumberRule { + static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); + + /** + * Constructs an instance of {@link TwoDigitYearField}. + */ + TwoDigitYearField() { + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.YEAR) % 100); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + appendDigits(buffer, value % 100); + } + } + + /** + * Inner class to output a two digit month. + */ + private static class TwoDigitMonthField implements NumberRule { + static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); + + /** + * Constructs an instance of {@link TwoDigitMonthField}. + */ + TwoDigitMonthField() { + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + appendDigits(buffer, value); + } + } + + /** + * Inner class to output the twelve hour field. + */ + private static class TwelveHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@link TwelveHourField} with the specified + * {@link NumberRule}. + * + * @param rule the rule + */ + TwelveHourField(final NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int value = calendar.get(Calendar.HOUR); + if (value == 0) { + value = calendar.getLeastMaximum(Calendar.HOUR) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + * Inner class to output the twenty four hour field. + */ + private static class TwentyFourHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@link TwentyFourHourField} with the specified + * {@link NumberRule}. + * + * @param rule the rule + */ + TwentyFourHourField(final NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int value = calendar.get(Calendar.HOUR_OF_DAY); + if (value == 0) { + value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + * Inner class to output the numeric day in week. + */ + private static class DayInWeekField implements NumberRule { + private final NumberRule mRule; + + DayInWeekField(final NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + final int value = calendar.get(Calendar.DAY_OF_WEEK); + mRule.appendTo(buffer, value == Calendar.SUNDAY ? 7 : value - 1); + } + + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + * Inner class to output the numeric day in week. + */ + private static class WeekYear implements NumberRule { + private final NumberRule mRule; + + WeekYear(final NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + mRule.appendTo(buffer, calendar.getWeekYear()); + } + + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + + private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache = + new ConcurrentHashMap<>(7); + + /** + * Gets the time zone display name, using a cache for performance. + * + * @param tz the zone to query + * @param daylight true if daylight savings + * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} + * @param locale the locale to use + * @return the textual name of the time zone + */ + static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { + final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); + // This is a very slow call, so cache the results. + return cTimeZoneDisplayCache.computeIfAbsent(key, k -> tz.getDisplayName(daylight, style, locale)); + } + + /** + * Inner class to output a time zone name. + */ + private static class TimeZoneNameRule implements Rule { + private final Locale mLocale; + private final int mStyle; + private final String mStandard; + private final String mDaylight; + + /** + * Constructs an instance of {@link TimeZoneNameRule} with the specified properties. + * + * @param timeZone the time zone + * @param locale the locale + * @param style the style + */ + TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) { + mLocale = LocaleUtils.toLocale(locale); + mStyle = style; + + mStandard = getTimeZoneDisplay(timeZone, false, style, locale); + mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + // We have no access to the Calendar object that will be passed to + // appendTo so base estimate on the TimeZone passed to the + // constructor + return Math.max(mStandard.length(), mDaylight.length()); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + final TimeZone zone = calendar.getTimeZone(); + if (calendar.get(Calendar.DST_OFFSET) == 0) { + buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); + } else { + buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); + } + } + } + + /** + * Inner class to output a time zone as a number {@code +/-HHMM} + * or {@code +/-HH:MM}. + */ + private static class TimeZoneNumberRule implements Rule { + static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); + static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); + + final boolean mColon; + + /** + * Constructs an instance of {@link TimeZoneNumberRule} with the specified properties. + * + * @param colon add colon between HH and MM in the output if {@code true} + */ + TimeZoneNumberRule(final boolean colon) { + mColon = colon; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 5; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + + int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + final int hours = offset / (60 * 60 * 1000); + appendDigits(buffer, hours); + + if (mColon) { + buffer.append(':'); + } + + final int minutes = offset / (60 * 1000) - 60 * hours; + appendDigits(buffer, minutes); + } + } + + /** + * Inner class to output a time zone as a number {@code +/-HHMM} + * or {@code +/-HH:MM}. + */ + private static class Iso8601_Rule implements Rule { + + // Sign TwoDigitHours or Z + static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3); + // Sign TwoDigitHours Minutes or Z + static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5); + // Sign TwoDigitHours : Minutes or Z + static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6); + + /** + * Factory method for Iso8601_Rules. + * + * @param tokenLen a token indicating the length of the TimeZone String to be formatted. + * @return an Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such + * rule exists, an IllegalArgumentException will be thrown. + */ + static Iso8601_Rule getRule(final int tokenLen) { + switch(tokenLen) { + case 1: + return ISO8601_HOURS; + case 2: + return ISO8601_HOURS_MINUTES; + case 3: + return ISO8601_HOURS_COLON_MINUTES; + default: + throw new IllegalArgumentException("invalid number of X"); + } + } + + final int length; + + /** + * Constructs an instance of {@code Iso8601_Rule} with the specified properties. + * + * @param length The number of characters in output (unless Z is output) + */ + Iso8601_Rule(final int length) { + this.length = length; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return length; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); + if (offset == 0) { + buffer.append("Z"); + return; + } + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + final int hours = offset / (60 * 60 * 1000); + appendDigits(buffer, hours); + + if (length<5) { + return; + } + + if (length==6) { + buffer.append(':'); + } + + final int minutes = offset / (60 * 1000) - 60 * hours; + appendDigits(buffer, minutes); + } + } + + /** + * Inner class that acts as a compound key for time zone names. + */ + private static class TimeZoneDisplayKey { + private final TimeZone mTimeZone; + private final int mStyle; + private final Locale mLocale; + + /** + * Constructs an instance of {@link TimeZoneDisplayKey} with the specified properties. + * + * @param timeZone the time zone + * @param daylight adjust the style for daylight saving time if {@code true} + * @param style the time zone style + * @param locale the time zone locale + */ + TimeZoneDisplayKey(final TimeZone timeZone, + final boolean daylight, final int style, final Locale locale) { + mTimeZone = timeZone; + if (daylight) { + mStyle = style | 0x80000000; + } else { + mStyle = style; + } + mLocale = LocaleUtils.toLocale(locale); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TimeZoneDisplayKey) { + final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj; + return + mTimeZone.equals(other.mTimeZone) && + mStyle == other.mStyle && + mLocale.equals(other.mLocale); + } + return false; + } + } +} |