/*
* Copyright (C) NetStruxr, Inc. All rights reserved.
*
* This software is published under the terms of the NetStruxr
* Public Software License version 0.5, a copy of which has been
* included with this distribution in the LICENSE.NPL file. */
package er.extensions.formatters;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.FieldPosition;
import java.text.Format;
import java.util.Hashtable;
import java.util.Map;
import com.webobjects.foundation.NSNumberFormatter;
import er.extensions.foundation.ERXProperties;
import er.extensions.localization.ERXLocalizer;
/**
* An extension to the number formatter. It
* will strip out the characters '%$,' when parsing
* a string and can scale values by setting a pattern like
* <code>(/1024=)0.00 KB</code> which will divide the actual value by 1024 or
* <code>(*60;4=)0.00</code> which will multiply the actual value by 60.
* When used for parsing, the resulting value will be scaled to a scale of 4.
* So when the real value is 0.0165, the display value will be 0.99 and
* when this is re-entered, the resulting value will again be 0.0165.
*/
public class ERXNumberFormatter extends NSNumberFormatter {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
/** holds a reference to the repository */
private static Map<String, NSNumberFormatter> _repository = new Hashtable<>();
protected static final String DefaultKey = "ERXNumberFormatter.DefaultKey";
static {
_repository.put(DefaultKey, new ERXNumberFormatter());
}
private String _ignoredChars = ERXProperties.stringForKeyWithDefault("er.extensions.ERXNumberFormatter.ignoredChars", "%$");
private Integer _scale;
private BigDecimal _factor;
private String _operator;
private String _stringForNegativeInfinity = "-Inf";
private String _stringForPositiveInfinity = "+Inf";
/**
* Returns the default shared instance
* @return shared instance
*/
public static NSNumberFormatter sharedInstance() {
return numberFormatterForPattern(DefaultKey);
}
/**
* @param object
*/
public static Format defaultNumberFormatterForObject(Object object) {
Format result = null;
if(object != null && !(object instanceof String)) {
if((object instanceof Double) || (object instanceof BigDecimal) || (object instanceof Float))
result = numberFormatterForPattern("#,##0.00;-(#,##0.00)");
else if(object instanceof Number)
result = numberFormatterForPattern("0");
}
return result;
}
/**
* Returns a shared instance for the specified pattern.
* @return shared instance of formatter
*/
public static NSNumberFormatter numberFormatterForPattern(String pattern) {
NSNumberFormatter formatter;
if(ERXLocalizer.useLocalizedFormatters()) {
ERXLocalizer localizer = ERXLocalizer.currentLocalizer();
formatter = (NSNumberFormatter)localizer.localizedNumberFormatForKey(pattern);
} else {
formatter = _repository.get(pattern);
if(formatter == null) {
formatter = new ERXNumberFormatter(pattern);
_repository.put(pattern, formatter);
}
}
return formatter;
}
/**
* Sets a shared instance for the specified pattern.
*/
public static void setNumberFormatterForPattern(NSNumberFormatter formatter, String pattern) {
if(ERXLocalizer.useLocalizedFormatters()) {
ERXLocalizer localizer = ERXLocalizer.currentLocalizer();
localizer.setLocalizedNumberFormatForKey(formatter, pattern);
} else {
if(formatter == null) {
_repository.remove(pattern);
} else {
_repository.put(pattern, formatter);
}
}
}
/**
* Public constructor
*/
public ERXNumberFormatter(String pattern) {
super(pattern);
}
/**
*
*/
public ERXNumberFormatter() {
}
public void setIgnoredChars(String value) {
_ignoredChars = value;
}
protected void setFactor(BigDecimal value) {
_factor = value;
}
protected void setOperator(String value) {
_operator = value;
}
protected void setScale(Integer value) {
_scale = value;
}
/**
* Overridden to search the pattern for operators and factors. The pattern should be
* <code>'(' . operatorChar . factor . [';' scale ] . '=)' normalFormatterString</code>
* @see com.webobjects.foundation.NSNumberFormatter#setPattern(java.lang.String)
*/
@Override
public void setPattern(String pattern) {
int offset = pattern == null ? -1 : pattern.indexOf("=)");
if(offset != -1) {
try {
String factorString = pattern.substring(2, offset);
int scaleOffset = factorString.indexOf(";");
if(scaleOffset >= 0) {
String scaleString = factorString.substring(scaleOffset+1);
Integer scale = Integer.valueOf(scaleString);
setScale(scale);
factorString = factorString.substring(0,scaleOffset);
}
setFactor(new BigDecimal(factorString));
setOperator(pattern.substring(1, 2));
pattern = pattern.substring(offset+2);
} catch(NumberFormatException e1) {
throw new IllegalArgumentException("ERXNumberFormatter must have a pattern like '(*1024=)#,##0.00', where 1024 is the factor.");
} catch(IndexOutOfBoundsException e) {
throw new IllegalArgumentException("ERXNumberFormatter must have a pattern like '(*1024=)#,##0.00', where 1024 is the factor.");
}
} else {
setFactor(null);
setOperator(null);
}
super.setPattern(pattern);
}
/**
* Override this in your subclass to provide for other operations when formatting a value.
* @param value
*/
protected BigDecimal performFormat(BigDecimal value) {
if("*".equals(_operator)) {
value = value.multiply(_factor);
} else if("/".equals(_operator)) {
int scale = _scale == null ? value.scale() : _scale.intValue();
value = value.divide(_factor, scale, BigDecimal.ROUND_HALF_EVEN);
}
return value;
}
/**
* Override this in your subclass to provide for other operations when parsing a value.
* @param value
*/
protected BigDecimal performParse(BigDecimal value) {
if("*".equals(_operator)) {
int scale = _scale == null ? value.scale() : _scale.intValue();
value = value.divide(_factor, scale, BigDecimal.ROUND_HALF_EVEN);
} else if("/".equals(_operator)) {
value = value.multiply(_factor);
}
return value;
}
/**
* Strips out the ignored characters and optionally performs an operation on the value
* from the string to be parsed.
* @param aString to be parsed
* @return the parsed object
*/
@Override
public Object parseObject(String aString) throws java.text.ParseException {
char[] chars = aString.toCharArray();
char[] filteredChars = new char[chars.length];
int count = 0;
for (int i = 0; i < chars.length; i++) {
if (_ignoredChars.indexOf(chars[i]) < 0) {
filteredChars[count++] = chars[i];
}
}
String filteredString = new String(filteredChars, 0, count);
Object result = super.parseObject(filteredString);
if(result instanceof Number && _operator != null) {
BigDecimal newValue = null;
if(result instanceof BigDecimal) {
newValue = (BigDecimal)result;
} else {
newValue = new BigDecimal(((Number)result).doubleValue());
}
newValue = performParse(newValue);
if(result instanceof BigInteger && !(result instanceof BigDecimal)) {
result = new BigInteger("" + newValue.intValue());
} else {
result = newValue;
}
}
return result;
}
/**
* Overridden to perform optional conversions on the value given.
* @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
*/
@Override
public StringBuffer format(Object value, StringBuffer buffer, FieldPosition position) {
if (value instanceof Number && _operator != null) {
BigDecimal newValue = null;
if(value instanceof BigDecimal) {
newValue = (BigDecimal)value;
} else {
newValue = new BigDecimal(((Number)value).doubleValue());
}
// HACK ak: if we get an integer, we add a few digits to the right,
// because the BigDecimal constructor will have a scale of zero
// FIXME: we should actually find out how many digits we need to display
if(newValue.scale() == 0) {
newValue = newValue.setScale(4);
}
newValue = performFormat(newValue);
value = newValue;
}
// handling for NaN and Infinity
if (value instanceof Double) {
Double doubleValue = (Double) value;
if (doubleValue.isNaN()) {
return buffer.append(stringForNotANumber());
} else if (doubleValue == Double.NEGATIVE_INFINITY) {
return buffer.append(stringForNegativeInfinity());
} else if (doubleValue == Double.POSITIVE_INFINITY) {
return buffer.append(stringForPositiveInfinity());
}
} else if (value instanceof Float) {
Float floatValue = (Float) value;
if (floatValue.isNaN()) {
return buffer.append(stringForNotANumber());
} else if (floatValue == Float.NEGATIVE_INFINITY) {
return buffer.append(stringForNegativeInfinity());
} else if (floatValue == Float.POSITIVE_INFINITY) {
return buffer.append(stringForPositiveInfinity());
}
}
return super.format(value, buffer, position);
}
public String stringForNegativeInfinity() {
return _stringForNegativeInfinity;
}
public void setStringForNegativeInfinity(String newString) {
if (newString == null) {
throw new IllegalArgumentException("The string for Negative Infinity must not be null");
}
this._stringForNegativeInfinity = newString;
}
public String stringForPositiveInfinity() {
return _stringForPositiveInfinity;
}
public void setStringForPositiveInfinity(String newString) {
if (newString == null) {
throw new IllegalArgumentException("The string for Positive Infinity must not be null");
}
this._stringForPositiveInfinity = newString;
}
}