/* -----BEGIN PGP SIGNED MESSAGE----- */ import com.ibm.math.*; // IBM's improved BigDecimal class import java.math.BigInteger; // BigInteger class import java.text.*; // Release level 1.1 or above import java.util.*; // Locales import java.io.*; // for test code in main() only /** A class of useful methods for financial applications that use BigIntegers internally but also have to cope with floating point numbers and strings. The idea is that 100 per cent of the rounding, formatting and parsing for an entire bookkeeping or similar application should occur in one class like this one. The code is internationalized to accept and produce decimal and currency strings based on the default or specified locale of whatever application instantiates this class.
The BDFormat class must be in the same directory as this class. BDFormat does BigDecimal formatting and parsing. There is a limit of 19 digits for the integer portion of your application's numbers. You'll get a message (in English) returned if you try to use too big of a BigDecimal.
The two-parameter morphs of displayAsDecimal() and displayAsCurrency() allow right-justified output. Just specify the field size. When needed, rounding uses the method favored by bankers in which discarded fractions > .5 round the last remaining digit away from zero, and discarded fractions == .5 round the last remaining digit up or down towards the nearest *even* digit.
Right-justified output is improved so the field size you specify is the exact length of the result string (if the result fits in the field size). With multiple instances of BigCurrencyDecimal you can display (and align decimals) accross multiple currencies at the same time. Suggestions and criticism are welcome, but use at your own risk.
This version uses IBM's new BigDecimal class. The com.ibm.math package must be available. If you have "decimal.jar" from IBM alphaWorks, put an entry in your classpath pointing to it, e.g. SET CLASSPATH=%classpath%;f:\Java11\lib\decimal.jar. With Java release level 1.1 you'll also need to add the java/lang/Comparable interface (just one abstract method declaration) to your classes.zip. @author Tony Dahlman @version 1.1 rev. 98/10/20 */ public class BigCurrencyDecimal { /** The number of decimal digits required by this locale's currency. */ private int digits; /** The default rounding mode used by BigDecimal conversions. */ private static final int DEFAULT_RMODE = BigDecimal.ROUND_HALF_EVEN; /** The rounding mode actually in effect. Set it with setRoundingMode(int). */ private int rmode = DEFAULT_RMODE; /** The BDFormat class does BigDecimal formatting and parsing */ private BDFormat bdnf; /** The default constructor sets up formatting objects for the default locale. Locale-specific constants and limits are set. The FieldPosition object can find the decimal point in the decimal strings produced by NumberFormat.format()
*/ public BigCurrencyDecimal() { this( Locale.getDefault() ); } /** The locale-specific constructor sets up formatting objects for the requested locale. Use java.util.Locale class to create a Locale object. A list of 2-letter language codes is available here, and a list of 2-letter country codes is available here. Locale-specific constants are set by this constructor.
*/ public BigCurrencyDecimal(Locale loc) { NumberFormat cf = NumberFormat.getCurrencyInstance(loc); digits = cf.getMaximumFractionDigits(); bdnf = new BDFormat(loc); } /** Although the calling application will use only BigIntegers internally, calculating averages or doing currency conversions will occasionally yield floating point numbers. This method gets rid of them by converting back to an internal BigInteger object. This implements bankers rounding (as described above). */ BigInteger roundInternalBigDecimal( BigDecimal bd ) { bd = bd.setScale(0, rmode); return bd.toBigInteger(); } /** When the user or a spreadsheet supplies your application with floating point input, this method converts floats and doubles to internal BigInteger objects. */ BigInteger importExternalDouble( double dval ) { BigDecimal bd = new BigDecimal(dval); bd = bd.setScale(digits, rmode); bd = bd.movePointRight(digits); return bd.toBigInteger(); } /** When the user or a spreadsheet supplies your application with floating point input, this method converts BigDecimal objects to internal BigInteger objects. */ BigInteger importExternalBigDecimal( BigDecimal bd ) { bd = bd.setScale(digits, rmode); bd = bd.movePointRight(digits); return bd.toBigInteger(); } /** Makes a string, formatted as currency, for output or display, as in a report, journal, etc. For example, you have the BigInteger value 123456. displayAsCurency() yields the strings, $1,234.56 (en_US locale) or kr 1.234,56 (da_DK locale). In Italy where they've done away with "pennies", 123456 is dislpayed as L. 123.456 . */ String displayAsCurrency( BigInteger bi ) { BigDecimal bd = new BigDecimal(bi, digits); return bdnf.curformat(bd); } /** Makes a right-justified string, formatted as currency, for output or display as in a report, journal, etc. In this morph of the displayAsCurrency() function, you supply a BigInteger and a field size as parameters. The output will be right-justified (if possible) with decimal points aligned. We assume a monospaced font. In JDK 1.2 java.awt.font.TextLayout will open up decent, optimized, i18n methods for using prettier fonts. @returns a currency-formatted string of length fieldsize. */ String displayAsCurrency( BigInteger bi, int fieldsize ) { BigDecimal bd = new BigDecimal(bi, digits); return bdnf.curformat(bd, fieldsize); } /** Makes a string, formatted as a currency-like decimal for output or display, as in a report, journal, ledger, etc. For example you have the BigInteger -123456. displayAsDecimal() yields the strings, -1,234.56 (en_US locale) or -1.234,56 (da_DK locale). In Italy where they've done away with "pennies", -123456 is displayed as -123.456 . */ String displayAsDecimal( BigInteger bi ) { BigDecimal bd = new BigDecimal( bi, digits ); return bdnf.format(bd); } /** Makes a right-justified string, formatted as a currency-like decimal for output or display, as in a report, journal, ledger, etc. In this morph of the displayAsDecimal() function, you provide a a BigInteger and a field size as parameters. The output will be right-justified (if possible) with decimals aligned. We assume a monospaced font. In JDK 1.2 java.awt.font.TextLayout will open up decent, optimized, i18n methods for using prettier fonts. @returns a currency-like decimal string of length fieldsize. */ String displayAsDecimal( BigInteger bi, int fieldsize ) { BigDecimal bd = new BigDecimal( bi, digits ); return bdnf.format(bd, fieldsize); } /** Numeric user input, in decimal format, is converted to a double, using banker's rounding (described above) if required to achieve the locale-specified number of decimal places. Returns BigDecimal 0.0 if parsing fails. */ BigDecimal decimalStringToDecimal( String input ) { return bdnf.parse( input ); } /** Numeric user input, in currency format, is converted to a double, using banker's rounding (described above) if required to achieve the locale-specified number of decimal places. Returns BigDecimal 0.0 if parsing fails. */ BigDecimal currencyStringToDecimal( String input ) { return bdnf.parse ( input ); } /** The BigDecimal rounding mode (as an int) currently in effect. */ int getRoundingMode() { return rmode; } /** Change to a different BigDecimal rounding/truncating mode */ void setRoundingMode( int i ) { if (i>=0 && i<7) { rmode = i; } } /** Command-line test code: you can specify two arguments, the two-letter codes for language (lower case) and COUNTRY (upper case) in order to exercise this class's international features.
If you specify foreign locales, be sure to use its correct decimal separator and sign convention (e.g. parentheses for negative currency values in the en_US locale) when supplying input. A list of 2-letter language codes is available here, and a list of 2-letter country codes is available here. Even if your operating system implements unicode, all the fonts for all the countries and languages may not be available.
The prompts and text print-out here are all in English, although the numbers, currencies, sign conventions, etc. should fully implement "i18n". */ public static void main(String[] args) throws IOException, NumberFormatException { String ls = System.getProperty("line.separator","\n"); Locale defLoc = Locale.getDefault(); // set Locale from args... Locale loc; if (args.length == 2) { loc = new Locale( args[0], args[1] ); Locale.setDefault(loc); } else { loc = defLoc; System.out.println( "To try a non-default locale, supply the 2-letter language and " + ls +" country codes as two command-line args."); } System.out.println("Setting language to " + loc.getDisplayLanguage() +" and country to " + loc.getDisplayCountry() + ls); BigCurrencyDecimal cd = new BigCurrencyDecimal(loc); BufferedReader ins = new BufferedReader(new InputStreamReader(System.in)); String input; int fieldsize = 40; System.out.print("Enter a field size for right-aligned output: "); try { fieldsize = new Double( ins.readLine() ).intValue(); } catch ( NumberFormatException e ) { System.out.println("Let's just set the fieldsize to 40..."); } while (true) { System.out.print("Enter an amount: "); input = ins.readLine().trim(); if (input.length() < 2 || input.charAt(0) == 'q') System.exit(0); BigDecimal bdec = cd.decimalStringToDecimal(input); BigDecimal bcur = cd.currencyStringToDecimal(input); if (bdec.doubleValue() == 0.0) bdec = bcur; BigInteger bi = cd.importExternalBigDecimal(bdec); System.out.println( "Importing that as external or rounding it as internal yields a BigInteger:" + ls + " importExternalBigDecimal(bdec) => " + bi.toString() + ls + " roundInternalBigDecimal(bdec) => " + cd.roundInternalBigDecimal(bdec).toString() + ls + " importExternalDouble(double) => " + " importExternalDouble(double) => " + cd.importExternalDouble( bdec.doubleValue() ) + ls + "Using " + bi.toString() + " to make decimal & currency strings:" + ls + " displayAsDecimal(BigInteger) => " + cd.displayAsDecimal(bi) + ls + " displayAsCurrency(BigInteger) => " + cd.displayAsCurrency(bi) + ls + " displayAsDecimal(BigInteger,int) and displayAsCurrency(BigInteger,int) =>" + ls + cd.displayAsDecimal(bi,fieldsize) + ls + cd.displayAsCurrency(bi,fieldsize) ); if (defLoc != loc) { BigCurrencyDecimal cd2 = new BigCurrencyDecimal(defLoc); BigInteger bi2 = cd2.importExternalBigDecimal(bdec); System.out.println( " The default locale would have displayed:" + ls + cd2.displayAsDecimal(bi2,fieldsize) + ls + cd2.displayAsCurrency(bi2,fieldsize) ); } System.out.println(); System.out.flush(); } // end while() } // main() } /* -----BEGIN PGP SIGNATURE----- Version: 2.6.2 iQCVAwUBNn2MG2bsFmrW0oYFAQEDRwP+KPm9kOdMQ/9rdTWYzg4pQqvV6lgFgxgW zEXJcxDeRtl3qyHyt/5eQh45n/5TlCbxFv6FZe5gW7KrrZ2Trp8vl13G0PwYftGo Nd80T5GISYS9403uWJxEyOw2kyfHCiXU/L/324kfuR4a6HUwnOK9164c8sMDVvFq MD4NiGPL8d0= =cNSU -----END PGP SIGNATURE----- */