package Framework;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.AttributedCharacterIterator;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
/**
* This class allows the formatting of numbers in a manner similar to the standard
* java number format. However, this class allows the parsing of numbers based on
* numericData as well as numbers, as well as supporting 4 different masks -- a
* positive mask, a negative mask, a zero mask and a null mask. If the negative mask
* isn't specified, the positive mask will be used. If the zero mask isn't specified,
* the positive mask will be used.
* @author Tim
*/
@SuppressWarnings("serial")
public class NullAwareNumberFormat extends NumberFormat {
/**
* The default value to display if the value is null and no template has been set
* for the null template
*/
protected static final String SYSTEM_DEFAULT_NULL_STRING = "N/A";
private static final int cNOT_SET = -1;
private String originalPattern;
/**
* Unfortunately, even though java supports a formatter with a positive format and a negative
* format, this doesn't always work. For example, it assumes that the number of characters in
* the mask after the decimal point are the same for positive and negative masks. Thus, a format
* like<pre>
* $#,##0.00;($#,##0)
* </pre>
* will confuse it. Thus, we need to store separate formatters for positive and negative values.
*/
protected DecimalFormat posFormat;
/**
* The original positive format string
*/
private String posFormatString;
/**
* The negative format mask, or null if none is specified.
*/
protected DecimalFormat negFormat = null;
/**
* The original negative format string
*/
private String negFormatString;
/**
* The zero format. This is stored as a string, since Java does something funny with
* numeric masks. For example, if the zero mask is "ZERO", it will encode this as
* ZERO0 when called with a value of 0 (ie it assumes is needs at least one digit)
*/
protected String zeroFormat;
/**
* An iterator over the attributed characters in the zero format
*/
protected AttributedCharacterIterator zeroFormatIterator;
/**
* The number of decimal places maximum in the zero format
*/
protected int zeroFormatMaximumFractionalDigits = cNOT_SET;
/**
* The null format string
*/
protected String nullFormat = SYSTEM_DEFAULT_NULL_STRING;
/**
* If the template allows a blank value which represents 0. Eg: "#"
*
* CraigM:23/07/2008.
*/
private boolean blankForZeroTemplate = false;
/**
* A template part to select. This is used to extract a particular part of the
* passed format string so an appropriate positive, negative, zero or null format
* can be created.
*/
private enum TemplatePart {POSITIVE, NEGATIVE, ZERO, NULL};
public NullAwareNumberFormat() {
posFormat = new DecimalFormat();
}
public NullAwareNumberFormat(String pattern) {
super();
setTemplate(pattern);
}
public NullAwareNumberFormat(TextData pattern) {
super();
setTemplate(pattern);
}
public void setTemplate(TextData pattern) {
setTemplate( pattern.toString() );
}
/**
* This method attempts to turn parts of patterns that were valid in forte and
* are not valid in java into their valid java equivalents. For example, 000# is
* not valid in java, but is valid in Forte (and is equivalent to 000)
* @param patternPart
* @return
*/
private String fixTemplate(String patternPart) {
if (patternPart == null) {
return null;
}
// Test for zeros followed by hashes (only)
else if (patternPart.matches("^0+#+$")) {
return patternPart.substring(0, patternPart.indexOf('#'));
}
else {
return patternPart;
}
}
public void setTemplate(String pattern) {
if (pattern.equalsIgnoreCase("INTEGER")) {
this.originalPattern = pattern = "#,##0;-#,##0";
}
else {
this.originalPattern = pattern;
}
this.posFormatString = getFormatPatternPart(pattern, TemplatePart.POSITIVE);
this.posFormat = new DecimalFormat(this.posFormatString);
this.negFormatString = getFormatPatternPart(pattern, TemplatePart.NEGATIVE);
if (this.negFormatString != null && this.negFormatString.length() != 0) {//PM:7 oct. 2008:performance
// We need to give the negative format a dummy positive mask, otherwise it will return
// it's parsed values as positive.
this.negFormat = new DecimalFormat("'D'" + this.negFormatString + ";" + this.negFormatString);
}
this.zeroFormat = getFormatPatternPart(pattern, TemplatePart.ZERO);
this.nullFormat = getFormatPatternPart(pattern, TemplatePart.NULL);
// CraigM:23/07/2008 - If they put '#' as the positive part of the template and there's no zero mask,
// use blank as the zero mask.
// TF:05/02/2009:DET-74:Changed this to handle #;-# as well as other masks that are equivalent for 0, such as
// #,### and ?,???. Note that without re-writing the standard decimal formatter, it is too hard to handle
// unusual masks such as USD#,### which should give USD when passed 0, instead of USD0. This should not be an
// issue, and if it is it can be corrected using a specific 0 mask.
this.blankForZeroTemplate = zeroFormat == null && this.posFormatString.matches("^([#?],?)+$");
// We need to get a character iterator for the zero part so we can use it if requested
if (this.zeroFormat != null && zeroFormat.length()!=0) {//PM:7 oct. 2008:performance
DecimalFormat temp = new DecimalFormat(this.zeroFormat);
temp.setMultiplier(1);
this.zeroFormatIterator = temp.formatToCharacterIterator(0);
this.zeroFormatMaximumFractionalDigits = temp.getMaximumFractionDigits();
}
else {
this.zeroFormatIterator = null;
this.zeroFormatMaximumFractionalDigits = cNOT_SET;
}
// We have to set the multiplier of the posNegFormat to 1, even for percentage fields,
// so that the behaviour matches Forte
this.posFormat.setMultiplier(1);
if (negFormat != null) {
this.negFormat.setMultiplier(1);
}
}
public TextData getTemplate() {
return new TextData(originalPattern);
}
private String getFormatPatternPart(String wholeFormat, TemplatePart part){
String[] parts = wholeFormat.split(";");
if (part == TemplatePart.POSITIVE) {
return fixTemplate(parts[0]);
}
else if (part == TemplatePart.NEGATIVE) {
if (parts.length > 1) {
return fixTemplate(parts[1]);
}
else {
// Special case: if the pattern has a positive and a negative mask is just a
// semi colon, it has a blank negative mask. Split returns the wrong result
// here if there is no null mask or the null mask is also zero
if (wholeFormat.matches(".*;;")) {
return "";
}
else {
// No negative statement, return null
return null;
}
}
}
else if (part == TemplatePart.ZERO) {
if (parts.length >=3) {
return fixTemplate(parts[2]);
}
else {
// Special case: if the pattern has a positive and a negative mask and then an
// extra semi colon, it has a blank zero mask. Split returns the wrong result
// here if there is no null mask or the null mask is also zero
if (wholeFormat.matches(".*;.*;;")) {
return "";
}
else {
// No zero statement, return null
return null;
}
}
}
else {
// Want the null pattern
if (parts.length >= 4) {
return fixTemplate(parts[3]);
}
else {
// Special case: if the pattern has a positive and a negative mask and a zero mask
// and an extra semi colon, it has a blank null mask. Split returns the wrong result
if (wholeFormat.matches(".*;.*;.*;;")) {
return "";
}
else {
return SYSTEM_DEFAULT_NULL_STRING;
}
}
}
}
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
StringBuffer _Result;
if (number == 0.0 && zeroFormat != null) {
_Result = toAppendTo.append(zeroFormat);
// If the pattern wants a ".", but we don't have one. Then put one in. CraigM: 07/04/2008
// TF:05/02/2009:DET-74:Fixed this to be specific to the format. Otherwise, strings like "$#,##0.00;($#,###)" will return the wrong result
if (this.zeroFormat.indexOf('.') != -1 && _Result.indexOf(DoubleData.getDecimalSeparatorStr()) == -1) {
_Result.append(DoubleData.getDecimalSeparator());
}
}
else if (number < 0.0 && negFormat != null){
_Result = negFormat.format(number, toAppendTo, pos);
// If the pattern wants a ".", but we don't have one. Then put one in. CraigM: 07/04/2008
// TF:05/02/2009:DET-74:Fixed this to be specific to the format. Otherwise, strings like "$#,##0.00;($#,###)" will return the wrong result
if (this.negFormatString.indexOf('.') != -1 && _Result.indexOf(DoubleData.getDecimalSeparatorStr()) == -1) {
_Result.append(DoubleData.getDecimalSeparator());
}
}
else {
_Result = posFormat.format(number, toAppendTo, pos);
// If the pattern wants a ".", but we don't have one. Then put one in. CraigM: 07/04/2008
// TF:05/02/2009:DET-74:Fixed this to be specific to the format. Otherwise, strings like "$#,##0.00;($#,###)" will return the wrong result
if (this.posFormatString.indexOf('.') != -1 && _Result.indexOf(DoubleData.getDecimalSeparatorStr()) == -1) {
_Result.append(DoubleData.getDecimalSeparator());
}
}
return _Result;
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
StringBuffer _Result;
if (number == 0 && zeroFormat != null) {
_Result = toAppendTo.append(zeroFormat);
// If the pattern wants a ".", but we don't have one. Then put one in. CraigM: 07/04/2008
// TF:05/02/2009:DET-74:Fixed this to be specific to the format. Otherwise, strings like "$#,##0.00;($#,###)" will return the wrong result
if (this.zeroFormat.indexOf('.') != -1 && _Result.indexOf(DoubleData.getDecimalSeparatorStr()) == -1) {
_Result.append(DoubleData.getDecimalSeparator());
}
}
// CraigM:23/07/2008 - If the number is 0, and the pattern is '#' then we just want a blank string
else if (number == 0 && this.isBlankForZeroTemplate()) {
_Result = new StringBuffer();
}
else if (number < 0 && negFormat != null){
_Result = negFormat.format(number, toAppendTo, pos);
// If the pattern wants a ".", but we don't have one. Then put one in. CraigM: 07/04/2008
// TF:05/02/2009:DET-74:Fixed this to be specific to the format. Otherwise, strings like "$#,##0.00;($#,###)" will return the wrong result
if (this.negFormatString.indexOf('.') != -1 && _Result.indexOf(DoubleData.getDecimalSeparatorStr()) == -1) {
_Result.append(DoubleData.getDecimalSeparator());
}
}
else {
_Result = posFormat.format(number, toAppendTo, pos);
// If the pattern wants a ".", but we don't have one. Then put one in. CraigM: 07/04/2008
// TF:05/02/2009:DET-74:Fixed this to be specific to the format. Otherwise, strings like "$#,##0.00;($#,###)" will return the wrong result
if (this.posFormatString.indexOf('.') != -1 && _Result.indexOf(DoubleData.getDecimalSeparatorStr()) == -1) {
_Result.append(DoubleData.getDecimalSeparator());
}
}
return _Result;
}
/**
* @return true if they have a template that allows blank as 0. Ie: "#".
*
* CraigM:23/07/2008.
*/
public boolean isBlankForZeroTemplate() {
return this.blankForZeroTemplate;
}
public int getMaximumFractionDigits(Double value) {
if (value == null) {
// Null values don't have a maximum fractional digits
return 0;
}
else {
double dValue = value.doubleValue();
return getMaximumFractionDigits(dValue);
}
}
/**
* Get the maximum number of fractional digits for a particular number
* @param value
* @return
*/
public int getMaximumFractionDigits(double value) {
if (value == 0 && this.zeroFormatMaximumFractionalDigits != cNOT_SET) {
return this.zeroFormatMaximumFractionalDigits;
}
else if (value < 0.0 && negFormat != null){
return negFormat.getMaximumFractionDigits();
}
else {
return posFormat.getMaximumFractionDigits();
}
}
@Override
public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) {
if (number == null) {
return toAppendTo.append(nullFormat);
}
if (number instanceof Long ||
number instanceof Integer ||
number instanceof Short ||
number instanceof Byte ||
(number instanceof BigInteger && ((BigInteger) number).bitLength() < 64)) {
return format(((Number) number).longValue(), toAppendTo, pos);
}
else if (number instanceof BigDecimal) {
return format(((BigDecimal) number).doubleValue(), toAppendTo, pos);//PM:31/07/2008:to prevent infinite recursion
}
else if (number instanceof BigInteger) {
return format(((BigInteger) number).longValue(), toAppendTo, pos);//PM:31/07/2008:to prevent infinite recursion
}
else if (number instanceof Number) {
return format(((Number) number).doubleValue(), toAppendTo, pos);
}
else if (number instanceof NumericData) {
if (((NumericData)number).isNull()) {
return format(null, toAppendTo, pos);
}
else if (number instanceof IntegerData) {
return format(((IntegerData)number).intValue(), toAppendTo, pos);
}
else if (number instanceof DecimalData) {
return format(((DecimalData)number).doubleValue(), toAppendTo, pos);
}
else if (number instanceof DoubleData) {
return format(((DoubleData)number).doubleValue(), toAppendTo, pos);
}
}
throw new IllegalArgumentException("Cannot format given Object as a Number");
}
@Override
public Number parse(String source, ParsePosition parsePosition) {
// We want to match the most specific mask. For example, if we have a positive mask of #,##0.00 and a
// zero mask of 0.0 then we want 0.0 to match the zero mask, but 0.09 to match the positive mask
int nullMatchEndIndex = -1;
int zeroMatchEndIndex = -1;
int negMatchEndIndex = -1;
int posMatchEndIndex = -1;
int startIndex = parsePosition.getIndex();
Number negResult = null;
Number posResult = null;
if (source.regionMatches(parsePosition.getIndex(), this.nullFormat, 0, this.nullFormat.length())) {
nullMatchEndIndex = startIndex + this.nullFormat.length();
}
if (zeroFormat != null && source.regionMatches(parsePosition.getIndex(), this.zeroFormat, 0, this.zeroFormat.length())) {
zeroMatchEndIndex = startIndex + this.zeroFormat.length();
}
if (negFormat != null) {
// See if we match a negative number first.
negResult = negFormat.parse(source, parsePosition);
if (negResult != null) {
negMatchEndIndex = parsePosition.getIndex();
}
else {
// Probably a positive number. Reset the error position first.
parsePosition.setErrorIndex(-1);
}
parsePosition.setIndex(startIndex);
}
posResult = posFormat.parse(source, parsePosition);
if (posResult != null) {
posMatchEndIndex = parsePosition.getIndex();
}
else {
// Probably a positive number. Reset the error position first.
parsePosition.setErrorIndex(-1);
}
if (posMatchEndIndex > -1 && posMatchEndIndex >= negMatchEndIndex && posMatchEndIndex >= zeroMatchEndIndex && posMatchEndIndex >= nullMatchEndIndex) {
// Use the positive mask
parsePosition.setIndex(posMatchEndIndex);
return posResult;
}
else if (negMatchEndIndex > -1 && negMatchEndIndex >= zeroMatchEndIndex && negMatchEndIndex >= nullMatchEndIndex) {
// Use the negative mask
parsePosition.setIndex(negMatchEndIndex);
return negResult;
}
else if (zeroMatchEndIndex > -1 && zeroMatchEndIndex >= nullMatchEndIndex) {
parsePosition.setIndex(zeroMatchEndIndex);
return Long.valueOf(0);
}
else if (nullMatchEndIndex > -1) {
parsePosition.setIndex(nullMatchEndIndex);
return null;
}
else {
// CraigM:23/07/2008 - Cater for a '#' mask and blank source
if (source.length() == 0 && this.isBlankForZeroTemplate()) {
return Long.valueOf(0);
}
// Nothing matches, must return an error.
parsePosition.setErrorIndex(startIndex);
return null;
}
}
/**
* Get the passed number as an AttributedCharacterIterator.
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(Object number) {
if (number == null) {
return super.formatToCharacterIterator(number);
}
if (zeroFormat == null && negFormat == null && number instanceof Number) {
return posFormat.formatToCharacterIterator(number);
}
if (number instanceof Number) {
if (zeroFormat != null && ((Number) number).doubleValue() == 0){
return zeroFormatIterator;
}
else if (((Number) number).doubleValue() < 0 && negFormat != null) {
return negFormat.formatToCharacterIterator(number);
}
}
else if (number instanceof NumericData) {
if (((NumericData)number).isNull()) {
return super.formatToCharacterIterator(null);
}
else if (zeroFormat != null && ((NumericData)number).doubleValue() == 0) {
return zeroFormatIterator;
}
else if (((NumericData) number).doubleValue() < 0 && negFormat != null) {
return negFormat.formatToCharacterIterator(((NumericData)number).doubleValue());
}
else {
return posFormat.formatToCharacterIterator(((NumericData)number).doubleValue());
}
}
return posFormat.formatToCharacterIterator(number);
}
public static void main(String[] args) {
NullAwareNumberFormat fnf = new NullAwareNumberFormat("$#,##0.00 CR;DB $#,##0.00;ZERO;nil");
System.out.println(fnf.format(1234.57));
System.out.println(fnf.format(-1234.57));
System.out.println(fnf.format(0));
System.out.println(fnf.format(null));
try {
System.out.println(fnf.parse("nil"));
System.out.println(fnf.parse("ZERO"));
System.out.println(fnf.parse("DB $1,234.56"));
System.out.println(fnf.parse("$1,234.56 CR"));
}
catch (ParseException e) {
e.printStackTrace();
}
}
}