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