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----- */