possible rounding bug:
locale=de_CH, isocode=CHF, price=39.95
com.ibm.icu.text.NumberFormat nf =
com.ibm.icu.text.NumberFormat.getCurrencyInstance(locale);
nf.setCurrency(com.ibm.icu.util.Currency.getInstance(isoCode));
localePrice = nf.format(price);
localePrice is 40.0
switzerland does have a 5-Rappen-Rounding, but this should lead to 39.95 and not
40.0.
ICU4C fix resubmitted as 5343
Mon Dec 5 14:23:54 2005 weiv changed notes2: assign: "" to "mark", priority: "" to "critical", target: "UNSCH" to "", comments: "" to "
",
Mon Dec 5 14:23:54 2005 weiv moved from incoming to formatting
Thu Dec 8 09:46:30 2005 mark sent reply 1
Mon Dec 12 13:44:23 2005 weiv changed notes2: comments: "
" to "",
Tue Dec 13 15:18:34 2005 mark changed notes2: target: "UNSCH" to "3.6",
Tue Dec 13 15:19:45 2005 mark changed notes2: assign: "mark" to "mark, rhoten",
Fri Mar 31 13:54:41 2006 ram changed notes2: assign: "mark, rhoten" to "george",
Thu Aug 24 14:31:49 2006 grhoten changed notes2: assign: "george" to "mark", xref: "" to "5343",
Thu Aug 24 14:31:49 2006 grhoten changed notes
Thu Aug 24 14:31:49 2006 grhoten moved from formatting to fixed
Thu Aug 24 14:31:53 2006 grhoten changed notes2: review: "" to "grhoten",
Sun Oct 22 07:30:54 2006 grhoten moved from fixed to closed
I verified the bug.
The workaround for now is to use:
((DecimalFormat)nf).setRoundingMode(BigDecimal.ROUND_HALF_DOWN);
Here is test code showing the problem:
static void checkrounding(boolean useWorkaround) {
ULocale locale = new ULocale("de_CH");
String isoCode = "CHF";
com.ibm.icu.text.NumberFormat nf = com.ibm.icu.text.NumberFormat
.getCurrencyInstance(locale);
nf.setCurrency(com.ibm.icu.util.Currency.getInstance(isoCode));
if (useWorkaround) {
System.out.println("* Using work-around");
((DecimalFormat)nf)
.setRoundingMode(BigDecimal.ROUND_HALF_DOWN);
}
// use increments of 1000 to avoid accumulated rounding in test
double basePrice = 39950;
for (double delta = -30; delta <= 30; delta += 5) {
double price = (basePrice + delta) / 1000.0;
String localePrice = nf.format(price);
System.out.println(price + "\t=>\t" + localePrice);
}
}
and output:
39.92 => SFr. 39.90
39.925 => SFr. 39.90
39.93 => SFr. 39.95
39.935 => SFr. 39.95
39.94 => SFr. 39.95
39.945 => SFr. 39.95
39.95 => SFr. 40.00
39.955 => SFr. 39.95
39.96 => SFr. 39.95
39.965 => SFr. 39.95
39.97 => SFr. 39.95
39.975 => SFr. 40.00
39.98 => SFr. 40.00
Using work-around
39.92 => SFr. 39.90
39.925 => SFr. 39.90
39.93 => SFr. 39.95
39.935 => SFr. 39.95
39.94 => SFr. 39.95
39.945 => SFr. 39.95
39.95 => SFr. 39.95
39.955 => SFr. 39.95
39.96 => SFr. 39.95
39.965 => SFr. 39.95
39.97 => SFr. 39.95
39.975 => SFr. 39.95
39.98 => SFr. 40.00
The suspect code is in DecimalFormat. I don't know who did this originally, but it is tricky and needs clear comments (as well as the bugfix!)
// Handle complex cases
double ceil = Math.ceil(div);
double ceildiff = (ceil * roundingInc) - number;
double floor = Math.floor(div);
double floordiff = number - (floor * roundingInc);
switch (mode) {
case java.math.BigDecimal.ROUND_HALF_EVEN:
// We should be able to just return Math.rint(a), but this
// doesn't work in some VMs.
if (ceildiff != floordiff) {
return (Math.rint(div)) * roundingInc;
}
floor /= 2.0;
return (floor == Math.floor(floor) ? Math.floor(div)
: (Math.floor(div) + 1.0))
roundingInc;