import java.math.*; // the BigDecimal class
import java.text.*; // NumberFormat, DecimalFormatSymbols classes
import java.util.*; // for Locales
import java.io.*; // for test code in main() only
/** Convert between Strings, in locale-specific currency or decimal format,
and BigDecimal objects with this class's five methods. The java.text
classes, NumberFormat and DecimalFormat, seem to implement only partial
formatting from and parsing into BigDecimal objects. Here is code to
cope with this as of JDK 1.5. Decimal-point-aligned output is supported.
Use at your own risk after looking at and understanding the source code,
of course.
A limitation of no more than 19 integer digits in my version 1.1, due to
the underlying java code in DecimalFormat seems to have disappeared. The
constructors allow you to set the rounding mode and number of decimal
places when formatting non-currency decimals.
In the current version, 1.2 January 2008, all four format()ing methods
and the parse() method were improved (and corrected==What was I thinking
ten years ago?!) And three bean-ready properties were provided with get()ters and
set()ters: 'decDigits', 'rmode' and 'loc'. Locale is read-only so it can
only be changed by a call to BDFormat's constructor.
Here's a little test routine that illustrates what BDFormat can do:
import java.util.*; // for locales
import java.io.*; // for I/O to console
public class TestBDFormat {
public static void main(String[] args) {
PrintWriter outs = new PrintWriter( new BufferedWriter(
new OutputStreamWriter( System.out ) ), true );
String dots = "........................................"; // 40 dots
String input;
if (args.length < 1)
input = "1234567890123456789.01500";
else
input = args[0];
java.math.BigDecimal bd = new BDFormat().parse(input);
Locale[] locs = java.text.NumberFormat.getAvailableLocales();
StringBuffer output = new StringBuffer(80);
for (int i=0; i < locs.length; i++) {
Locale loc = locs[i];
output.setLength(0);
output.append( loc.getLanguage() ); output.append( '_' );
output.append( loc.getCountry() ); output.append( " " );
output.append( loc.getDisplayLanguage() ); output.append(" - ");
output.append( loc.getDisplayCountry() );
int len = output.length();
// 39 spaces text + 40 spaces for number field
output.append(dots.substring( 40 - (39 - len)));
output.append(new BDFormat(loc).curformat(bd,40));
outs.println( output.toString() );
}
outs.close();
}
}
@version 1.2 2008/1/27
@author Tony Dahlman
*/
public class BDFormat {
// references to the two NumberFormat instances
/** Reference to NumberFormat for decimal formatting. */
private NumberFormat nf;
/** Reference to NumberFormat for currency formatting. */
private NumberFormat cf;
/** Marks the integer portion of formatted strings. Used for
aligning decimal points in the two-parameter versions of
format() and curformat().
*/
private FieldPosition ifp;
/** Special character (the locale's decimal separator) needed for
parsing and formatting. */
private char dsep;
/** Special character(s) needed for parsing and formatting. */
private char minus;
/** Characters before or after negatives, which may be needed
for parsing or formatting.
*/
private String negpre, negsuf;
/** Flag whether number of decimal digits is being preset. This flag
is set when one of the constructors is used which takes "int digits"
and "int rmode" as parameters.
*/
private boolean decFractionSet = false;
/** Number of decimal digits for use in non-currency format() methods.
Default number of decimal digits is 6. To set a particular number of
digits for non-currency formatting and parsing, use one of the constructors
that takes "int decDigits" and "int rmode" (BigDecimal.ROUND_HALF_UP, for example)
as parameters.
*/
private int decDigits = 6;
/** This formatter/parser's locale. */
private Locale loc = null;
/** The rounding mode for non-currency formatting */
private int decRMode = BigDecimal.ROUND_HALF_UP;
/** Number of fractional digits in this locale's currency. */
private int curDigits;
/** Rounding mode preset to banker-favored method for use by
curformat() methods. */
private int rmode = BigDecimal.ROUND_HALF_EVEN;
/** Eighty dots for use by the decimal-point aligning formatters. */
private static final String dots =
"............................................................................... "; // 80
/** Ten spaces for padding after formatting for currency */
private static final String spaces = " "; // 10
/** Eighty zeroes for adding back dropped zeroes to non-currency decimals */
private static final String zeroes =
"0000000000000000000000000000000000000000000000000000000000000000000000000000000 "; // 80
private static final BigDecimal ONE = new BigDecimal("1");
/** Default constructor: Uses the default locale to set the instance variables
* like decimal separator, negative prefix, etc..
* @throws BDFormatException - if no support for this locale from DecimalFormat
*/
public BDFormat() {
this( Locale.getDefault() );
}
/** Convenience constructor: Uses the specified locale to set the instance variables: decimal
separator, negative prefix, etc. If NumberFormat.getXXXInstance() has
given us something other than an instance of DecimalFormat, we throw
an exception.
@param loc - an instance of java.util.Locale.
@throws BDFormatException - if no support for this locale from DecimalFormat
*/
public BDFormat(Locale loc) {
this( loc, 6 );
}
/** Convenience constructor: Locale and non-currency decimal digits are specified, rounding mode
will be the default, BigDecimal.ROUND_HALF_EVEN. The Locale determines
the number of fraction digits for currency formatting.
@param loc java.util.Locale object.
@param decDigits int number of digits to right of the decimal separator.
@throws BDFormatException - if no support for this locale from DecimalFormat.
*/
public BDFormat( Locale loc, int decDigits) {
this( loc, decDigits, BigDecimal.ROUND_HALF_EVEN ); // uses default rmode, half_even
}
/** Constructor. Locale, non-currency decimal digits and the BigDecimal rounding
mode are specified. Sets the locale, number of digits for non-currency
formatting, and the BigDecimal rounding mode.
@param loc - an instance of java.util.Locale.
@param digits - for non-currency decimals, the number of decimal places.
@param rmode - one of the BigDecimal rounding modes in case you are
displaying fewer decimal places than are available from your input
to one of the format() methods.
@throws BDFormatException - if no support for this locale from DecimalFormat
*/
public BDFormat(Locale loc, int decDigits, int rmode) throws BDFormatException {
synchronized( this ) {
nf = NumberFormat.getInstance(loc);
cf = NumberFormat.getCurrencyInstance(loc);
this.loc = loc;
if( ! (nf instanceof DecimalFormat )
|| ! (cf instanceof DecimalFormat) ) {
throw new BDFormatException("BDFormat doesn't support your language/country.");
}
negpre = ((DecimalFormat)cf).getNegativePrefix();
negsuf = ((DecimalFormat)cf).getNegativeSuffix();
ifp = new FieldPosition(NumberFormat.INTEGER_FIELD);
curDigits = cf.getMaximumFractionDigits();
cf.setMinimumFractionDigits(curDigits);
nf.setMaximumFractionDigits(decDigits);
DecimalFormatSymbols dfs = new DecimalFormatSymbols(loc);
minus = dfs.getMinusSign();
dsep = dfs.getDecimalSeparator();
if( decDigits >= 0 && decDigits < 24 )
this.decDigits = decDigits;
if( rmode >= 0 && rmode < 7)
decRMode = rmode;
decFractionSet = true;
}
}
/** Convenience constructor: Uses the default locale to set some instance variables, and
causes the format() methods to display a set number of fractional
digits, using the specified rounding mode if necessary.
@param digits - for non-currency decimals, the number of decimal places.
@param rmode - one of the BigDecimal rounding modes in case you are
displaying fewer decimal places than are available from your input
to one of the format() methods.
*/
public BDFormat( int decDigits, int rmode ) {
this( Locale.getDefault(), decDigits, rmode);
}
/** An i18n-friendly parse method for creating BigDecimals from an
input string. Should successfully parse either currency strings
or regular decimal strings indiscrimately. Numbers outside the
Long.MAX_VALUE to Long.MIN_VALUE will parse but those BigDecimals
will not be formatted by this class's methods.
@param input - a string that should resemble a number in some way.
@throws BDFormatException - if unable to convert input to BigDecimal object.
*/
public BigDecimal parse( String input ) throws BDFormatException {
BigDecimal bd = null;
StringBuffer sbuf = new StringBuffer(80);
int dsepCount = 0;
// find out if positive or negative ( but doesn't recognize locale's
// minus symbol as a negpre or negsuf). Parentheses recognized as
// negative input, which seems to be a necessary hack.
boolean neg = false;
if (negpre.length() + negsuf.length() > 0) {
if( ( input.indexOf(negpre) > 0 && input.indexOf(negsuf) > 0 )
|| ( input.startsWith("(") && input.endsWith(")") ) ) {
neg = true;
sbuf.append('-'); // neg BigDecimals start with -
}
}
for (int i=0; i 1 ) {
throw new BDFormatException("BDFormat.parse() couldn't parse "+input);
}
input = sbuf.toString();
try {
bd = new BigDecimal(input);
} catch ( NumberFormatException e ) {
throw new BDFormatException("BDFormat.parse() couldn't convert "
+ sbuf.toString()
+" to a BigDecimal object." );
}
return bd;
}
/** A format method for BigDecimals. i18n-frinedly. Seems to work for numbers
well outside the range Long.MIN_VALUE to Long.MAX_VALUE.
@param bdec - a BigDecimal object.
@return a String representation of the BigDecimal
*/
public String format( BigDecimal bdec ){
bdec = bdec.setScale( decDigits, decRMode );
String temp = bdec.toString();
// Converting this string to BigInteger will lose the minus sign
// for values between 0 and -1, because BigInteger(-0) gives +0.
// So we set a flag, subtract one, then correct the result later.
boolean negZeroFlag = false;
if( temp.startsWith("-0") || temp.startsWith("-.") ) {
negZeroFlag = true;
bdec = bdec.subtract(ONE);
temp = bdec.toString();
}
// split into integer and fraction segments
BigInteger bint = null;
String fraction = null;
int decPos = temp.indexOf('.');
if( decPos >= 0 ) {
bint = new BigInteger( temp.substring(0, temp.indexOf('.') ) );
fraction = temp.substring( temp.indexOf('.') + 1 );
} else {
bint = new BigInteger( temp );
fraction = "";
}
// just format the integer segment. DecimalFormat inconsistently rounds
// and loses precision (or worse) when given a BigDecimal object.
StringBuffer sbuf = new StringBuffer(80);
sbuf = ((DecimalFormat)nf).format( bint, sbuf, ifp );
// now re-insert decimal separator and fraction segment. Note that last
// character in buffer may be neg suffix, parenthesis, currency symbol, etc.
int dspos = ifp.getEndIndex();
char dsep = ((DecimalFormat)nf).getDecimalFormatSymbols().getDecimalSeparator();
sbuf.insert( dspos, dsep );
sbuf.insert( dspos + 1, fraction );
if( negZeroFlag ) {
// sbuf.setCharAt( sbuf.indexOf("1"), '0' ); // not backwards compatible
int negZeroDex = sbuf.toString().indexOf('1');
sbuf.setCharAt( negZeroDex, '0' );
}
return sbuf.toString();
}
/** A right-justified format method for Bigecimals. Formatted
output strings are i18n-friendly. If the number to be formatted is too
large for the requested field size, the result String will be longer
than what you asked for. Output from this method and from BDFormat.curformat(
BigDecimal, fieldsize) method will generally have their decimal separators
aligned. Seems to work for numbers well outside the range Long.MIN_VALUE to
Long.MAX_VALUE.
@param bdec - a BigDecimal object.
@param fieldsize - an int representing the width in number of digits of the
desired output String.
@return - a padded String of width, fieldsize, representing the BigDecimal.
*/
public String format( BigDecimal bdec, int fieldsize ) {
bdec = bdec.setScale( decDigits, decRMode );
String temp = bdec.toString();
// Converting this string to BigInteger will lose the minus sign
// for values between 0 and -1, because BigInteger(-0) gives +0.
// So we set a flag, subtract one, then correct the result later.
boolean negZeroFlag = false;
if( temp.startsWith("-0") || temp.startsWith("-.") ) {
negZeroFlag = true;
bdec = bdec.subtract(ONE);
temp = bdec.toString();
}
// split into integer and fraction segments
BigInteger bint = null;
String fraction = null;
int decPos = temp.indexOf('.');
if( decPos >= 0 ) {
bint = new BigInteger( temp.substring(0, temp.indexOf('.') ) );
fraction = temp.substring( temp.indexOf('.') + 1 );
} else {
bint = new BigInteger( temp );
fraction = "";
}
// just format the integer segment. DecimalFormat inconsistently rounds
// and loses precision (or worse) when given a BigDecimal object.
StringBuffer sbuf = new StringBuffer(80);
sbuf = ((DecimalFormat)nf).format( bint, sbuf, ifp );
// now re-insert decimal separator and fraction segment. Note that last
// character in buffer may be neg suffix, parenthesis, currency symbol, etc.
int dspos = ifp.getEndIndex();
char dsep = ((DecimalFormat)nf).getDecimalFormatSymbols().getDecimalSeparator();
sbuf.insert( dspos, dsep );
sbuf.insert( dspos + 1, fraction );
if( negZeroFlag ) {
// sbuf.setCharAt( sbuf.indexOf("1"), '0' ); // not backward compatible
int negZeroDex = sbuf.toString().indexOf('1');
sbuf.setCharAt( negZeroDex, '0' );
}
int len = sbuf.length();
if ( fieldsize < len ) fieldsize = len + 4;
if (fieldsize > 80) fieldsize = 80;
// decimal + 3 digits + 4 spaces (for currency symbol, parenthesis, etc)
// so decimal will align across most or all currencies
int offset = fieldsize - 8 - ifp.getEndIndex();
if( offset < 0 ) offset = 0;
sbuf = sbuf.insert(0, dots.substring(80 - offset) );
// now pad the end of the string with spaces so strlen() == fieldsize
if( (fieldsize-offset-len) > 0 && (fieldsize-offset-len) <= 10 )
sbuf = sbuf.append( spaces.substring( 10 - (fieldsize-offset-len) ) );
return sbuf.toString();
}
/** A currency format method for BigDecimals. i18n-friendly.
Seems to work for numbers well outside the range Long.MIN_VALUE
to Long.MAX_VALUE.
@param bdec - a BigDecimal object.
@return - a string representing this amount of money.
*/
public String curformat( BigDecimal bdec ){
bdec = bdec.setScale( curDigits, rmode );
String temp = bdec.toString();
// Converting this string to BigInteger will lose the minus sign
// for values between 0 and -1, because BigInteger(-0) gives +0.
// So we set a flag, subtract one, then correct the result later.
boolean negZeroFlag = false;
if( temp.startsWith("-0") || temp.startsWith("-.") ) {
negZeroFlag = true;
bdec = bdec.subtract(ONE);
temp = bdec.toString();
}
// split into integer and fraction segments
BigInteger bint = null;
String fraction = null;
int decPos = temp.indexOf('.');
if( decPos >= 0 ) {
bint = new BigInteger( temp.substring(0, temp.indexOf('.') ) );
fraction = temp.substring( temp.indexOf('.') + 1 );
} else {
bint = new BigInteger( temp );
fraction = "";
}
// just format the integer segment. DecimalFormat inconsistently rounds
// and loses precision (or worse) when given a BigDecimal object.
StringBuffer sbuf = new StringBuffer(80);
sbuf = ((DecimalFormat)cf).format( bint, sbuf, ifp );
// unlike the decimal formatter, the currency formatter adds a decimal
// separator and two or three zeroes (per the locale), so delete the zeroes.
int zeroDex = ifp.getEndIndex() + 1;
// int lastZero = sbuf.lastIndexOf("0"); // not backwards compatible
int lastZero = sbuf.toString().lastIndexOf('0');
// where fractional currency values don't exist, BigDecimal setScale()
// used zero scale so there is no fraction to insert.
if( lastZero > zeroDex ) // fractional currency exists in this locale
sbuf = sbuf.replace( zeroDex, lastZero+1, fraction );
if( negZeroFlag ) {
// sbuf.setCharAt( sbuf.indexOf("1"), '0' ); //not backwards compatible
int negZeroDex = sbuf.toString().indexOf('1');
sbuf.setCharAt( negZeroDex, '0' );
}
return sbuf.toString();
}
/** A right-justified currency format method for BigDecimals. Formatted
output strings are i18n-friendly. If the number to be formatted is too
large for the requested field size, the result String will be longer
than what you asked for. Output from this method and from BDFormat.format(
BigDecimal, fieldsize) method will generally have their decimal separators
aligned. Seems to work for numbers well outside the range Long.MIN_VALUE
to Long.MAX_VALUE.
@param bdec - a BigDecimal object.
@param fieldsize - an int representing the width in number of digits of the
desired output String.
@return a padded String of width, fieldsize, representing this amount of money.
*/
public String curformat( BigDecimal bdec, int fieldsize ) {
bdec = bdec.setScale( curDigits, rmode );
String temp = bdec.toString();
// Converting this string to BigInteger will lose the minus sign
// for values between 0 and -1, because BigInteger(-0) gives +0.
// So we set a flag, subtract one, then correct the result later.
boolean negZeroFlag = false;
if( temp.startsWith("-0") || temp.startsWith("-.") ) {
negZeroFlag = true;
bdec = bdec.subtract(ONE);
temp = bdec.toString();
}
// split into integer and fraction segments
BigInteger bint = null;
String fraction = null;
int decPos = temp.indexOf('.');
if( decPos >= 0 ) {
bint = new BigInteger( temp.substring(0, temp.indexOf('.') ) );
fraction = temp.substring( temp.indexOf('.') + 1 );
} else {
bint = new BigInteger( temp );
fraction = "";
}
// just format the integer segment. DecimalFormat inconsistently rounds
// and loses precision (or worse) when given a BigDecimal object.
StringBuffer sbuf = new StringBuffer(80);
sbuf = ((DecimalFormat)cf).format( bint, sbuf, ifp );
// unlike the decimal formatter, the currency formatter adds a decimal
// separator and two or three zeroes (per the locale), so replace any zeroes,
// but keep the decimal separator.
int zeroDex = ifp.getEndIndex() + 1;
int lastZero = sbuf.toString().lastIndexOf('0');
// where fractional currency values don't exist, BigDecimal setScale()
// used zero scale so there is no fraction to insert.
if( lastZero > zeroDex ) // fractional currency exists in this locale
sbuf = sbuf.replace( zeroDex, lastZero+1, fraction );
if( negZeroFlag ) {
// sbuf.setCharAt( sbuf.indexOf("1"), '0' ); // not backwards compatible
int negZeroDex = sbuf.toString().indexOf('1');
sbuf.setCharAt( negZeroDex, '0' );
}
int len = sbuf.length();
if ( fieldsize < len ) fieldsize = len + 4;
if (fieldsize > 80) fieldsize = 80;
// decimal + 3 digits + 4 spaces (for currency symbol, parenthesis, etc)
// so decimal will align across most or all currencies
int offset = fieldsize - 8 - ifp.getEndIndex();
if( offset < 0 ) offset = 0;
sbuf = sbuf.insert(0, dots.substring(80 - offset) );
// now pad the end of the string with spaces so strlen() == fieldsize
if( (fieldsize-offset-len) > 0 && (fieldsize-offset-len) <= 10 )
sbuf = sbuf.append( spaces.substring( 10 - (fieldsize-offset-len) ) );
return sbuf.toString();
}
/** Accessor for 'decDigits' property, the number of digits to right of
the decimal separator.
@param numDigits - an int to indicate the number of decimal digits
to be displayed. For non-currency output, the resulting value is
rounded according to the current 'rmode' property. */
public void setDecDigits( int numDigits ) {
decDigits = numDigits;
decFractionSet = true;
}
/** Accessor for 'decDigits' property, the number of digits to right of
the decimal separator.
@return - an int, the number of decimal digits to be returned by
format() using the current settings.
*/
public int getDecDigits() {
return decDigits;
}
/** Accessor for 'rmode' property, one of BigDecimal's static fields for
the rounding method to use.
@param rmode - the desired rounding mode, e.g. BigDecimal.ROUND_FLOOR
*/
public void setRmode( int rmode ) {
if( rmode >= 0 && rmode < 7 )
this.rmode = rmode;
}
/** Accessor for 'rmode' property, one of BigDecimal's static fields for
the rounding method to use.
@return - an int representing the current rounding mode, e.g.,
BigDecimal.ROUND_HALF-EVEN (the default).
*/
public int getRmode() {
return rmode;
}
/** Accessor for 'loc' property, the Locale. It's "read-only" because
setting it would change everything :-)
*/
public Locale getLoc() {
// if set using the constructor, the default locale is modified...
return loc;
}
/** Accessor for 'loc' property, the Locale. It's "read-only" because
setting it would change everything :-)
@return - the name of this locale.
*/
public String getLocName() {
return loc.getDisplayName();
}
/** Command-line test code to exercise the class's parse() and the two
padded format() methods. Specify the two-letter codes for
language (lower case) and COUNTRY (upper case) on the command-line
to try out the international capability. Be sure the numbers entered
use the correct decimal and grouping separators for the selected locale.
@param args - uses the language and country codes (if both supplied,
separated by a space, otherwise just uses the default Locale.)
*/
public static void main(String[] args)
throws IOException {
BDFormat bdf = null, bdf2 = null;
BufferedReader ins
= new BufferedReader( new InputStreamReader(System.in) );
PrintWriter outs = new PrintWriter( new BufferedWriter(
new OutputStreamWriter( System.out ) ), true );
String crlf = System.getProperty("line.separator","\n");
// set locale from args...
Locale loc;
boolean usingDefaultLocale = false;
Locale defaultLocale = Locale.getDefault();
if (args.length == 2) {
loc = new Locale( args[0], args[1] );
} else {
loc = defaultLocale;
usingDefaultLocale = true;
outs.println(
"For non-default locales, supply the 2-letter language and " + crlf
+"country codes as two command-line args.");
}
outs.println("Setting language to "
+ loc.getDisplayLanguage()
+" and country to "
+ loc.getDisplayCountry() + crlf);
outs.print("For non-currency decimals, how many decimal digits should be displayed? ");
outs.flush();
int digits = new Integer( ins.readLine().trim() ).intValue();
outs.print("Which rounding mode (0 - 6):" + crlf
+"0 - UP 1 - DOWN 2 - CEILING 3 - FLOOR" + crlf
+"4 - HALF_UP 5 - HALF_DOWN 6 - HALF_EVEN ? ");
outs.flush();
int rmode = new Integer( ins.readLine().trim() ).intValue();
outs.println();
// having chosen a locale, a rounding mode, and the number of fractional
// digits to use for non-currency decimals, get a BDFormat object or two.
bdf = new BDFormat( loc, digits, rmode );
if( ! usingDefaultLocale )
bdf2 = new BDFormat( defaultLocale, digits, rmode );
// parse and format some decimal numbers...
outs.println("Don't forget to use your chosen locale's correct decimal separator!");
BigDecimal bdec;
while (true) {
outs.print("Enter a number string to parse and format: "); outs.flush();
String input = ins.readLine().trim();
if (input.length() < 1 || input.toLowerCase().charAt(0) == 'q') {
ins.close();
outs.close();
System.exit(0); // Carriage return or "q(uit)" exits loop here
}
try{
bdec = bdf.parse(input);
} catch(BDFormatException bdfe) {
outs.println("**BDFormat could not parse that input.**");
continue;
}
try{
outs.println(" Decimal: " + bdf.format(bdec, 40) );
} catch(BDFormatException bdfe) {
outs.println("**BDFormat.format() failed.**");
continue;
}
try{
outs.println("Currency: " + bdf.curformat(bdec, 40) );
} catch(BDFormatException bdfe) {
outs.println("**BDFormat.curformat() failed.**");
continue;
}
if ( ! usingDefaultLocale ) {
outs.println("In the default locale the output would be: ");
try{
outs.println(" Decimal: " + bdf2.format(bdec, 40) );
} catch(BDFormatException bdfe) {
outs.println("**BDFormat.format() failed.**");
continue;
}
try{
outs.println("Currency: " + bdf2.curformat(bdec, 40) );
} catch(BDFormatException bdfe) {
outs.println("**BDFormat.curformat() failed.**");
continue;
}
} /* endif */
outs.println();
} /* endwhile */
}
}
class BDFormatException extends NumberFormatException {
public BDFormatException( String msg ) {
super(msg);
}
}
/*
-----BEGIN PGP SIGNATURE-----
Version: 2.6.2
iQCVAwUBNn2iyWbsFmrW0oYFAQG61wQAw3EImK2Q169Hri1JPggr2EBXjm6N7wPF
Bm/Z7mMD/4Ta/0jcy2Jqp17ejuvSTRudDnR/QLuo5m9IKWwVUYhCGZAOxw+szGYz
O53nHxmjmB78b/DzGpRcxraBP3OvMkJiUmuaZtNkCMQqz9ltNedo8M2cLGv2Mg8O
o+pW01QsVnM=
=wZ/L
-----END PGP SIGNATURE-----
*/