package org.civilian.text;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import org.civilian.util.Check;
import org.civilian.util.StringUtil;
/**
* NumberFormat provides locale dependent formatting of numbers.
*/
public class NumberFormat implements Serializable
{
private static final long serialVersionUID = 1L;
/**
* Creates a new NumberFormat.
*/
public NumberFormat(Locale locale)
{
locale_ = Check.notNull(locale, "locale");
impl_ = java.text.NumberFormat.getNumberInstance(locale);
if (impl_ instanceof DecimalFormat)
{
DecimalFormatSymbols symbols = ((DecimalFormat)impl_).getDecimalFormatSymbols();
groupingSeparator_ = symbols.getGroupingSeparator();
groupingSeparatorString_ = groupingSeparator_ > 0 ? String.valueOf(groupingSeparator_) : null;
decimalSeparator_ = symbols.getDecimalSeparator();
}
}
/**
* Returns the locale of the NumberFormat.
*/
public Locale getLocale()
{
return locale_;
}
/**
* Returns the grouping separator char of the NumberFormat.
* @return the separator or 0 if not defined
*/
public char getGroupingSeparator()
{
return groupingSeparator_;
}
/**
* Returns the grouping separator as String.
* @return the separator or null if not defined
*/
public String getGroupingSepString()
{
return groupingSeparatorString_;
}
/**
* Returns the decimal separator char of the NumberFormat.
* @return the separator or 0 if not known
*/
public char getDecimalSeparator()
{
return decimalSeparator_;
}
//----------------------------
// format natural
//----------------------------
/**
* Formats a long value.
*/
public String formatNatural(long value)
{
return formatNatural(value, null).toString();
}
/**
* Formats a long value.
*/
public String formatNatural(long value, NumberStyle style)
{
return formatNatural(value, style, null).toString();
}
/**
* Formats a long value.
*/
public StringBuilder formatNatural(long value, NumberStyle style, StringBuilder builder)
{
return formatNatural(String.valueOf(value), style, builder);
}
/**
* Formats a natural number.
* @param value the value.
* @param style a NumberStyle to determine grouping. If null the default
* NumberStyle is used.
* @param builder a StringBuilder to which the formatted number is appended.
* If the builder is null, then a new StringBuilder will be created.
* @return the StringBuilder containing the number
*/
public StringBuilder formatNatural(Number value, NumberStyle style, StringBuilder builder)
{
return formatNatural(value != null ? value.toString() : "", style, builder);
}
private StringBuilder formatNatural(String raw, NumberStyle style, StringBuilder builder)
{
builder = setup(builder);
format(raw, norm(style).useGrouping(), 0, 0, builder);
return builder;
}
//----------------------------
// format decimal
//----------------------------
/**
* Formats a double value.
*/
public String formatDecimal(double value)
{
return formatDecimal(value, null);
}
/**
* Formats a double value.
*/
public String formatDecimal(double value, NumberStyle style)
{
return formatDecimal(value, style, null).toString();
}
/**
* Formats a long value.
*/
public StringBuilder formatDecimal(double value, NumberStyle style, StringBuilder builder)
{
return formatDecimal(String.valueOf(value), style, builder);
}
/**
* Formats a number.
*/
public StringBuilder formatDecimal(Number value, NumberStyle style, StringBuilder builder)
{
return formatDecimal(value != null ? value.toString() : "", style, builder);
}
/**
* Formats a decimal number.
* @param raw the raw value.
* @param style a NumberStyle to determine grouping and number of decimals. If null the default
* NumberStyle is used.
* @param builder a StringBuilder to which the formatted number is appended.
* If the builder is null, then a new StringBuilder will be created.
* @return the StringBuilder containing the number
*/
private StringBuilder formatDecimal(String raw, NumberStyle style, StringBuilder builder)
{
builder = setup(builder);
if (raw.indexOf('E') >= 0)
builder.append(raw); // scientific notation: give up
else
{
style = norm(style);
format(raw, style.useGrouping(), style.minDecimals(), style.maxDecimals(), builder);
}
return builder;
}
//----------------------------
// format helpers
//----------------------------
private void format(String raw, boolean grouping, int minDecimals, int maxDecimals, StringBuilder builder)
{
int length = raw.length();
int dot = raw.indexOf('.');
formatNaturalPart(raw, grouping, dot < 0 ? length : dot, builder);
if (maxDecimals > 0)
formatFractionPart(raw, dot, minDecimals, maxDecimals, builder);
}
private void formatNaturalPart(String raw, boolean grouping, int end, StringBuilder builder)
{
int length = end;
int start = 0;
if (raw.startsWith("-"))
{
start = 1;
length--;
builder.append('-');
}
if ((length > 3) && grouping)
{
String sep = getGroupingSepString();
if (sep != null)
{
int next = length % 3;
if (next == 0)
next = 3;
next += start;
builder.append(raw, start, next);
while(next < end)
{
builder.append(sep);
builder.append(raw, next, next + 3);
next += 3;
}
return;
}
}
// fallback
if (length == 0)
builder.append('0');
else
builder.append(raw, start, end);
}
private void formatFractionPart(String raw, int dot, int minDecimals, int maxDecimals, StringBuilder builder)
{
builder.append(getDecimalSeparator());
int length = raw.length();
int added = 0;
if (dot >= 0)
{
int start = dot + 1;
int decimals = length - start;
added = Math.min(maxDecimals, decimals);
builder.append(raw, start, start + added);
}
for (int i=added; i<minDecimals; i++)
builder.append('0');
}
private static NumberStyle norm(NumberStyle style)
{
return style != null ? style : NumberStyle.DEFAULT;
}
private StringBuilder setup(StringBuilder builder)
{
return builder != null ? builder : new StringBuilder();
}
//----------------------------
// parse
//----------------------------
public BigDecimal parseBigDecimal(String s) throws ParseException
{
Number n = parseNumber(s);
if (n == null)
return null;
else if (n instanceof BigDecimal)
return (BigDecimal)n;
else if (groupingSeparatorString_ != null)
{
s = s.replace(groupingSeparatorString_, "");
if (decimalSeparator_ != '.')
s = s.replace(decimalSeparator_, '.');
return new BigDecimal(s);
}
else
return new BigDecimal(n.doubleValue());
}
public BigInteger parseBigInteger(String s) throws ParseException
{
Number n = parseNumber(s);
if (n == null)
return null;
else if (n instanceof BigInteger)
return (BigInteger)n;
else if (groupingSeparatorString_ != null)
{
s = s.replace(groupingSeparatorString_, "");
return new BigInteger(s);
}
else
return new BigInteger(String.valueOf(n.longValue()));
}
public Double parseDouble(String s) throws ParseException
{
Number n = parseNumber(s);
if (n == null)
return null;
else
return (n instanceof Double) ? (Double)n : new Double(n.doubleValue());
}
public Float parseFloat(String s) throws ParseException
{
Number n = parseNumber(s);
if (n == null)
return null;
else
return (n instanceof Float) ? (Float)n : Float.valueOf(n.floatValue());
}
public Integer parseInteger(String s)
{
return StringUtil.isBlank(s) ? null : Integer.valueOf((int)parseLongValue(s));
}
public Long parseLong(String s)
{
return StringUtil.isBlank(s) ? null : Long.valueOf(parseLongValue(s));
}
private long parseLongValue(String s)
{
int length = s.length();
long result = 0;
boolean negative = false;
int i = skipSpace(s, 0, true);
char c = s.charAt(i);
if ((c == '-') || (c == '+'))
{
negative = (c == '-');
i = skipSpace(s, i+1, true);
}
while(i < length)
{
c = s.charAt(i);
if (('0' <= c) && (c <= '9'))
{
result *= 10;
result -= (c - '0');
}
else if (c != groupingSeparator_)
{
if (isSpace(c))
{
if (!isSpace(groupingSeparator_))
{
skipSpace(s, i, false);
}
}
else
throw new NumberFormatException(s);
}
i++;
}
return negative ? result : -result;
}
public Short parseShort(String s)
{
return StringUtil.isBlank(s) ? null : Short.valueOf((short)parseLongValue(s));
}
private Number parseNumber(String s) throws ParseException
{
return StringUtil.isBlank(s) ? null : impl_.parse(normNumberString(s));
}
/**
* Motivation: french numbers use \160 as grouping separator
* if such a number is entered with a space, only the part until the
* first separator is recognized as number
*/
private String normNumberString(String s)
{
if (isSpace(groupingSeparator_))
{
if (s.indexOf(' ') != -1)
s = s.replace(" ", "");
if (s.indexOf((char)0xa0) != -1)
s = s.replace("\u00a0", "");
}
return s;
}
private int skipSpace(String s, int start, boolean expectMore)
{
int length = s.length();
int i = start;
while ((i < length) && isSpace(s.charAt(i)))
i++;
if (expectMore && (i == length))
throw new NumberFormatException("unexpected end of number: " + s);
else if (!expectMore && (i < length))
throw new NumberFormatException("unexpected characters of number: " + s);
return i;
}
/**
* Returns true iif the character is 0x20 or 0xA0 (nbsp).
*/
private static boolean isSpace(char c)
{
return (c == ' ') || (c == 0xa0);
}
private final Locale locale_;
private final java.text.NumberFormat impl_;
private char decimalSeparator_ = 0;
private char groupingSeparator_ = 0;
private String groupingSeparatorString_;
}