/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (C) 2000-2005 Julian Hyde
// Copyright (C) 2005-2017 Pentaho and others
// All Rights Reserved.
//
// jhyde, 2 November, 2000
*/
package mondrian.util;
import mondrian.olap.Util;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.*;
import java.util.*;
/**
* <code>Format</code> formats numbers, strings and dates according to the
* same specification as Visual Basic's
* <code>format()</code> function. This function is described in more detail
* <a href="http://www.apostate.com/programming/vb-format.html">here</a>. We
* have made the following enhancements to this specification:<ul>
*
* <li>if the international currency symbol (¤) occurs in a format
* string, it is translated to the locale's currency symbol.</li>
*
* <li>the format string "Currency" is translated to the locale's currency
* format string. Negative currency values appear in parentheses.</li>
*
* <li>the string "USD" (abbreviation for U.S. Dollars) may occur in a format
* string.</li>
*
* </ul>
*
* <p>One format object can be used to format multiple values, thereby
* amortizing the time required to parse the format string. Example:</p>
*
* <pre><code>
* double[] values;
* Format format = new Format("##,##0.###;(##,##0.###);;Nil");
* for (int i = 0; i < values.length; i++) {
* System.out.println("Value #" + i + " is " + format.format(values[i]));
* }
* </code></pre>
*
* <p>Still to be implemented:<ul>
*
* <li>String formatting (fill from left/right)</li>
*
* <li>Use client's timezone for printing times.</li>
*
* </ul>
*
* @author jhyde
*/
public class Format {
private String formatString;
private BasicFormat format;
private FormatLocale locale;
/**
* Maximum number of entries in the format cache used by
* {@link #get(String, java.util.Locale)}.
*/
public static final int CacheLimit = 1000;
/**
* Maps (formatString, locale) pairs to {@link Format} objects.
*
* <p>If the number of entries in the cache exceeds 1000,
*/
private static final Map<String, Format> cache =
new LinkedHashMap<String, Format>() {
public boolean removeEldestEntry(Map.Entry<String, Format> entry) {
return size() > CacheLimit;
}
};
static final char thousandSeparator_en = ',';
static final char decimalPlaceholder_en = '.';
static final String dateSeparator_en = "/";
static final String timeSeparator_en = ":";
static final String currencySymbol_en = "$";
static final String currencyFormat_en = "$#,##0.00";
static final String[] daysOfWeekShort_en = {
"", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
static final String[] daysOfWeekLong_en = {
"", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday"
};
static final String[] monthsShort_en = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "",
};
static final String[] monthsLong_en = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December", "",
};
static final char intlCurrencySymbol = '\u00a4';
/**
* Maps strings representing locales (for example, "en_US_Boston", "en_US",
* "en", or "" for the default) to a {@link Format.FormatLocale}.
*/
private static final Map<String, FormatLocale> mapLocaleToFormatLocale =
new HashMap<String, FormatLocale>();
/**
* Cache of parsed format strings and their thousand separator
* tokens length. Used so we don't have to tokenize a format string
* over and over again.
*/
private static final Map<String, ArrayStack<Integer>>
thousandSeparatorTokenMap = new HashMap<String, ArrayStack<Integer>>();
/**
* Locale for US English, also the default for English and for all
* locales.
*/
static final FormatLocale locale_US = createLocale(
'\0', '\0', null, null, null, null, null, null, null, null,
Locale.US);
static class Token {
final int code;
final int flags;
final String token;
final FormatType formatType;
Token(int code, int flags, String token)
{
this.code = code;
this.flags = flags;
this.token = token;
this.formatType =
isNumeric() ? FormatType.NUMERIC
: isDate() ? FormatType.DATE
: isString() ? FormatType.STRING
: null;
}
boolean compatibleWith(FormatType formatType)
{
return formatType == null
|| this.formatType == null
|| formatType == this.formatType;
}
boolean isSpecial()
{
return (flags & SPECIAL) == SPECIAL;
}
boolean isNumeric()
{
return (flags & NUMERIC) == NUMERIC;
}
boolean isDate()
{
return (flags & DATE) == DATE;
}
boolean isString()
{
return (flags & STRING) == STRING;
}
FormatType getFormatType() {
return formatType;
}
BasicFormat makeFormat(FormatLocale locale)
{
if (isDate()) {
return new DateFormat(code, token, locale, false);
} else if (isNumeric()) {
return new LiteralFormat(code, token);
} else {
return new LiteralFormat(token);
}
}
}
/**
* BasicFormat is the interface implemented by the classes which do all
* the work. Whereas {@link Format} has only one method for formatting,
* {@link Format#format(Object)}, this class provides methods for several
* primitive types. To make it easy to combine formatting objects, all
* methods write to a {@link PrintWriter}.
*
* <p>The base implementation of most of these methods throws; there
* is no requirement that a derived class implements all of these methods.
* It is up to {@link Format#parseFormatString} to ensure that, for
* example, the {@link #format(double,StringBuilder)} method is
* never called for {@link DateFormat}.
*/
static class BasicFormat {
final int code;
BasicFormat() {
this(0);
}
BasicFormat(int code) {
this.code = code;
}
FormatType getFormatType() {
return null;
}
void formatNull(StringBuilder buf) {
// SSAS formats null values as the empty string. However, SQL Server
// Management Studio's pivot table formats them as "(null)", so many
// people believe that this is the server's behavior.
}
void format(double d, StringBuilder buf) {
throw new Error();
}
void format(long n, StringBuilder buf) {
throw new Error();
}
void format(String s, StringBuilder buf) {
throw new Error();
}
void format(Date date, StringBuilder buf) {
Calendar calendar = Calendar.getInstance(); // todo: use locale
calendar.setTime(date);
format(calendar, buf);
}
void format(Calendar calendar, StringBuilder buf) {
throw new Error();
}
/**
* Returns whether this format can handle a given value.
*
* <p>Usually returns true;
* one notable exception is a format for negative numbers which
* causes the number to be underflow to zero and therefore be
* ineligible for the negative format.
*
* @param n value
* @return Whether this format is applicable for a given value
*/
boolean isApplicableTo(double n) {
return true;
}
/**
* Returns whether this format can handle a given value.
*
* <p>Usually returns true;
* one notable exception is a format for negative numbers which
* causes the number to be underflow to zero and therefore be
* ineligible for the negative format.
*
* @param n value
* @return Whether this format is applicable for a given value
*/
boolean isApplicableTo(long n) {
return true;
}
}
/**
* AlternateFormat is an implementation of {@link Format.BasicFormat} which
* allows a different format to be used for different kinds of values. If
* there are 4 formats, purposes are as follows:<ol>
* <li>positive numbers</li>
* <li>negative numbers</li>
* <li>zero</li>
* <li>null values</li>
* </ol>
*
* <p>If there are fewer than 4 formats, the first is used as a fall-back.
* See the <a href="http://apostate.com/vb-format-syntax">the
* visual basic format specification</a> for more details.
*/
static class AlternateFormat extends BasicFormat {
final BasicFormat[] formats;
final JavaFormat javaFormat;
AlternateFormat(BasicFormat[] formats, FormatLocale locale)
{
this.formats = formats;
assert formats.length >= 1;
this.javaFormat = new JavaFormat(locale.locale);
}
void formatNull(StringBuilder buf) {
if (formats.length >= 4) {
formats[3].format(0, buf);
} else {
super.formatNull(buf);
}
}
void format(double n, StringBuilder buf) {
int i;
if (n == 0
&& formats.length >= 3
&& formats[2] != null)
{
i = 2;
} else if (n < 0) {
if (formats.length >= 2
&& formats[1] != null)
{
if (formats[1].isApplicableTo(n)) {
n = -n;
i = 1;
} else {
// Does not fit into the negative mask, so use the
// nil mask, if there is one. For example,
// "#.0;(#.0);Nil" formats -0.0001 as "Nil".
if (formats.length >= 3
&& formats[2] != null)
{
i = 2;
} else {
i = 0;
}
}
} else {
i = 0;
if (formats[0].isApplicableTo(n)) {
if (!Bug.BugMondrian687Fixed) {
// Special case for format strings with style,
// like "|#|style='red'". JPivot expects the
// '-' to immediately precede the digits, viz
// "|-6|style='red'|", not "-|6|style='red'|".
// This is not consistent with SSAS 2005, hence
// the bug.
//
// But for other formats, we want '-' to precede
// literals, viz '-$6' not '$-6'. This is SSAS
// 2005's behavior too.
int size = buf.length();
buf.append('-');
n = -n;
formats[i].format(n, buf);
if (buf.substring(size, size + 2).equals(
"-|"))
{
buf.setCharAt(size, '|');
buf.setCharAt(size + 1, '-');
}
return;
}
buf.append('-');
n = -n;
} else {
n = 0;
}
}
} else {
i = 0;
}
formats[i].format(n, buf);
}
void format(long n, StringBuilder buf) {
int i;
if (n == 0
&& formats.length >= 3
&& formats[2] != null)
{
i = 2;
} else if (n < 0) {
if (formats.length >= 2
&& formats[1] != null)
{
if (formats[1].isApplicableTo(n)) {
n = -n;
i = 1;
} else {
// Does not fit into the negative mask, so use the
// nil mask, if there is one. For example,
// "#.0;(#.0);Nil" formats -0.0001 as "Nil".
if (formats.length >= 3
&& formats[2] != null)
{
i = 2;
} else {
i = 0;
}
}
} else {
i = 0;
if (formats[0].isApplicableTo(n)) {
if (!Bug.BugMondrian687Fixed) {
// Special case for format strings with style,
// like "|#|style='red'". JPivot expects the
// '-' to immediately precede the digits, viz
// "|-6|style='red'|", not "-|6|style='red'|".
// This is not consistent with SSAS 2005, hence
// the bug.
//
// But for other formats, we want '-' to precede
// literals, viz '-$6' not '$-6'. This is SSAS
// 2005's behavior too.
final int size = buf.length();
buf.append('-');
n = -n;
formats[i].format(n, buf);
if (buf.substring(size, size + 2).equals(
"-|"))
{
buf.setCharAt(size, '|');
buf.setCharAt(size + 1, '-');
}
return;
}
buf.append('-');
n = -n;
} else {
n = 0;
}
}
} else {
i = 0;
}
formats[i].format(n, buf);
}
void format(String s, StringBuilder buf) {
// since it is not a number, ignore all format strings
buf.append(s);
}
void format(Calendar calendar, StringBuilder buf) {
// We're passing a date to a numeric format string. Convert it to
// the number of days since 1900.
BigDecimal bigDecimal = daysSince1900(calendar);
// since it is not a number, ignore all format strings
format(bigDecimal.doubleValue(), buf);
}
private static BigDecimal daysSince1900(Calendar calendar) {
final long dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
final long year = calendar.get(Calendar.YEAR);
long yearForLeap = year;
if (calendar.get(Calendar.MONTH) < 2) {
--yearForLeap;
}
final long leapDays =
(yearForLeap - 1900) / 4
- (yearForLeap - 1900) / 100
+ (yearForLeap - 2000) / 400;
final long days =
(year - 1900) * 365
+ leapDays
+ dayOfYear
+ 2; // kludge factor to agree with Excel
final long millis =
calendar.get(Calendar.HOUR_OF_DAY) * 3600000
+ calendar.get(Calendar.MINUTE) * 60000
+ calendar.get(Calendar.SECOND) * 1000
+ calendar.get(Calendar.MILLISECOND);
return BigDecimal.valueOf(days).add(
BigDecimal.valueOf(millis).divide(
BigDecimal.valueOf(86400000),
8,
BigDecimal.ROUND_FLOOR));
}
}
/**
* LiteralFormat is an implementation of {@link Format.BasicFormat} which
* prints a constant value, regardless of the value to be formatted.
*
* @see CompoundFormat
*/
static class LiteralFormat extends BasicFormat
{
String s;
LiteralFormat(String s)
{
this(FORMAT_LITERAL, s);
}
LiteralFormat(int code, String s)
{
super(code);
this.s = s;
}
void format(double d, StringBuilder buf) {
buf.append(s);
}
void format(long n, StringBuilder buf) {
buf.append(s);
}
void format(String str, StringBuilder buf) {
buf.append(s);
}
void format(Date date, StringBuilder buf) {
buf.append(s);
}
void format(Calendar calendar, StringBuilder buf) {
buf.append(s);
}
}
/**
* CompoundFormat is an implementation of {@link Format.BasicFormat} where
* each value is formatted by applying a sequence of format elements. Each
* format element is itself a format.
*
* @see AlternateFormat
*/
static class CompoundFormat extends BasicFormat
{
final BasicFormat[] formats;
CompoundFormat(BasicFormat[] formats)
{
this.formats = formats;
assert formats.length >= 2;
}
void format(double v, StringBuilder buf) {
for (int i = 0; i < formats.length; i++) {
formats[i].format(v, buf);
}
}
void format(long v, StringBuilder buf) {
for (int i = 0; i < formats.length; i++) {
formats[i].format(v, buf);
}
}
void format(String v, StringBuilder buf) {
for (int i = 0; i < formats.length; i++) {
formats[i].format(v, buf);
}
}
void format(Date v, StringBuilder buf) {
for (int i = 0; i < formats.length; i++) {
formats[i].format(v, buf);
}
}
void format(Calendar v, StringBuilder buf) {
for (int i = 0; i < formats.length; i++) {
formats[i].format(v, buf);
}
}
boolean isApplicableTo(double n) {
for (int i = 0; i < formats.length; i++) {
if (!formats[i].isApplicableTo(n)) {
return false;
}
}
return true;
}
}
/**
* JavaFormat is an implementation of {@link Format.BasicFormat} which
* prints values using Java's default formatting for their type.
* <code>null</code> values appear as an empty string.
*/
static class JavaFormat extends BasicFormat
{
private final NumberFormat numberFormat;
private final java.text.DateFormat dateFormat;
JavaFormat(Locale locale)
{
this.numberFormat = NumberFormat.getNumberInstance(locale);
this.dateFormat =
java.text.DateFormat.getDateTimeInstance(
java.text.DateFormat.SHORT,
java.text.DateFormat.MEDIUM,
locale);
}
// No need to override format(Object,PrintWriter) or
// format(Date,PrintWriter).
void format(double d, StringBuilder buf) {
// NOTE (jhyde, 2006/12/1): We'd use
// NumberFormat(double,StringBuilder,FieldPosition) if it existed.
buf.append(numberFormat.format(d));
}
void format(long n, StringBuilder buf) {
// NOTE (jhyde, 2006/12/1): We'd use
// NumberFormat(long,StringBuilder,FieldPosition) if it existed.
buf.append(numberFormat.format(n));
}
void format(String s, StringBuilder buf) {
buf.append(s);
}
void format(Calendar calendar, StringBuilder buf) {
// NOTE (jhyde, 2006/12/1): We'd use
// NumberFormat(Date,StringBuilder,FieldPosition) if it existed.
buf.append(dateFormat.format(calendar.getTime()));
}
}
/**
* FallbackFormat catches un-handled datatypes and prints the original
* format string. Better than giving an error. Abstract base class for
* NumericFormat and DateFormat.
*/
static abstract class FallbackFormat extends BasicFormat
{
final String token;
FallbackFormat(int code, String token)
{
super(code);
this.token = token;
}
void format(double d, StringBuilder buf) {
buf.append(token);
}
void format(long n, StringBuilder buf) {
buf.append(token);
}
void format(String s, StringBuilder buf) {
buf.append(token);
}
void format(Calendar calendar, StringBuilder buf) {
buf.append(token);
}
}
/**
* NumericFormat is an implementation of {@link Format.BasicFormat} which
* prints numbers with a given number of decimal places, leading zeroes, in
* exponential notation, etc.
*
* <p>It is implemented using {@link MondrianFloatingDecimal}.
*/
static class NumericFormat extends JavaFormat
{
final FormatLocale locale;
final int digitsLeftOfPoint;
final int zeroesLeftOfPoint;
final int digitsRightOfPoint;
final int zeroesRightOfPoint;
final int digitsRightOfExp;
final int zeroesRightOfExp;
/**
* Number of decimal places to shift the number left before
* formatting it: 2 means multiply by 100; -3 means divide by
* 1000.
*/
int decimalShift;
final char expChar;
final boolean expSign;
final boolean useDecimal;
final boolean useThouSep;
final ArrayStack<Integer> cachedThousandSeparatorPositions;
NumericFormat(
String token,
FormatLocale locale,
int expFormat,
int digitsLeftOfPoint, int zeroesLeftOfPoint,
int digitsRightOfPoint, int zeroesRightOfPoint,
int digitsRightOfExp, int zeroesRightOfExp,
boolean useDecimal, boolean useThouSep,
String formatString)
{
super(locale.locale);
this.locale = locale;
switch (expFormat) {
case FORMAT_E_MINUS_UPPER:
this.expChar = 'E';
this.expSign = false;
break;
case FORMAT_E_PLUS_UPPER:
this.expChar = 'E';
this.expSign = true;
break;
case FORMAT_E_MINUS_LOWER:
this.expChar = 'e';
this.expSign = false;
break;
case FORMAT_E_PLUS_LOWER:
this.expChar = 'e';
this.expSign = true;
break;
default:
this.expChar = 0;
this.expSign = false;
}
this.digitsLeftOfPoint = digitsLeftOfPoint;
this.zeroesLeftOfPoint = zeroesLeftOfPoint;
this.digitsRightOfPoint = digitsRightOfPoint;
this.zeroesRightOfPoint = zeroesRightOfPoint;
this.digitsRightOfExp = digitsRightOfExp;
this.zeroesRightOfExp = zeroesRightOfExp;
this.useDecimal = useDecimal;
this.useThouSep = useThouSep;
this.decimalShift = 0; // set later
// Check if we're dealing with a format macro token rather than
// an actual format string.
formatString = MacroToken.expand(locale, formatString);
if (thousandSeparatorTokenMap.containsKey(formatString)) {
cachedThousandSeparatorPositions =
thousandSeparatorTokenMap.get(formatString);
} else {
// To provide backwards compatibility, we apply the old
// formatting rules if there are less than 2 thousand
// separators in the format string.
String formatStringBuffer = formatString;
// If the format includes a negative format part, we strip it.
final int semiPos =
formatStringBuffer.indexOf(getFormatToken(FORMAT_SEMI));
if (semiPos > 0) {
formatStringBuffer =
formatStringBuffer.substring(0, semiPos);
}
final int nbThousandSeparators =
countOccurrences(
formatStringBuffer,
getFormatToken(FORMAT_THOUSEP).charAt(0));
cachedThousandSeparatorPositions = new ArrayStack<Integer>();
if (nbThousandSeparators > 1) {
// Extract the whole part of the format string
final int decimalPos =
formatStringBuffer.indexOf(
getFormatToken(FORMAT_DECIMAL));
final int endIndex =
decimalPos == -1
? formatStringBuffer.length()
: decimalPos;
final String wholeFormat =
formatStringBuffer.substring(0, endIndex);
// Tokenize it so we can analyze it's structure
final StringTokenizer st =
new StringTokenizer(
wholeFormat,
String.valueOf(
getFormatToken(FORMAT_THOUSEP)));
// We ignore the first token.
// ie: #,###,###
st.nextToken();
// Now we build a list of the token lengths in
// reverse order. The last one in the reversed
// list will be re-applied if the number is
// longer than the format string.
while (st.hasMoreTokens()) {
cachedThousandSeparatorPositions.push(
st.nextToken().length());
}
} else if (nbThousandSeparators == 1) {
// Use old style formatting.
cachedThousandSeparatorPositions.add(3);
}
thousandSeparatorTokenMap.put(
formatString, cachedThousandSeparatorPositions);
}
}
FormatType getFormatType() {
return FormatType.NUMERIC;
}
private ArrayStack<Integer> getThousandSeparatorPositions() {
// Defensive copy
return new ArrayStack<Integer>(cachedThousandSeparatorPositions);
}
private int countOccurrences(final String s, final char c) {
final char[] chars = s.toCharArray();
int count = 0;
for (int i = 0; i < chars.length; i++) {
if (chars[i] == c) {
count++;
}
}
return count;
}
void format(double n, StringBuilder buf)
{
MondrianFloatingDecimal fd = new MondrianFloatingDecimal(n);
shift(fd, decimalShift);
final int formatDigitsRightOfPoint =
zeroesRightOfPoint + digitsRightOfPoint;
if (n == 0.0 || (n < 0 && !shows(fd, formatDigitsRightOfPoint))) {
// Underflow of negative number. Make it zero, so there is no
// '-' sign.
fd = new MondrianFloatingDecimal(0);
}
formatFd0(
fd,
buf,
zeroesLeftOfPoint,
locale.decimalPlaceholder,
zeroesRightOfPoint,
formatDigitsRightOfPoint,
expChar,
expSign,
zeroesRightOfExp,
useThouSep ? locale.thousandSeparator : '\0',
useDecimal,
getThousandSeparatorPositions());
}
boolean isApplicableTo(double n) {
if (n >= 0) {
return true;
}
MondrianFloatingDecimal fd = new MondrianFloatingDecimal(n);
shift(fd, decimalShift);
final int formatDigitsRightOfPoint =
zeroesRightOfPoint + digitsRightOfPoint;
return shows(fd, formatDigitsRightOfPoint);
}
private static boolean shows(
MondrianFloatingDecimal fd,
int formatDigitsRightOfPoint)
{
final int i0 = - fd.decExponent - formatDigitsRightOfPoint;
if (i0 < 0) {
return true;
}
if (i0 > 0) {
return false;
}
if (fd.digits[0] >= '5') {
return true;
}
return false;
}
void format(long n, StringBuilder buf)
{
MondrianFloatingDecimal fd =
new MondrianFloatingDecimal(n);
shift(fd, decimalShift);
formatFd0(
fd,
buf,
zeroesLeftOfPoint,
locale.decimalPlaceholder,
zeroesRightOfPoint,
zeroesRightOfPoint + digitsRightOfPoint,
expChar,
expSign,
zeroesRightOfExp,
useThouSep ? locale.thousandSeparator : '\0',
useDecimal,
getThousandSeparatorPositions());
}
}
/**
* DateFormat is an element of a {@link Format.CompoundFormat} which has a
* value when applied to a {@link Calendar} object. (Values of type {@link
* Date} are automatically converted into {@link Calendar}s when you call
* {@link Format.BasicFormat#format(Date, StringBuilder)} calls
* to format other kinds of values give a runtime error.)
*
* <p>In a typical use of this class, a format string such as "m/d/yy" is
* parsed into DateFormat objects for "m", "d", and "yy", and {@link
* LiteralFormat} objects for "/". A {@link Format.CompoundFormat} object
* is created to bind them together.
*/
static class DateFormat extends FallbackFormat
{
FormatLocale locale;
boolean twelveHourClock;
DateFormat(
int code, String s, FormatLocale locale, boolean twelveHourClock)
{
super(code, s);
this.locale = locale;
this.twelveHourClock = twelveHourClock;
}
FormatType getFormatType() {
return FormatType.DATE;
}
void setTwelveHourClock(boolean twelveHourClock)
{
this.twelveHourClock = twelveHourClock;
}
void format(Calendar calendar, StringBuilder buf)
{
format(code, calendar, buf);
}
private void format(
int code,
Calendar calendar,
StringBuilder buf)
{
switch (code) {
case FORMAT_C:
{
boolean dateSet = !(
calendar.get(Calendar.DAY_OF_YEAR) == 0
&& calendar.get(Calendar.YEAR) == 0);
boolean timeSet = !(
calendar.get(Calendar.SECOND) == 0
&& calendar.get(Calendar.MINUTE) == 0
&& calendar.get(Calendar.HOUR) == 0);
if (dateSet) {
format(FORMAT_DDDDD, calendar, buf);
}
if (dateSet && timeSet) {
buf.append(' ');
}
if (timeSet) {
format(FORMAT_TTTTT, calendar, buf);
}
break;
}
case FORMAT_D:
{
int d = calendar.get(Calendar.DAY_OF_MONTH);
buf.append(d);
break;
}
case FORMAT_DD:
{
int d = calendar.get(Calendar.DAY_OF_MONTH);
if (d < 10) {
buf.append('0');
}
buf.append(d);
break;
}
case FORMAT_DDD:
{
int dow = calendar.get(Calendar.DAY_OF_WEEK);
buf.append(locale.daysOfWeekShort[dow]); // e.g. Sun
break;
}
case FORMAT_DDDD:
{
int dow = calendar.get(Calendar.DAY_OF_WEEK);
buf.append(locale.daysOfWeekLong[dow]); // e.g. Sunday
break;
}
case FORMAT_DDDDD:
{
// Officially, we should use the system's short date
// format. But for now, we always print using the default
// format, m/d/yy.
format(FORMAT_M, calendar, buf);
buf.append(locale.dateSeparator);
format(FORMAT_D, calendar, buf);
buf.append(locale.dateSeparator);
format(FORMAT_YY, calendar, buf);
break;
}
case FORMAT_DDDDDD:
{
format(FORMAT_MMMM_UPPER, calendar, buf);
buf.append(" ");
format(FORMAT_DD, calendar, buf);
buf.append(", ");
format(FORMAT_YYYY, calendar, buf);
break;
}
case FORMAT_W:
{
int dow = calendar.get(Calendar.DAY_OF_WEEK);
buf.append(dow);
break;
}
case FORMAT_WW:
{
int woy = calendar.get(Calendar.WEEK_OF_YEAR);
buf.append(woy);
break;
}
case FORMAT_M:
case FORMAT_M_UPPER:
{
int m = calendar.get(Calendar.MONTH) + 1; // 0-based
buf.append(m);
break;
}
case FORMAT_MM:
case FORMAT_MM_UPPER:
{
int mm = calendar.get(Calendar.MONTH) + 1; // 0-based
if (mm < 10) {
buf.append('0');
}
buf.append(mm);
break;
}
case FORMAT_MMM_LOWER:
case FORMAT_MMM_UPPER:
{
int m = calendar.get(Calendar.MONTH); // 0-based
buf.append(locale.monthsShort[m]); // e.g. Jan
break;
}
case FORMAT_MMMM_LOWER:
case FORMAT_MMMM_UPPER:
case FORMAT_MMMMM_LOWER:
case FORMAT_MMMMM_UPPER:
{
int m = calendar.get(Calendar.MONTH); // 0-based
buf.append(locale.monthsLong[m]); // e.g. January
break;
}
case FORMAT_Q:
{
int m = calendar.get(Calendar.MONTH);
// 0(Jan) -> q1, 1(Feb) -> q1, 2(Mar) -> q1, 3(Apr) -> q2
int q = m / 3 + 1;
buf.append(q);
break;
}
case FORMAT_Y:
{
int doy = calendar.get(Calendar.DAY_OF_YEAR);
buf.append(doy);
break;
}
case FORMAT_YY:
{
int y = calendar.get(Calendar.YEAR) % 100;
if (y < 10) {
buf.append('0');
}
buf.append(y);
break;
}
case FORMAT_YYYY:
{
int y = calendar.get(Calendar.YEAR);
buf.append(y);
break;
}
case FORMAT_H:
{
int h = calendar.get(
twelveHourClock ? Calendar.HOUR : Calendar.HOUR_OF_DAY);
buf.append(h);
break;
}
case FORMAT_HH:
case FORMAT_HH_UPPER:
{
int h = calendar.get(
twelveHourClock ? Calendar.HOUR : Calendar.HOUR_OF_DAY);
if (h < 10) {
buf.append('0');
}
buf.append(h);
break;
}
case FORMAT_N:
{
int n = calendar.get(Calendar.MINUTE);
buf.append(n);
break;
}
case FORMAT_NN:
{
int n = calendar.get(Calendar.MINUTE);
if (n < 10) {
buf.append('0');
}
buf.append(n);
break;
}
case FORMAT_S:
{
int s = calendar.get(Calendar.SECOND);
buf.append(s);
break;
}
case FORMAT_SS:
{
int s = calendar.get(Calendar.SECOND);
if (s < 10) {
buf.append('0');
}
buf.append(s);
break;
}
case FORMAT_TTTTT:
{
// Officially, we should use the system's time format. But
// for now, we always print using the default format, h:mm:ss.
format(FORMAT_H, calendar, buf);
buf.append(locale.timeSeparator);
format(FORMAT_NN, calendar, buf);
buf.append(locale.timeSeparator);
format(FORMAT_SS, calendar, buf);
break;
}
case FORMAT_AMPM:
case FORMAT_UPPER_AM_SOLIDUS_PM:
{
boolean isAm = calendar.get(Calendar.AM_PM) == Calendar.AM;
buf.append(isAm ? "AM" : "PM");
break;
}
case FORMAT_LOWER_AM_SOLIDUS_PM:
{
boolean isAm = calendar.get(Calendar.AM_PM) == Calendar.AM;
buf.append(isAm ? "am" : "pm");
break;
}
case FORMAT_UPPER_A_SOLIDUS_P:
{
boolean isAm = calendar.get(Calendar.AM_PM) == Calendar.AM;
buf.append(isAm ? "A" : "P");
break;
}
case FORMAT_LOWER_A_SOLIDUS_P:
{
boolean isAm = calendar.get(Calendar.AM_PM) == Calendar.AM;
buf.append(isAm ? "a" : "p");
break;
}
default:
throw new Error();
}
}
}
/**
* A FormatLocale contains all information necessary to format objects
* based upon the locale of the end-user. Use {@link Format#createLocale}
* to make one.
*/
public static class FormatLocale
{
char thousandSeparator;
char decimalPlaceholder;
String dateSeparator;
String timeSeparator;
String currencySymbol;
String currencyFormat;
String[] daysOfWeekShort;
String[] daysOfWeekLong;
String[] monthsShort;
String[] monthsLong;
private final Locale locale;
private FormatLocale(
char thousandSeparator,
char decimalPlaceholder,
String dateSeparator,
String timeSeparator,
String currencySymbol,
String currencyFormat,
String[] daysOfWeekShort,
String[] daysOfWeekLong,
String[] monthsShort,
String[] monthsLong,
Locale locale)
{
this.locale = locale;
if (thousandSeparator == '\0') {
thousandSeparator = thousandSeparator_en;
}
this.thousandSeparator = thousandSeparator;
if (decimalPlaceholder == '\0') {
decimalPlaceholder = decimalPlaceholder_en;
}
this.decimalPlaceholder = decimalPlaceholder;
if (dateSeparator == null) {
dateSeparator = dateSeparator_en;
}
this.dateSeparator = dateSeparator;
if (timeSeparator == null) {
timeSeparator = timeSeparator_en;
}
this.timeSeparator = timeSeparator;
if (currencySymbol == null) {
currencySymbol = currencySymbol_en;
}
this.currencySymbol = currencySymbol;
if (currencyFormat == null) {
currencyFormat = currencyFormat_en;
}
this.currencyFormat = currencyFormat;
if (daysOfWeekShort == null) {
daysOfWeekShort = daysOfWeekShort_en;
}
this.daysOfWeekShort = daysOfWeekShort;
if (daysOfWeekLong == null) {
daysOfWeekLong = daysOfWeekLong_en;
}
this.daysOfWeekLong = daysOfWeekLong;
if (monthsShort == null) {
monthsShort = monthsShort_en;
}
this.monthsShort = monthsShort;
if (monthsLong == null) {
monthsLong = monthsLong_en;
}
this.monthsLong = monthsLong;
if (daysOfWeekShort.length != 8
|| daysOfWeekLong.length != 8
|| monthsShort.length != 13
|| monthsLong.length != 13)
{
throw new IllegalArgumentException(
"Format: day or month array has incorrect length");
}
}
}
private static class StringFormat extends BasicFormat
{
final StringCase stringCase;
final String literal;
final JavaFormat javaFormat;
StringFormat(StringCase stringCase, String literal, Locale locale) {
assert stringCase != null;
this.stringCase = stringCase;
this.literal = literal;
this.javaFormat = new JavaFormat(locale);
}
@Override
void format(String s, StringBuilder buf) {
switch (stringCase) {
case UPPER:
s = s.toUpperCase();
break;
case LOWER:
s = s.toLowerCase();
break;
}
buf.append(s);
}
void format(double d, StringBuilder buf) {
final int x = buf.length();
javaFormat.format(d, buf);
String s = buf.substring(x);
buf.setLength(x);
format(s, buf);
}
void format(long n, StringBuilder buf) {
final int x = buf.length();
javaFormat.format(n, buf);
String s = buf.substring(x);
buf.setLength(x);
format(s, buf);
}
void format(Date date, StringBuilder buf) {
final int x = buf.length();
javaFormat.format(date, buf);
String s = buf.substring(x);
buf.setLength(x);
format(s, buf);
}
void format(Calendar calendar, StringBuilder buf) {
final int x = buf.length();
javaFormat.format(calendar, buf);
String s = buf.substring(x);
buf.setLength(x);
format(s, buf);
}
}
private enum StringCase {
UPPER,
LOWER
}
/** Types of Format. */
private static final int GENERAL = 0;
private static final int DATE = 1;
private static final int NUMERIC = 2;
private static final int STRING = 4;
/** A Format is flagged SPECIAL if it needs special processing
* during parsing. */
private static final int SPECIAL = 8;
/** Values for {@link Format.BasicFormat#code}. */
private static final int FORMAT_NULL = 0;
private static final int FORMAT_C = 3;
private static final int FORMAT_D = 4;
private static final int FORMAT_DD = 5;
private static final int FORMAT_DDD = 6;
private static final int FORMAT_DDDD = 7;
private static final int FORMAT_DDDDD = 8;
private static final int FORMAT_DDDDDD = 9;
private static final int FORMAT_W = 10;
private static final int FORMAT_WW = 11;
private static final int FORMAT_M = 12;
private static final int FORMAT_MM = 13;
private static final int FORMAT_MMM_UPPER = 14;
private static final int FORMAT_MMMM_UPPER = 15;
private static final int FORMAT_Q = 16;
private static final int FORMAT_Y = 17;
private static final int FORMAT_YY = 18;
private static final int FORMAT_YYYY = 19;
private static final int FORMAT_H = 20;
private static final int FORMAT_HH = 21;
private static final int FORMAT_N = 22;
private static final int FORMAT_NN = 23;
private static final int FORMAT_S = 24;
private static final int FORMAT_SS = 25;
private static final int FORMAT_TTTTT = 26;
private static final int FORMAT_UPPER_AM_SOLIDUS_PM = 27;
private static final int FORMAT_LOWER_AM_SOLIDUS_PM = 28;
private static final int FORMAT_UPPER_A_SOLIDUS_P = 29;
private static final int FORMAT_LOWER_A_SOLIDUS_P = 30;
private static final int FORMAT_AMPM = 31;
private static final int FORMAT_0 = 32;
private static final int FORMAT_POUND = 33;
private static final int FORMAT_DECIMAL = 34;
private static final int FORMAT_PERCENT = 35;
private static final int FORMAT_THOUSEP = 36;
private static final int FORMAT_TIMESEP = 37;
private static final int FORMAT_DATESEP = 38;
private static final int FORMAT_E_MINUS_UPPER = 39;
private static final int FORMAT_E_PLUS_UPPER = 40;
private static final int FORMAT_E_MINUS_LOWER = 41;
private static final int FORMAT_E_PLUS_LOWER = 42;
private static final int FORMAT_LITERAL = 43;
private static final int FORMAT_BACKSLASH = 44;
private static final int FORMAT_QUOTE = 45;
private static final int FORMAT_CHARACTER_OR_SPACE = 46;
private static final int FORMAT_CHARACTER_OR_NOTHING = 47;
private static final int FORMAT_LOWER = 48;
private static final int FORMAT_UPPER = 49;
private static final int FORMAT_FILL_FROM_LEFT = 50;
private static final int FORMAT_SEMI = 51;
private static final int FORMAT_GENERAL_NUMBER = 52;
private static final int FORMAT_GENERAL_DATE = 53;
private static final int FORMAT_INTL_CURRENCY = 54;
private static final int FORMAT_MMM_LOWER = 55;
private static final int FORMAT_MMMM_LOWER = 56;
private static final int FORMAT_USD = 57;
private static final int FORMAT_MM_UPPER = 58;
private static final int FORMAT_MMMMM_LOWER = 59;
private static final int FORMAT_MMMMM_UPPER = 60;
private static final int FORMAT_HH_UPPER = 61;
private static final int FORMAT_M_UPPER = 62;
private static final Map<Integer, String> formatTokenToFormatString =
new HashMap<Integer, String>();
private static Token nfe(
int code, int flags, String token, String purpose, String description)
{
Util.discard(purpose);
Util.discard(description);
formatTokenToFormatString.put(code, token);
return new Token(code, flags, token);
}
public static List<Token> getTokenList()
{
return Collections.unmodifiableList(Arrays.asList(tokens));
}
/**
* Returns the format token as a string representation
* which corresponds to a given token code.
* @param code The code of the token to obtain.
* @return The string representation of that token.
*/
public static String getFormatToken(int code)
{
return formatTokenToFormatString.get(code);
}
private static final Token[] tokens = {
nfe(
FORMAT_NULL,
NUMERIC,
null,
"No formatting",
"Display the number with no formatting."),
nfe(
FORMAT_C,
DATE,
"C",
null,
"Display the date as ddddd and display the time as t t t t t, in "
+ "that order. Display only date information if there is no "
+ "fractional part to the date serial number; display only time "
+ "information if there is no integer portion."),
nfe(
FORMAT_D,
DATE,
"d",
null,
"Display the day as a number without a leading zero (1 - 31)."),
nfe(
FORMAT_DD,
DATE,
"dd",
null,
"Display the day as a number with a leading zero (01 - 31)."),
nfe(
FORMAT_DDD,
DATE,
"Ddd",
null,
"Display the day as an abbreviation (Sun - Sat)."),
nfe(
FORMAT_DDDD,
DATE,
"dddd",
null,
"Display the day as a full name (Sunday - Saturday)."),
nfe(
FORMAT_DDDDD,
DATE,
"ddddd",
null,
"Display the date as a complete date (including day, month, and "
+ "year), formatted according to your system's short date format "
+ "setting. The default short date format is m/d/yy."),
nfe(
FORMAT_DDDDDD,
DATE,
"dddddd",
null,
"Display a date serial number as a complete date (including day, "
+ "month, and year) formatted according to the long date setting "
+ "recognized by your system. The default long date format is mmmm "
+ "dd, yyyy."),
nfe(
FORMAT_W,
DATE,
"w",
null,
"Display the day of the week as a number (1 for Sunday through 7 "
+ "for Saturday)."),
nfe(
FORMAT_WW,
DATE,
"ww",
null,
"Display the week of the year as a number (1 - 53)."),
nfe(
FORMAT_M,
DATE | SPECIAL,
"m",
null,
"Display the month as a number without a leading zero (1 - 12). If "
+ "m immediately follows h or hh, the minute rather than the month "
+ "is displayed."),
nfe(
FORMAT_M_UPPER,
DATE,
"M",
null,
"Display the month as a number without a leading zero (1 - 12)."),
nfe(
FORMAT_MM,
DATE | SPECIAL,
"mm",
null,
"Display the month as a number with a leading zero (01 - 12). If m "
+ "immediately follows h or hh, the minute rather than the month "
+ "is displayed."),
nfe(
FORMAT_MM_UPPER,
DATE,
"MM",
null,
"Display the month as a number with a leading zero (01 - 12)."),
nfe(
FORMAT_MMM_LOWER,
DATE,
"mmm",
null,
"Display the month as an abbreviation (Jan - Dec)."),
nfe(
FORMAT_MMMM_LOWER,
DATE,
"mmmm",
null,
"Display the month as a full month name (January - December)."),
nfe(
FORMAT_MMM_UPPER,
DATE,
"MMM",
null,
"Display the month as an abbreviation (Jan - Dec)."),
nfe(
FORMAT_MMMM_UPPER,
DATE,
"MMMM",
null,
"Display the month as a full month name (January - December)."),
nfe(
FORMAT_Q,
DATE,
"q",
null,
"Display the quarter of the year as a number (1 - 4)."),
nfe(
FORMAT_Y,
DATE,
"y",
null,
"Display the day of the year as a number (1 - 366)."),
nfe(
FORMAT_YY,
DATE,
"yy",
null,
"Display the year as a 2-digit number (00 - 99)."),
nfe(
FORMAT_YYYY,
DATE,
"yyyy",
null,
"Display the year as a 4-digit number (100 - 9999)."),
nfe(
FORMAT_H,
DATE,
"h",
null,
"Display the hour as a number without leading zeros (0 - 23)."),
nfe(
FORMAT_HH,
DATE,
"hh",
null,
"Display the hour as a number with leading zeros (00 - 23)."),
nfe(
FORMAT_N,
DATE,
"n",
null,
"Display the minute as a number without leading zeros (0 - 59)."),
nfe(
FORMAT_NN,
DATE,
"nn",
null,
"Display the minute as a number with leading zeros (00 - 59)."),
nfe(
FORMAT_S,
DATE,
"s",
null,
"Display the second as a number without leading zeros (0 - 59)."),
nfe(
FORMAT_SS,
DATE,
"ss",
null,
"Display the second as a number with leading zeros (00 - 59)."),
nfe(
FORMAT_TTTTT,
DATE,
"ttttt",
null,
"Display a time as a complete time (including hour, minute, and "
+ "second), formatted using the time separator defined by the time "
+ "format recognized by your system. A leading zero is displayed "
+ "if the leading zero option is selected and the time is before "
+ "10:00 A.M. or P.M. The default time format is h:mm:ss."),
nfe(
FORMAT_UPPER_AM_SOLIDUS_PM,
DATE,
"AM/PM",
null,
"Use the 12-hour clock and display an uppercase AM with any hour "
+ "before noon; display an uppercase PM with any hour between noon and 11:59 P.M."),
nfe(
FORMAT_LOWER_AM_SOLIDUS_PM,
DATE,
"am/pm",
null,
"Use the 12-hour clock and display a lowercase AM with any hour "
+ "before noon; display a lowercase PM with any hour between noon "
+ "and 11:59 P.M."),
nfe(
FORMAT_UPPER_A_SOLIDUS_P,
DATE,
"A/P",
null,
"Use the 12-hour clock and display an uppercase A with any hour "
+ "before noon; display an uppercase P with any hour between noon "
+ "and 11:59 P.M."),
nfe(
FORMAT_LOWER_A_SOLIDUS_P,
DATE,
"a/p",
null,
"Use the 12-hour clock and display a lowercase A with any hour "
+ "before noon; display a lowercase P with any hour between noon "
+ "and 11:59 P.M."),
nfe(
FORMAT_AMPM,
DATE,
"AMPM",
null,
"Use the 12-hour clock and display the AM string literal as "
+ "defined by your system with any hour before noon; display the "
+ "PM string literal as defined by your system with any hour "
+ "between noon and 11:59 P.M. AMPM can be either uppercase or "
+ "lowercase, but the case of the string displayed matches the "
+ "string as defined by your system settings. The default format "
+ "is AM/PM."),
nfe(
FORMAT_0,
NUMERIC | SPECIAL,
"0",
"Digit placeholder",
"Display a digit or a zero. If the expression has a digit in the "
+ "position where the 0 appears in the format string, display it; "
+ "otherwise, display a zero in that position. If the number has "
+ "fewer digits than there are zeros (on either side of the "
+ "decimal) in the format expression, display leading or trailing "
+ "zeros. If the number has more digits to the right of the "
+ "decimal separator than there are zeros to the right of the "
+ "decimal separator in the format expression, round the number to "
+ "as many decimal places as there are zeros. If the number has "
+ "more digits to the left of the decimal separator than there are "
+ "zeros to the left of the decimal separator in the format "
+ "expression, display the extra digits without modification."),
nfe(
FORMAT_POUND,
NUMERIC | SPECIAL,
"#",
"Digit placeholder",
"Display a digit or nothing. If the expression has a digit in the "
+ "position where the # appears in the format string, display it; "
+ "otherwise, display nothing in that position. This symbol works "
+ "like the 0 digit placeholder, except that leading and trailing "
+ "zeros aren't displayed if the number has the same or fewer "
+ "digits than there are # characters on either side of the "
+ "decimal separator in the format expression."),
nfe(
FORMAT_DECIMAL,
NUMERIC | SPECIAL,
".",
"Decimal placeholder",
"In some locales, a comma is used as the decimal separator. The "
+ "decimal placeholder determines how many digits are displayed to "
+ "the left and right of the decimal separator. If the format "
+ "expression contains only number signs to the left of this "
+ "symbol, numbers smaller than 1 begin with a decimal separator. "
+ "If you always want a leading zero displayed with fractional "
+ "numbers, use 0 as the first digit placeholder to the left of "
+ "the decimal separator instead. The actual character used as a "
+ "decimal placeholder in the formatted output depends on the "
+ "Number Format recognized by your system."),
nfe(
FORMAT_PERCENT,
NUMERIC,
"%",
"Percent placeholder",
"The expression is multiplied by 100. The percent character (%) is "
+ "inserted in the position where it appears in the format "
+ "string."),
nfe(
FORMAT_THOUSEP,
NUMERIC | SPECIAL,
",",
"Thousand separator",
"In some locales, a period is used as a thousand separator. The "
+ "thousand separator separates thousands from hundreds within a "
+ "number that has four or more places to the left of the decimal "
+ "separator. Standard use of the thousand separator is specified "
+ "if the format contains a thousand separator surrounded by digit "
+ "placeholders (0 or #). Two adjacent thousand separators or a "
+ "thousand separator immediately to the left of the decimal "
+ "separator (whether or not a decimal is specified) means \"scale "
+ "the number by dividing it by 1000, rounding as needed.\" You "
+ "can scale large numbers using this technique. For example, you "
+ "can use the format string \"##0,,\" to represent 100 million as "
+ "100. Numbers smaller than 1 million are displayed as 0. Two "
+ "adjacent thousand separators in any position other than "
+ "immediately to the left of the decimal separator are treated "
+ "simply as specifying the use of a thousand separator. The "
+ "actual character used as the thousand separator in the "
+ "formatted output depends on the Number Format recognized by "
+ "your system."),
nfe(
FORMAT_TIMESEP,
DATE | SPECIAL,
":",
"Time separator",
"In some locales, other characters may be used to represent the "
+ "time separator. The time separator separates hours, minutes, "
+ "and seconds when time values are formatted. The actual "
+ "character used as the time separator in formatted output is "
+ "determined by your system settings."),
nfe(
FORMAT_DATESEP,
DATE | SPECIAL,
"/",
"Date separator",
"In some locales, other characters may be used to represent the "
+ "date separator. The date separator separates the day, month, "
+ "and year when date values are formatted. The actual character "
+ "used as the date separator in formatted output is determined by "
+ "your system settings."),
nfe(
FORMAT_E_MINUS_UPPER,
NUMERIC | SPECIAL,
"E-",
"Scientific format",
"If the format expression contains at least one digit placeholder "
+ "(0 or #) to the right of E-, E+, e-, or e+, the number is "
+ "displayed in scientific format and E or e is inserted between "
+ "the number and its exponent. The number of digit placeholders "
+ "to the right determines the number of digits in the exponent. "
+ "Use E- or e- to place a minus sign next to negative exponents. "
+ "Use E+ or e+ to place a minus sign next to negative exponents "
+ "and a plus sign next to positive exponents."),
nfe(
FORMAT_E_PLUS_UPPER,
NUMERIC | SPECIAL,
"E+",
"Scientific format",
"See E-."),
nfe(
FORMAT_E_MINUS_LOWER,
NUMERIC | SPECIAL,
"e-",
"Scientific format",
"See E-."),
nfe(
FORMAT_E_PLUS_LOWER,
NUMERIC | SPECIAL,
"e+",
"Scientific format",
"See E-."),
nfe(
FORMAT_LITERAL,
GENERAL,
"-",
"Display a literal character",
"To display a character other than one of those listed, precede it "
+ "with a backslash (\\) or enclose it in double quotation marks "
+ "(\" \")."),
nfe(
FORMAT_LITERAL,
GENERAL,
"+",
"Display a literal character",
"See -."),
nfe(
FORMAT_LITERAL,
GENERAL,
"$",
"Display a literal character",
"See -."),
nfe(
FORMAT_LITERAL,
GENERAL,
"(",
"Display a literal character",
"See -."),
nfe(
FORMAT_LITERAL,
GENERAL,
")",
"Display a literal character",
"See -."),
nfe(
FORMAT_LITERAL,
GENERAL,
" ",
"Display a literal character",
"See -."),
nfe(
FORMAT_BACKSLASH,
GENERAL | SPECIAL,
"\\",
"Display the next character in the format string",
"Many characters in the format expression have a special meaning "
+ "and can't be displayed as literal characters unless they are "
+ "preceded by a backslash. The backslash itself isn't displayed. "
+ "Using a backslash is the same as enclosing the next character "
+ "in double quotation marks. To display a backslash, use two "
+ "backslashes (\\). Examples of characters that can't be "
+ "displayed as literal characters are the date- and "
+ "time-formatting characters (a, c, d, h, m, n, p, q, s, t, w, y, "
+ "and /:), the numeric-formatting characters (#, 0, %, E, e, "
+ "comma, and period), and the string-formatting characters (@, &, "
+ "<, >, and !)."),
nfe(
FORMAT_QUOTE,
GENERAL | SPECIAL,
"\"",
"Display the string inside the double quotation marks",
"To include a string in format from within code, you must use "
+ "Chr(34) to enclose the text (34 is the character code for a "
+ "double quotation mark)."),
nfe(
FORMAT_CHARACTER_OR_SPACE,
STRING,
"@",
"Character placeholder",
"Display a character or a space. If the string has a character in "
+ "the position where the @ appears in the format string, display "
+ "it; otherwise, display a space in that position. Placeholders "
+ "are filled from right to left unless there is an ! character in "
+ "the format string. See below."),
nfe(
FORMAT_CHARACTER_OR_NOTHING,
STRING,
"&",
"Character placeholder",
"Display a character or nothing. If the string has a character in "
+ "the position where the & appears, display it; otherwise, "
+ "display nothing. Placeholders are filled from right to left "
+ "unless there is an ! character in the format string. See "
+ "below."),
nfe(
FORMAT_LOWER,
STRING | SPECIAL,
"<",
"Force lowercase",
"Display all characters in lowercase format."),
nfe(
FORMAT_UPPER,
STRING | SPECIAL,
">",
"Force uppercase",
"Display all characters in uppercase format."),
nfe(
FORMAT_FILL_FROM_LEFT,
STRING | SPECIAL,
"!",
"Force left to right fill of placeholders",
"The default is to fill from right to left."),
nfe(
FORMAT_SEMI,
GENERAL | SPECIAL,
";",
"Separates format strings for different kinds of values",
"If there is one section, the format expression applies to all "
+ "values. If there are two sections, the first section applies "
+ "to positive values and zeros, the second to negative values. If "
+ "there are three sections, the first section applies to positive "
+ "values, the second to negative values, and the third to zeros. "
+ "If there are four sections, the first section applies to "
+ "positive values, the second to negative values, the third to "
+ "zeros, and the fourth to Null values."),
nfe(
FORMAT_INTL_CURRENCY,
NUMERIC | SPECIAL,
intlCurrencySymbol + "",
null,
"Display the locale's currency symbol."),
nfe(
FORMAT_USD,
GENERAL,
"USD",
null,
"Display USD (U.S. Dollars)."),
nfe(
FORMAT_GENERAL_NUMBER,
NUMERIC | SPECIAL,
"General Number",
null,
"Shows numbers as entered."),
nfe(
FORMAT_GENERAL_DATE,
DATE | SPECIAL,
"General Date",
null,
"Shows date and time if expression contains both. If expression is "
+ "only a date or a time, the missing information is not "
+ "displayed."),
nfe(
FORMAT_MMMMM_LOWER,
DATE,
"mmmmm",
null,
"Display the month as a full month name (January - December)."),
nfe(
FORMAT_MMMMM_UPPER,
DATE,
"MMMMM",
null,
"Display the month as a full month name (January - December)."),
nfe(
FORMAT_HH_UPPER,
DATE,
"HH",
null,
"Display the hour as a number with leading zeros (00 - 23)."),
};
// Named formats. todo: Supply the translation strings.
private enum MacroToken {
CURRENCY(
"Currency",
null,
"Shows currency values according to the locale's CurrencyFormat. "
+ "Negative numbers are inside parentheses."),
FIXED(
"Fixed", "0", "Shows at least one digit."),
STANDARD(
"Standard", "#,##0", "Uses a thousands separator."),
PERCENT(
"Percent",
"0.00%",
"Multiplies the value by 100 with a percent sign at the end."),
SCIENTIFIC(
"Scientific", "0.00e+00", "Uses standard scientific notation."),
LONG_DATE(
"Long Date",
"dddd, mmmm dd, yyyy",
"Uses the Long Date format specified in the Regional Settings "
+ "dialog box of the Microsoft Windows Control Panel."),
MEDIUM_DATE(
"Medium Date",
"dd-mmm-yy",
"Uses the dd-mmm-yy format (for example, 03-Apr-93)"),
SHORT_DATE(
"Short Date",
"m/d/yy",
"Uses the Short Date format specified in the Regional Settings "
+ "dialog box of the Windows Control Panel."),
LONG_TIME(
"Long Time",
"h:mm:ss AM/PM",
"Shows the hour, minute, second, and \"AM\" or \"PM\" using the "
+ "h:mm:ss format."),
MEDIUM_TIME(
"Medium Time",
"h:mm AM/PM",
"Shows the hour, minute, and \"AM\" or \"PM\" using the \"hh:mm "
+ "AM/PM\" format."),
SHORT_TIME(
"Short Time",
"hh:mm",
"Shows the hour and minute using the hh:mm format."),
YES_NO(
"Yes/No",
"\\Y\\e\\s;\\Y\\e\\s;\\N\\o;\\N\\o",
"Any nonzero numeric value (usually - 1) is Yes. Zero is No."),
TRUE_FALSE(
"True/False",
"\\T\\r\\u\\e;\\T\\r\\u\\e;\\F\\a\\l\\s\\e;\\F\\a\\l\\s\\e",
"Any nonzero numeric value (usually - 1) is True. Zero is False."),
ON_OFF(
"On/Off",
"\\O\\n;\\O\\n;\\O\\f\\f;\\O\\f\\f",
"Any nonzero numeric value (usually - 1) is On. Zero is Off.");
/**
* Maps macro token names with their related object. Used
* to fast-resolve a macro token without iterating.
*/
private static final Map<String, MacroToken> MAP =
new HashMap<String, MacroToken>();
static {
for (MacroToken macroToken : values()) {
MAP.put(macroToken.token, macroToken);
}
}
MacroToken(String token, String translation, String description) {
this.token = token;
this.translation = translation;
this.description = description;
assert name().equals(
token
.replace(',', '_')
.replace(' ', '_')
.replace('/', '_')
.toUpperCase());
assert (translation == null) == name().equals("CURRENCY");
}
final String token;
final String translation;
final String description;
static String expand(FormatLocale locale, String formatString) {
final MacroToken macroToken = MAP.get(formatString);
if (macroToken == null) {
return formatString;
}
if (macroToken == MacroToken.CURRENCY) {
// e.g. "$#,##0.00;($#,##0.00)"
return locale.currencyFormat
+ ";(" + locale.currencyFormat + ")";
} else {
return macroToken.translation;
}
}
}
/**
* Constructs a <code>Format</code> in a specific locale.
*
* @param formatString the format string; see
* <a href="http://www.apostate.com/programming/vb-format.html">this
* description</a> for more details
* @param locale The locale
*/
public Format(String formatString, Locale locale)
{
this(formatString, getBestFormatLocale(locale));
}
/**
* Constructs a <code>Format</code> in a specific locale.
*
* @param formatString the format string; see
* <a href="http://www.apostate.com/programming/vb-format.html">this
* description</a> for more details
* @param locale The locale
*
* @see FormatLocale
* @see #createLocale
*/
public Format(String formatString, FormatLocale locale)
{
if (formatString == null) {
formatString = "";
}
this.formatString = formatString;
if (locale == null) {
locale = locale_US;
}
this.locale = locale;
List<BasicFormat> alternateFormatList = new ArrayList<BasicFormat>();
FormatType[] formatType = {null};
while (formatString.length() > 0) {
formatString = parseFormatString(
formatString, alternateFormatList, formatType);
}
// If the format string is empty, use a Java format.
// Later entries in the formats list default to the first (e.g.
// "#.00;;Nil"), but the first entry must be set.
if (alternateFormatList.size() == 0
|| alternateFormatList.get(0) == null)
{
format = new JavaFormat(locale.locale);
} else if (alternateFormatList.size() == 1
&& (formatType[0] == FormatType.DATE
|| formatType[0] == FormatType.STRING))
{
format = alternateFormatList.get(0);
} else {
BasicFormat[] alternateFormats =
alternateFormatList.toArray(
new BasicFormat[alternateFormatList.size()]);
format = new AlternateFormat(alternateFormats, locale);
}
}
/**
* Constructs a <code>Format</code> in a specific locale, or retrieves
* one from the cache if one already exists.
*
* <p>If the number of entries in the cache exceeds {@link #CacheLimit},
* replaces the eldest entry in the cache.
*
* @param formatString the format string; see
* <a href="http://www.apostate.com/programming/vb-format.html">this
* description</a> for more details
*
* @return format for given format string in given locale
*/
public static Format get(String formatString, Locale locale) {
String key = formatString + "@@@" + locale;
Format format = cache.get(key);
if (format == null) {
synchronized (cache) {
format = cache.get(key);
if (format == null) {
format = new Format(formatString, locale);
cache.put(key, format);
}
}
}
return format;
}
/**
* Create a {@link FormatLocale} object characterized by the given
* properties.
*
* @param thousandSeparator the character used to separate thousands in
* numbers, or ',' by default. For example, 12345 is '12,345 in English,
* '12.345 in French.
* @param decimalPlaceholder the character placed between the integer and
* the fractional part of decimal numbers, or '.' by default. For
* example, 12.34 is '12.34' in English, '12,34' in French.
* @param dateSeparator the character placed between the year, month and
* day of a date such as '12/07/2001', or '/' by default.
* @param timeSeparator the character placed between the hour, minute and
* second value of a time such as '1:23:45 AM', or ':' by default.
* @param daysOfWeekShort Short forms of the days of the week.
* The array is 1-based, because position
* {@link Calendar#SUNDAY} (= 1) must hold Sunday, etc.
* The array must have 8 elements.
* For example {"", "Sun", "Mon", ..., "Sat"}.
* @param daysOfWeekLong Long forms of the days of the week.
* The array is 1-based, because position
* {@link Calendar#SUNDAY} must hold Sunday, etc.
* The array must have 8 elements.
* For example {"", "Sunday", ..., "Saturday"}.
* @param monthsShort Short forms of the months of the year.
* The array is 0-based, because position
* {@link Calendar#JANUARY} (= 0) holds January, etc.
* For example {"Jan", ..., "Dec", ""}.
* @param monthsLong Long forms of the months of the year.
* The array is 0-based, because position
* {@link Calendar#JANUARY} (= 0) holds January, etc.
* For example {"January", ..., "December", ""}.
* @param locale if this is not null, register that the constructed
* <code>FormatLocale</code> is the default for <code>locale</code>
*/
public static FormatLocale createLocale(
char thousandSeparator,
char decimalPlaceholder,
String dateSeparator,
String timeSeparator,
String currencySymbol,
String currencyFormat,
String[] daysOfWeekShort,
String[] daysOfWeekLong,
String[] monthsShort,
String[] monthsLong,
Locale locale)
{
FormatLocale formatLocale = new FormatLocale(
thousandSeparator, decimalPlaceholder, dateSeparator,
timeSeparator, currencySymbol, currencyFormat, daysOfWeekShort,
daysOfWeekLong, monthsShort, monthsLong, locale);
if (locale != null) {
registerFormatLocale(formatLocale, locale);
}
return formatLocale;
}
public static FormatLocale createLocale(Locale locale)
{
final DecimalFormatSymbols decimalSymbols =
new DecimalFormatSymbols(locale);
final DateFormatSymbols dateSymbols = new DateFormatSymbols(locale);
Calendar calendar = Calendar.getInstance(locale);
calendar.set(1969, Calendar.DECEMBER, 31, 0, 0, 0);
final Date date = calendar.getTime();
final java.text.DateFormat dateFormat =
java.text.DateFormat.getDateInstance(
java.text.DateFormat.SHORT, locale);
final String dateValue = dateFormat.format(date); // "12/31/69"
String dateSeparator = dateValue.substring(2, 3); // "/"
final java.text.DateFormat timeFormat =
java.text.DateFormat.getTimeInstance(
java.text.DateFormat.SHORT, locale);
final String timeValue = timeFormat.format(date); // "12:00:00"
String timeSeparator = timeValue.substring(2, 3); // ":"
// Deduce the locale's currency format.
// For example, US is "$#,###.00"; France is "#,###-00FF".
final NumberFormat currencyFormat =
NumberFormat.getCurrencyInstance(locale);
final String currencyValue = currencyFormat.format(123456.78);
String currencyLeft =
currencyValue.substring(0, currencyValue.indexOf("1"));
String currencyRight =
currencyValue.substring(currencyValue.indexOf("8") + 1);
StringBuilder buf = new StringBuilder();
buf.append(currencyLeft);
int minimumIntegerDigits = currencyFormat.getMinimumIntegerDigits();
for (int i = Math.max(minimumIntegerDigits, 4) - 1; i >= 0; --i) {
buf.append(i < minimumIntegerDigits ? '0' : '#');
if (i % 3 == 0 && i > 0) {
buf.append(',');
}
}
if (currencyFormat.getMaximumFractionDigits() > 0) {
buf.append('.');
appendTimes(buf, '0', currencyFormat.getMinimumFractionDigits());
appendTimes(
buf, '#',
currencyFormat.getMaximumFractionDigits()
- currencyFormat.getMinimumFractionDigits());
}
buf.append(currencyRight);
String currencyFormatString = buf.toString();
// If the locale passed is only a language, Java cannot
// resolve the currency symbol and will instead return
// u00a4 (The international currency symbol). For those cases,
// we use the default system locale currency symbol.
String currencySymbol = decimalSymbols.getCurrencySymbol();
if (currencySymbol.equals(Format.intlCurrencySymbol + "")) {
final DecimalFormatSymbols defaultDecimalSymbols =
new DecimalFormatSymbols(Locale.getDefault());
currencySymbol = defaultDecimalSymbols.getCurrencySymbol();
}
return createLocale(
decimalSymbols.getGroupingSeparator(),
decimalSymbols.getDecimalSeparator(),
dateSeparator,
timeSeparator,
currencySymbol,
currencyFormatString,
dateSymbols.getShortWeekdays(),
dateSymbols.getWeekdays(),
dateSymbols.getShortMonths(),
dateSymbols.getMonths(),
locale);
}
private static void appendTimes(StringBuilder buf, char c, int i) {
while (i-- > 0) {
buf.append(c);
}
}
/**
* Returns the {@link FormatLocale} which precisely matches {@link Locale},
* if any, or null if there is none.
*/
public static FormatLocale getFormatLocale(Locale locale)
{
if (locale == null) {
locale = Locale.US;
}
String key = locale.toString();
return mapLocaleToFormatLocale.get(key);
}
/**
* Returns the best {@link FormatLocale} for a given {@link Locale}.
* Never returns null, even if <code>locale</code> is null.
*/
public static synchronized FormatLocale getBestFormatLocale(Locale locale)
{
FormatLocale formatLocale;
if (locale == null) {
return locale_US;
}
String key = locale.toString();
// Look in the cache first.
formatLocale = mapLocaleToFormatLocale.get(key);
if (formatLocale == null) {
// Not in the cache, so ask the factory.
formatLocale = getFormatLocaleUsingFactory(locale);
if (formatLocale == null) {
formatLocale = locale_US;
}
// Add to cache.
mapLocaleToFormatLocale.put(key, formatLocale);
}
return formatLocale;
}
private static FormatLocale getFormatLocaleUsingFactory(Locale locale)
{
FormatLocale formatLocale;
// Lookup full locale, e.g. "en-US-Boston"
if (!locale.getVariant().equals("")) {
formatLocale = createLocale(locale);
if (formatLocale != null) {
return formatLocale;
}
locale = new Locale(locale.getLanguage(), locale.getCountry());
}
// Lookup language and country, e.g. "en-US"
if (!locale.getCountry().equals("")) {
formatLocale = createLocale(locale);
if (formatLocale != null) {
return formatLocale;
}
locale = new Locale(locale.getLanguage());
}
// Lookup language, e.g. "en"
formatLocale = createLocale(locale);
if (formatLocale != null) {
return formatLocale;
}
return null;
}
/**
* Registers a {@link FormatLocale} to a given {@link Locale}. Returns the
* previous mapping.
*/
public static FormatLocale registerFormatLocale(
FormatLocale formatLocale, Locale locale)
{
String key = locale.toString(); // e.g. "en_us_Boston"
return mapLocaleToFormatLocale.put(key, formatLocale);
}
// Values for variable numberState below.
static final int NOT_IN_A_NUMBER = 0;
static final int LEFT_OF_POINT = 1;
static final int RIGHT_OF_POINT = 2;
static final int RIGHT_OF_EXP = 3;
/**
* Reads formatString up to the first semi-colon, or to the end if there
* are no semi-colons. Adds a format to alternateFormatList, and returns
* the remains of formatString.
*/
private String parseFormatString(
String formatString,
List<BasicFormat> alternateFormatList,
FormatType[] formatTypeOut)
{
// Cache the original value
final String originalFormatString = formatString;
// Where we are in a numeric format.
int numberState = NOT_IN_A_NUMBER;
StringBuilder ignored = new StringBuilder();
String prevIgnored = null;
boolean haveSeenNumber = false;
int digitsLeftOfPoint = 0,
digitsRightOfPoint = 0,
digitsRightOfExp = 0,
zeroesLeftOfPoint = 0,
zeroesRightOfPoint = 0,
zeroesRightOfExp = 0;
boolean useDecimal = false,
useThouSep = false,
fillFromRight = true;
// Whether to print numbers in decimal or exponential format. Valid
// values are FORMAT_NULL, FORMAT_E_PLUS_LOWER, FORMAT_E_MINUS_LOWER,
// FORMAT_E_PLUS_UPPER, FORMAT_E_MINUS_UPPER.
int expFormat = FORMAT_NULL;
// todo: Parse the string for ;s
// Look for the format string in the table of named formats.
formatString = MacroToken.expand(locale, formatString);
// Add a semi-colon to the end of the string so the end of the string
// looks like the end of an alternate.
if (!formatString.endsWith(";")) {
formatString = formatString + ";";
}
// Scan through the format string for format elements.
List<BasicFormat> formatList = new ArrayList<BasicFormat>();
List<Integer> thousands = new ArrayList<Integer>();
int decimalShift = 0;
loop:
while (formatString.length() > 0) {
BasicFormat format = null;
String newFormatString;
final Token token = findToken(formatString, formatTypeOut[0]);
if (token != null) {
String matched = token.token;
newFormatString = formatString.substring(matched.length());
if (token.isSpecial()) {
switch (token.code) {
case FORMAT_SEMI:
break loop;
case FORMAT_POUND:
switch (numberState) {
case NOT_IN_A_NUMBER:
numberState = LEFT_OF_POINT;
// fall through
case LEFT_OF_POINT:
digitsLeftOfPoint++;
break;
case RIGHT_OF_POINT:
digitsRightOfPoint++;
break;
case RIGHT_OF_EXP:
digitsRightOfExp++;
break;
default:
throw new Error();
}
break;
case FORMAT_0:
switch (numberState) {
case NOT_IN_A_NUMBER:
numberState = LEFT_OF_POINT;
// fall through
case LEFT_OF_POINT:
zeroesLeftOfPoint++;
break;
case RIGHT_OF_POINT:
zeroesRightOfPoint++;
break;
case RIGHT_OF_EXP:
zeroesRightOfExp++;
break;
default:
throw new Error();
}
break;
case FORMAT_M:
case FORMAT_MM:
{
// "m" and "mm" mean minute if immediately after
// "h" or "hh"; month otherwise.
boolean theyMeantMinute = false;
int j = formatList.size() - 1;
while (j >= 0) {
BasicFormat prevFormat = formatList.get(j);
if (prevFormat instanceof LiteralFormat) {
// ignore boilerplate
j--;
} else if (prevFormat.code == FORMAT_H
|| prevFormat.code == FORMAT_HH
|| prevFormat.code == FORMAT_HH_UPPER)
{
theyMeantMinute = true;
break;
} else {
theyMeantMinute = false;
break;
}
}
if (theyMeantMinute) {
format = new DateFormat(
(token.code == FORMAT_M
? FORMAT_N
: FORMAT_NN),
matched,
locale,
false);
} else {
format = token.makeFormat(locale);
}
break;
}
case FORMAT_DECIMAL:
{
if (numberState == LEFT_OF_POINT) {
decimalShift =
fixThousands(
thousands, formatString, decimalShift);
}
numberState = RIGHT_OF_POINT;
useDecimal = true;
break;
}
case FORMAT_THOUSEP:
{
if (numberState == LEFT_OF_POINT) {
// e.g. "#,##"
useThouSep = true;
thousands.add(formatString.length());
} else {
// e.g. "ddd, mmm dd, yyy"
format = token.makeFormat(locale);
}
break;
}
case FORMAT_TIMESEP:
{
format = new LiteralFormat(locale.timeSeparator);
break;
}
case FORMAT_DATESEP:
{
format = new LiteralFormat(locale.dateSeparator);
break;
}
case FORMAT_BACKSLASH:
{
// Display the next character in the format string.
String s;
if (formatString.length() == 1) {
// Backslash is the last character in the
// string.
s = "";
newFormatString = "";
} else {
s = formatString.substring(1, 2);
newFormatString = formatString.substring(2);
}
format = new LiteralFormat(s);
break;
}
case FORMAT_E_MINUS_UPPER:
case FORMAT_E_PLUS_UPPER:
case FORMAT_E_MINUS_LOWER:
case FORMAT_E_PLUS_LOWER:
{
if (numberState == LEFT_OF_POINT) {
decimalShift =
fixThousands(
thousands, formatString, decimalShift);
}
numberState = RIGHT_OF_EXP;
expFormat = token.code;
if (zeroesLeftOfPoint == 0
&& zeroesRightOfPoint == 0)
{
// We need a mantissa, so that format(123.45,
// "E+") gives "1E+2", not "0E+2" or "E+2".
zeroesLeftOfPoint = 1;
}
break;
}
case FORMAT_QUOTE:
{
// Display the string inside the double quotation
// marks.
String s;
int j = formatString.indexOf("\"", 1);
if (j == -1) {
// The string did not contain a closing quote.
// Use the whole string.
s = formatString.substring(1);
newFormatString = "";
} else {
// Take the string inside the quotes.
s = formatString.substring(1, j);
newFormatString = formatString.substring(
j + 1);
}
format = new LiteralFormat(s);
break;
}
case FORMAT_UPPER:
{
format =
new StringFormat(
StringCase.UPPER, ">", locale.locale);
break;
}
case FORMAT_LOWER:
{
format =
new StringFormat(
StringCase.LOWER, "<", locale.locale);
break;
}
case FORMAT_FILL_FROM_LEFT:
{
fillFromRight = false;
break;
}
case FORMAT_GENERAL_NUMBER:
{
format = new JavaFormat(locale.locale);
break;
}
case FORMAT_GENERAL_DATE:
{
format = new JavaFormat(locale.locale);
break;
}
case FORMAT_INTL_CURRENCY:
{
format = new LiteralFormat(locale.currencySymbol);
break;
}
default:
throw new Error();
}
if (formatTypeOut[0] == null) {
formatTypeOut[0] = token.getFormatType();
}
if (format == null) {
// If the special-case code does not set format,
// we should not create a format element. (The
// token probably caused some flag to be set.)
ignored.append(matched);
} else {
prevIgnored = ignored.toString();
ignored.setLength(0);
}
} else {
format = token.makeFormat(locale);
}
} else {
// None of the standard format elements matched. Make the
// current character into a literal.
format = new LiteralFormat(
formatString.substring(0, 1));
newFormatString = formatString.substring(1);
}
if (format != null) {
if (numberState != NOT_IN_A_NUMBER) {
// Having seen a few number tokens, we're looking at a
// non-number token. Create the number first.
if (numberState == LEFT_OF_POINT) {
decimalShift =
fixThousands(
thousands, formatString, decimalShift);
}
NumericFormat numericFormat = new NumericFormat(
prevIgnored, locale, expFormat, digitsLeftOfPoint,
zeroesLeftOfPoint, digitsRightOfPoint,
zeroesRightOfPoint, digitsRightOfExp, zeroesRightOfExp,
useDecimal, useThouSep, originalFormatString);
formatList.add(numericFormat);
numberState = NOT_IN_A_NUMBER;
haveSeenNumber = true;
}
formatList.add(format);
if (formatTypeOut[0] == null) {
formatTypeOut[0] = format.getFormatType();
}
}
formatString = newFormatString;
}
if (numberState != NOT_IN_A_NUMBER) {
// We're still in a number. Create a number format.
if (numberState == LEFT_OF_POINT) {
decimalShift =
fixThousands(
thousands, formatString, decimalShift);
}
NumericFormat numericFormat = new NumericFormat(
prevIgnored, locale, expFormat, digitsLeftOfPoint,
zeroesLeftOfPoint, digitsRightOfPoint, zeroesRightOfPoint,
digitsRightOfExp, zeroesRightOfExp, useDecimal, useThouSep,
originalFormatString);
formatList.add(numericFormat);
numberState = NOT_IN_A_NUMBER;
haveSeenNumber = true;
}
if (formatString.startsWith(";")) {
formatString = formatString.substring(1);
}
// If they used some symbol like 'AM/PM' in the format string, tell all
// date formats to use twelve hour clock. Likewise, figure out the
// multiplier implied by their use of "%" or ",".
boolean twelveHourClock = false;
// User 24 hour format if HH is used in the format String. This follows
// Java's date format convention.
boolean isFormatHH = false;
for (int i = 0; i < formatList.size(); i++) {
switch (formatList.get(i).code) {
case FORMAT_HH_UPPER:
isFormatHH = true;
break;
case FORMAT_UPPER_AM_SOLIDUS_PM:
case FORMAT_LOWER_AM_SOLIDUS_PM:
case FORMAT_UPPER_A_SOLIDUS_P:
case FORMAT_LOWER_A_SOLIDUS_P:
case FORMAT_AMPM:
twelveHourClock = true & !isFormatHH;
break;
case FORMAT_PERCENT:
// If "%" occurs, the number should be multiplied by 100.
decimalShift += 2;
break;
case FORMAT_THOUSEP:
// If there is a thousands separator (",") immediately to the
// left of the point, or at the end of the number, divide the
// number by 1000. (Or by 1000^n if there are more than one.)
if (haveSeenNumber
&& i + 1 < formatList.size())
{
final BasicFormat nextFormat = formatList.get(i + 1);
if (nextFormat.code != FORMAT_THOUSEP
&& nextFormat.code != FORMAT_0
&& nextFormat.code != FORMAT_POUND)
{
for (int j = i;
j >= 0 && formatList.get(j).code == FORMAT_THOUSEP;
j--)
{
decimalShift -= 3;
formatList.remove(j); // ignore
--i;
}
}
}
break;
default:
}
}
if (twelveHourClock) {
for (int i = 0; i < formatList.size(); i++) {
if (formatList.get(i) instanceof DateFormat) {
((DateFormat) formatList.get(i)).setTwelveHourClock(true);
}
}
}
if (decimalShift != 0) {
for (int i = 0; i < formatList.size(); i++) {
if (formatList.get(i) instanceof NumericFormat) {
((NumericFormat) formatList.get(i)).decimalShift =
decimalShift;
}
}
}
// Merge adjacent literal formats.
//
// Must do this AFTER adjusting for percent. Otherwise '%' and following
// '|' might be merged into a plain literal, and '%' would lose its
// special powers.
for (int i = 0; i < formatList.size(); ++i) {
if (i > 0
&& formatList.get(i) instanceof LiteralFormat
&& formatList.get(i - 1) instanceof LiteralFormat)
{
formatList.set(
i - 1,
new LiteralFormat(
((LiteralFormat) formatList.get(i - 1)).s
+ ((LiteralFormat) formatList.get(i)).s));
formatList.remove(i);
--i;
}
}
// Create a CompoundFormat containing all of the format elements.
// This is the end of an alternate - or of the whole format string.
// Push the current list of formats onto the list of alternates.
BasicFormat alternateFormat;
switch (formatList.size()) {
case 0:
alternateFormat = null;
break;
case 1:
alternateFormat = formatList.get(0);
break;
default:
alternateFormat =
new CompoundFormat(
formatList.toArray(
new BasicFormat[formatList.size()]));
break;
}
alternateFormatList.add(alternateFormat);
return formatString;
}
private Token findToken(String formatString, FormatType formatType) {
for (int i = tokens.length - 1; i > 0; i--) {
final Token token = tokens[i];
if (formatString.startsWith(token.token)
&& token.compatibleWith(formatType))
{
return token;
}
}
return null;
}
private int fixThousands(
List<Integer> thousands, String formatString, int shift)
{
int offset = formatString.length() + 1;
for (int i = thousands.size() - 1; i >= 0; i--) {
Integer integer = thousands.get(i);
thousands.set(i, integer - offset);
++offset;
}
while (thousands.size() > 0
&& thousands.get(thousands.size() - 1) == 0)
{
shift -= 3;
thousands.remove(thousands.size() - 1);
}
return shift;
}
public String format(Object o)
{
StringBuilder buf = new StringBuilder();
format(o, buf);
return buf.toString();
}
private StringBuilder format(Object o, StringBuilder buf) {
if (o == null) {
format.formatNull(buf);
} else {
// For final classes, it is more efficient to switch using
// class equality than using 'instanceof'.
Class<? extends Object> clazz = o.getClass();
if (clazz == Double.class) {
format.format((Double) o, buf);
} else if (clazz == Float.class) {
format.format((Float) o, buf);
} else if (clazz == Integer.class) {
format.format((Integer) o, buf);
} else if (clazz == Long.class) {
format.format((Long) o, buf);
} else if (clazz == Short.class) {
format.format((Short) o, buf);
} else if (clazz == Byte.class) {
format.format((Byte) o, buf);
} else if (o instanceof BigDecimal) {
format.format(
((BigDecimal) o).doubleValue(), buf);
} else if (o instanceof BigInteger) {
format.format(
((BigInteger) o).longValue(), buf);
} else if (clazz == String.class) {
format.format((String) o, buf);
} else if (o instanceof java.util.Date) {
// includes java.sql.Date, java.sql.Time and java.sql.Timestamp
format.format((Date) o, buf);
} else if (o instanceof Calendar) {
format.format((Calendar) o, buf);
} else {
buf.append(o.toString());
}
}
return buf;
}
public String getFormatString()
{
return formatString;
}
private static void shift(
MondrianFloatingDecimal fd,
int i)
{
if (fd.isExceptional
|| fd.nDigits == 1 && fd.digits[0] == '0')
{
; // don't multiply zero
} else {
fd.decExponent += i;
}
}
/** Formats a floating decimal to a given buffer. */
private static void formatFd0(
MondrianFloatingDecimal fd,
StringBuilder buf,
int minDigitsLeftOfDecimal,
char decimalChar, // '.' or ','
int minDigitsRightOfDecimal,
int maxDigitsRightOfDecimal,
char expChar, // 'E' or 'e'
boolean expSign, // whether to print '+' if exp is positive
int minExpDigits, // minimum digits in exponent
char thousandChar, // ',' or '.', or 0
boolean useDecimal,
ArrayStack<Integer> thousandSeparatorPositions)
{
// char result[] = new char[nDigits + 10]; // crashes for 1.000.000,00
// the result length does *not* depend from nDigits
// it is : decExponent
// +maxDigitsRightOfDecimal
// + 10 (for decimal point and sign or -Infinity)
// +decExponent/3 (for the thousand separators)
int resultLen =
10 + Math.abs(fd.decExponent) * 4 / 3 + maxDigitsRightOfDecimal;
char result[] = new char[resultLen];
int i = formatFd1(
fd,
result,
0,
minDigitsLeftOfDecimal,
decimalChar,
minDigitsRightOfDecimal,
maxDigitsRightOfDecimal,
expChar,
expSign,
minExpDigits,
thousandChar,
useDecimal,
thousandSeparatorPositions);
buf.append(result, 0, i);
}
/** Formats a floating decimal to a given char array. */
private static int formatFd1(
MondrianFloatingDecimal fd,
char result[],
int i,
int minDigitsLeftOfDecimal,
char decimalChar, // '.' or ','
int minDigitsRightOfDecimal,
int maxDigitsRightOfDecimal,
char expChar, // 'E' or 'e'
boolean expSign, // whether to print '+' if exp is positive
int minExpDigits, // minimum digits in exponent
char thousandChar, // ',' or '.' or 0
boolean useDecimal,
ArrayStack<Integer> thousandSeparatorPositions)
{
if (expChar != 0) {
// Print the digits left of the 'E'.
int oldExp = fd.decExponent;
fd.decExponent = Math.min(minDigitsLeftOfDecimal, fd.nDigits);
boolean oldIsNegative = fd.isNegative;
fd.isNegative = false;
i = formatFd2(
fd,
result,
i,
minDigitsLeftOfDecimal,
decimalChar,
minDigitsRightOfDecimal,
maxDigitsRightOfDecimal,
'\0',
useDecimal,
thousandSeparatorPositions);
fd.decExponent = oldExp;
fd.isNegative = oldIsNegative;
result[i++] = expChar;
// Print the digits right of the 'E'.
return fd.formatExponent(result, i, expSign, minExpDigits);
} else {
return formatFd2(
fd,
result,
i,
minDigitsLeftOfDecimal,
decimalChar,
minDigitsRightOfDecimal,
maxDigitsRightOfDecimal,
thousandChar,
useDecimal,
thousandSeparatorPositions);
}
}
static int formatFd2(
MondrianFloatingDecimal fd,
char result[],
int i,
int minDigitsLeftOfDecimal,
char decimalChar, // '.' or ','
int minDigitsRightOfDecimal,
int maxDigitsRightOfDecimal,
char thousandChar, // ',' or '.' or 0
boolean useDecimal,
ArrayStack<Integer> thousandSeparatorPositions)
{
if (fd.isNegative) {
result[i++] = '-';
}
if (fd.isExceptional) {
System.arraycopy(fd.digits, 0, result, i, fd.nDigits);
return i + fd.nDigits;
}
// Build a new array of digits, padded with 0s at either end. For
// example, here is the array we would build for 1234.56.
//
// | 0 0 1 2 3 . 4 5 6 0 0 |
// | |- nDigits=6 -----------------------| |
// | |- decExponent=3 -| |
// |- minDigitsLeftOfDecimal=5 --| |
// | |- minDigitsRightOfDecimal=5 --|
// |- wholeDigits=5 -------------|- fractionDigits=5 -----------|
// |- totalDigits=10 -------------------------------------------|
// | |- maxDigitsRightOfDecimal=5 --|
int wholeDigits = Math.max(fd.decExponent, minDigitsLeftOfDecimal),
fractionDigits = Math.max(
fd.nDigits - fd.decExponent, minDigitsRightOfDecimal),
totalDigits = wholeDigits + fractionDigits;
char[] digits2 = new char[totalDigits];
for (int j = 0; j < totalDigits; j++) {
digits2[j] = '0';
}
for (int j = 0; j < fd.nDigits; j++) {
digits2[wholeDigits - fd.decExponent + j] = fd.digits[j];
}
// Now round. Suppose that we want to round 1234.56 to 1 decimal
// place (that is, maxDigitsRightOfDecimal = 1). Then lastDigit
// initially points to '5'. We find out that we need to round only
// when we see that the next digit ('6') is non-zero.
//
// | 0 0 1 2 3 . 4 5 6 0 0 |
// | | ^ | |
// | maxDigitsRightOfDecimal=1 |
int lastDigit = wholeDigits + maxDigitsRightOfDecimal;
if (lastDigit < totalDigits) {
// We need to truncate -- also round if the trailing digits are
// 5000... or greater.
int m = totalDigits;
if ( digits2.length >= lastDigit && lastDigit != 0 ) {
while ( digits2[lastDigit - 1] < '0' || digits2[lastDigit - 1] > '9' ) {
// BACKLOG-15504
lastDigit--;
}
}
while (true) {
m--;
if (m < 0) {
// The entire number was 9s. Re-allocate, so we can
// prepend a '1'.
wholeDigits++;
totalDigits++;
lastDigit++;
char[] old = digits2;
digits2 = new char[totalDigits];
digits2[0] = '1';
System.arraycopy(old, 0, digits2, 1, old.length);
break;
} else if (m == lastDigit) {
char d = digits2[m];
digits2[m] = '0';
if (d < '5' || d == ':') {
break; // no need to round
}
} else if (m > lastDigit) {
digits2[m] = '0';
} else if (digits2[m] == '9') {
digits2[m] = '0';
// do not break - we have to carry
} else {
digits2[m]++;
break; // nothing to carry
}
}
}
// Find the first non-zero digit and the last non-zero digit.
int firstNonZero = wholeDigits,
firstTrailingZero = 0;
for (int j = 0; j < totalDigits; j++) {
if (digits2[j] != '0') {
if (j < firstNonZero) {
firstNonZero = j;
}
firstTrailingZero = j + 1;
}
}
int firstDigitToPrint = firstNonZero;
if (firstDigitToPrint > wholeDigits - minDigitsLeftOfDecimal) {
firstDigitToPrint = wholeDigits - minDigitsLeftOfDecimal;
}
int lastDigitToPrint = firstTrailingZero;
if (lastDigitToPrint > wholeDigits + maxDigitsRightOfDecimal) {
lastDigitToPrint = wholeDigits + maxDigitsRightOfDecimal;
}
if (lastDigitToPrint < wholeDigits + minDigitsRightOfDecimal) {
lastDigitToPrint = wholeDigits + minDigitsRightOfDecimal;
}
if (thousandChar != '\0'
&& thousandSeparatorPositions.size() > 0)
{
// Now print the number. That will happen backwards, so we
// store it temporarily and then invert.
ArrayStack<Character> formattedWholeDigits =
new ArrayStack<Character>();
// We need to keep track of how many digits we printed in the
// current token.
int nbInserted = 0;
for (int j = wholeDigits - 1; j >= firstDigitToPrint; j--) {
// Check if we need to insert another thousand separator
if (nbInserted % thousandSeparatorPositions.peek() == 0
&& nbInserted > 0)
{
formattedWholeDigits.push(thousandChar);
nbInserted = 0;
// The last format token is kept because we re-apply it
// until the end of the digits.
if (thousandSeparatorPositions.size() > 1) {
thousandSeparatorPositions.pop();
}
}
// Insert the next digit.
formattedWholeDigits.push(digits2[j]);
nbInserted++;
}
// We're done. Invert the print out and add it to
// the result array.
while (formattedWholeDigits.size() > 0) {
result[i++] = formattedWholeDigits.pop();
}
} else {
// There are no thousand separators. Just put the
// digits in the results array.
for (int j = firstDigitToPrint; j < wholeDigits; j++) {
result[i++] = digits2[j];
}
}
if (wholeDigits < lastDigitToPrint
|| (useDecimal
&& wholeDigits == lastDigitToPrint))
{
result[i++] = decimalChar;
}
for (int j = wholeDigits; j < lastDigitToPrint; j++) {
result[i++] = digits2[j];
}
return i;
}
private enum FormatType {
STRING,
DATE,
NUMERIC
}
private static class DummyDecimalFormat extends DecimalFormat {
private FieldPosition pos;
public StringBuffer format(
double number,
StringBuffer result,
FieldPosition fieldPosition)
{
pos = fieldPosition;
return result;
}
}
/** Specification for MondrianFloatingDecimal. */
private static class MondrianFloatingDecimalSpec {
boolean isExceptional;
boolean isNegative;
int decExponent;
char digits[];
int nDigits;
/** Creates a floating decimal with a given value. */
MondrianFloatingDecimalSpec(double n) {
}
/**
* Appends {@link #decExponent} to result string. Returns i plus the
* number of chars written.
*
* <p>Implementation may assume that exponent has 3 or fewer digits.</p>
*
* <p>For example, given {@code decExponent} = 2,
* {@code formatExponent(result, 5, true, 2)}
* will write '0' into result[5]
* and '2' into result[6] and return 7.</p>
*
* @param result Result buffer
* @param i Initial offset into result buffer
* @param expSign Whether to print a '+' sign if exponent is positive
* (always prints '-' if negative)
* @param minExpDigits Minimum number of digits to write
* @return Offset into result buffer after writing chars
*/
int formatExponent(
char[] result,
int i,
boolean expSign,
int minExpDigits)
{
return i;
}
/**
* Handles an exceptional number. If {@link #isExceptional} is false,
* does nothing. If {@link #isExceptional} is true, appends the contents
* of {@link #digits} to result starting from i and returns the
* incremented i.
*
* @param result Result buffer
* @param i Initial offset into result buffer
* @return Offset into result buffer after writing chars
*/
int handleExceptional(char[] result, int i) {
return i;
}
/**
* Handles a negative number. If {@link #isNegative}, appends '-' to
* result at i and returns i + 1; otherwise does nothing and returns i.
*
* @param result Result buffer
* @param i Initial offset into result buffer
* @return Offset into result buffer after writing chars
*/
int handleNegative(char[] result, int i) {
return i;
}
}
}
// End Format.java