/* -----BEGIN PGP SIGNED MESSAGE----- */ import java.text.*; // Release level 1.1 or above import java.util.*; // Locales import java.io.*; // for test code in main() only - STDIN, STDOUT /** A class of useful methods for financial applications that use long integer 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 system 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. Java release level 1.1 (and above) code.

The two-parameter morphs of displayAsDecimal() and displayAsCurrency() allow right-justified output. Just specify the field size. This version of CurrencyDecimal requires PaddedDecimalFormat for the methods that output decimal and currency strings. Use of that class, a generic padded-formatter, is the only significant change since the October '98 version of this class.

When needed, rounding is by 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.

Rounding is improved but may give up to a 1 cent error for amounts over $99 billion in some conditions. To completely do away with these rounding issues, use BigCurrencyDecimal and BDFormat, which are available wherever you got this.

With multiple instances of CurrencyDecimal you can display (and align decimals) accross multiple currencies at the same time. Use this freeware at your own risk, of course. @author Tony Dahlman @version 1.4 rev. 99/08/20 */ // Revisions: 1.1 - added right-justified options. // 1.2 - minor rounding bug fixed // 1.3 - fixed code for right-justified (monospace font) output so // decimals would align across currencies, and requested // field size would match the length of the result string. // Added the locale-specific constructor; removed most // class variables. 98/10/10 // 1.4 - simplified several methods by using the PaddedDecimalFormat // subclass of DecimalFormat. 99/08/20 public class CurrencyDecimal { /** Formatting object for currency-like decimals. */ private PaddedDecimalFormat nf; /** Formatting object for currency representations. */ private PaddedDecimalFormat cf; /** The number of decimal digits required by this locale's currency. */ private int digits; /** Derived from "digits", useful for shifting decimal points left and right. */ private int multiplier; /** Largest double the class can handle, individualized by locale */ private double maxval; /** Some spaces to append to formatted strings when fieldsize is specified */ private String spaces = " "; // 10 spaces /** 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 CurrencyDecimal() { this( Locale.getDefault() ); } /** The locale-specific constructor sets up formatting objects for the requested locale. Use java.util.Locale class to createa 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 and limits are set by the constructor. The FieldPosition object can find the decimal point in the decimal strings produced by NumberFormat.format()
*/ public CurrencyDecimal(Locale loc) { nf = PaddedDecimalFormat.getNumberInstance(4, loc); nf.setPadCharacter('.'); cf = PaddedDecimalFormat.getCurrencyInstance(4, loc); cf.setPadCharacter('.'); digits = cf.getMaximumFractionDigits(); nf.setFractionDigits(digits); multiplier = (int) Math.pow(10,digits); maxval = (double) Long.MAX_VALUE / multiplier; } /** Although the calling application will use only long integers internally, calculating averages or doing currency conversions will occasionally yield floating point numbers. This method gets rid of them by converting back to the internal long integer representation. This implements bankers rounding (as described above), and seems to work for numbers up to $99,999,999,999.56. (Please let me know if you find problems.) */ long roundInternalDouble( double dval) { if(dval > maxval*multiplier || dval < - maxval*multiplier ) { System.err.println("Amount is too large"); return 0; } boolean neg = false; if (dval<0) { dval = -dval; neg = true; } // when input is an *internal* double: no multiplier involved dval += 0.500000000; long temp = (long) dval; if( temp % 2 == 1 ) dval -= .01; return ( neg ? -(long)dval : (long) dval ); } /** Rarely the user or a spreadsheet will supply your application with floating point input. This converts it to the internal long integer representation. */ long importExternalDouble( double dval ) { dval = dval * multiplier; return roundInternalDouble(dval); } /** Makes a string, formatted as currency, for output or display, as in a report, journal, etc. For example, you have the long integer 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( long longval ) { double temp = (double) longval / multiplier; return cf.format( temp, 0 ); } /** 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 long integer 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( long longval, int fieldsize ) { double temp = (double) longval / multiplier; StringBuffer result = new StringBuffer( cf.format( temp, fieldsize ) ); // now pad the end of the string with spaces so strlen() == fieldsize int len = Math.min( fieldsize - result.length(), spaces.length() ); if( len > 0 ) result = result.append( spaces.substring( 10 - len) ); return result.toString(); } /** 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 long integer -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( long longval ) { double temp = (double) longval / multiplier; return nf.format ( temp, 0 ); } /** 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 long integer 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( long longval, int fieldsize ) { double temp = (double) longval / multiplier; StringBuffer result = new StringBuffer( nf.format( temp, fieldsize ) ); // now pad the end of the string with spaces so strlen() == fieldsize int len = Math.min( fieldsize - result.length(), spaces.length() ); if( len > 0 ) result = result.append( spaces.substring( 10 - len) ); return result.toString(); } /** Numeric user input, as decimal, is converted to a double, using banker's rounding (described above) if required to achieve the locale-specified number of decimal places. The formatter (NumberFormat/DecimalFormat) class implements this rounding, which is the type prescribed by IEEE754. Two discarded digits are examined so .12501 becomes .12 while .1251 becomes .13, when rounding to two retained decimal digits. */ double decimalStringToDouble( String input ) { double result = 0.0; try { result = nf.parse(input.trim()).doubleValue(); } catch ( ParseException e ) { System.err.println("decimalStringToDouble could not parse " + input ); return 0.0; } return result; } /** Numeric user input, as currency, is converted to a double, using banker's rounding (described above) if required to achieve the locale-specified number of decimal places. The formatter (NumberFormat/DecimalFormat) class implements this rounding, which is the type prescribed by IEEE754. Two discarded digits are examined so .12501 becomes .12 while .1251 becomes .13, when rounding to two retained decimal digits. */ double currencyStringToDouble( String input ) { double result = 0.0; try { result = cf.parse(input.trim()).doubleValue(); } catch ( ParseException e ) { System.err.println("currencyStringToDouble() could not parse " + input ); return 0.0; } return result; } /** Test code for the 8 methods.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 the 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 will probably not be available.
The prompts and text print-out here are all in English, although the numbers, currencies, sign conventions, etc. 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( "For non-default locales, 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); CurrencyDecimal cd = new CurrencyDecimal(); BufferedReader ins = new BufferedReader(new InputStreamReader(System.in)); String input; int fieldsize; System.out.print("Enter a field size for right-aligned output: "); fieldsize = new Double( ins.readLine() ).intValue(); while (true) { System.out.print("Enter an amount: "); input = ins.readLine().trim(); if (input.length() < 2 || input.charAt(0) == 'q') System.exit(0); double dval = cd.decimalStringToDouble(input); double cval = cd.currencyStringToDouble(input); if (dval == 0.0) dval = cval; long longval = cd.importExternalDouble(dval); System.out.println( "Importing that as external or rounding it as internal yields a long integer:" + ls + " importExternalDouble(double) => " + longval + ls + " roundInternalDouble(double) => " + cd.roundInternalDouble(dval) + ls + "Using " + longval + " to make decimal & currency strings:" + ls + " displayAsDecimal(long) => " + cd.displayAsDecimal(longval) + ls + " displayAsCurrency(long) => " + cd.displayAsCurrency(longval) + ls + " displayAsDecimal(long,int) and displayAsCurrency(long,int) =>" + ls + cd.displayAsDecimal(longval,fieldsize) + ls + cd.displayAsCurrency(longval,fieldsize) ); if (defLoc != loc) { CurrencyDecimal cd2 = new CurrencyDecimal(defLoc); long longval2 = cd2.importExternalDouble(dval); System.out.println( " The default locale would have displayed:" + ls + cd2.displayAsDecimal(longval2,fieldsize) + ls + cd2.displayAsCurrency(longval2,fieldsize) ); } System.out.println(); System.out.flush(); } // end while() } // main() } /* -----BEGIN PGP SIGNATURE----- Version: 2.6.2 iQCVAwUBN7+NfWbsFmrW0oYFAQFeCQQAnTtByqvBgo+3O3aHR297vdbn5RV/VSXF lSXxQsaw8SQK9BCRAvF/lNKMk5kTy05iYjpBfM/oTL8ZMLUBhm6E2brFmNJcWL6O PLPpIt2GX0N7OKg5XIQQHjLUiJioIGF/MdE2n9c9wVFAUfc5DGiQlvwJ85oWK7tv 8wjMRdcIL2c= =ar9E -----END PGP SIGNATURE----- */