From: Deborah G. <gol...@ap...> - 2006-08-29 00:25:36
|
I would like to get the ICU team's take on this analysis, and agreement on the best way to address the issue. Thanks, Deborah Begin forwarded message: > From: Jeff Hunter <jh...@ap...> > Date: August 18, 2006 9:22:08 PM PDT > To: Chris Kane <ck...@ap...>, Deborah Goldsmith > <gol...@ap...> > Cc: Candy Beymer <bey...@ap...>, Yaniv Gur <yg...@ap...>, > ao...@ap... > Subject: Re: CFNumberFormatter Rounding Modes Weirdness > > Hi, > > After digging through the ICU sources, I believe I have identified > the problem. > > ICU has 3 properties of interest: "rounding mode", "rounding > increment", and "max fraction digits". Rounding mode consists of > options like "half up", "half down", and "half even". Rounding > increment is a double that specifies the granularity of rounding. > This allows you to do things like round to the nearest .05, > although in most cases you just want to round to N decimal places, > that is, a rounding increment of 10^(-N). Max fraction digits are > the maximum number of decimal places to show. > > The interaction between these 3 properties is screwy. The ICU > documentation claims that the standard rounding increment is 0.0, > and that when the rounding increment is 0.0, then rounding is > disabled. This is not really true. That's because ICU does rounding > in multiple places. It does rounding on the double value before > converting it to text, then it does rounding on the text > representation. Unfortunately, only the former respects rounding > modes. > > Rounding of the double representation > > The rounding of the double representation only occurs if the > rounding increment is greater than 0. In that case, ICU performs > the following operations on the value being formatted: > > a = value/roundingIncrement > value = roundingIncrement * ((a - floor(a)) < 0.5) ? floor(a) : ceil > (a) > > The exact operations depend on the rounding mode. The above is for > the "half up" rounding mode. > > By default the rounding increment is zero, so unless you set a > rounding increment explicitly, the above computation never happens, > and you have no hope of getting a non-default rounding mode to > work. This was part of the problem I was having with > CFNumberFormatter. I was finally able to get CFNumberFormatter to > properly "half up" round 1.125 by first setting the max fraction > digits to 2, then setting the rounding mode to "half up", then > setting the rounding increment to 0.01. However, CFNumberFormatter > still fails to round 1.025 the way that I want, due to the floating > point precision issues mentioned by Chris. > > Rounding of the text representation > > After the above transformation is applied to the double value (or > not applied, depending on the value of the rounding increment), ICU > generates a string representation of the value. This string can > have more than the max number of fraction digits, in which case ICU > reduces the number of fraction digits to the maximum. This is where > the problem lies. When reducing the number of fraction digits in > the string representation, ICU does rounding again. However, the > method that they use to determine whether to round up or round down > always uses half even rounding, ignoring the rounding mode. By > changing this method to use half up rounding, all of my problems > went away. Here is the culprit method: > > UBool DigitList::shouldRoundUp(int32_t maximumDigits) const { > // Implement IEEE half-even rounding > if (fDigits[maximumDigits] == '5' ) { > for (int i=maximumDigits+1; i<fCount; ++i) { > if (fDigits[i] != kZero) { > return TRUE; > } > } > return maximumDigits > 0 && (fDigits[maximumDigits-1] % 2 ! > = 0); > } > return (fDigits[maximumDigits] > '5'); > } > > By changing this method to the following (and leaving the rounding > increment set to 0), I get exactly the behavior I want: > > UBool DigitList::shouldRoundUp(int32_t maximumDigits) const { > return (fDigits[maximumDigits] >= '5'); > } > > Summary thus far > > Using a rounding mode and a rounding increment, ICU will round the > floating-point representation of the value you are formatting. This > step happens only if you explicitly set a rounding increment. > > Next, using the max fraction digits, ICU will round the string > representation of the value you are formatting. However, this > rounding is always half even, even if you explicitly modify the > rounding mode. > > My app currently controls the number of decimal places shown by > using only the max fraction digits attribute. I do not set the > rounding increment or rounding mode. Thus, I have been seeing > "perfect" half even rounding. ("Perfect" meaning that the behavior > is completely understandable to the user. Since the rounding is > being done on the string representation of the value, the user > always gets understandable results as they increase and decrease > the number of decimal places shown.) I actually want to use half up > rounding in my app, so I was expecting to simply set the rounding > mode to half up and get on with my life. Unfortunately, the above > analysis shows that I can't get the behavior I want by doing this. > The only way to get ICU to respect my decision to use half up > rounding is by also using a rounding increment (something I don't > care about, I just want to show N decimal places), which subjects > me to floating point precision problems, which means my users are > baffled by the results they get when they change the number of > decimal places shown for a given value. > > What to do? > > The fact that DigitList::shouldRoundUp ignores the rounding mode is > a bug. We should get ICU to fix this (or fix it for them) and > include a fixed version of ICU in the OS. > > Do you agree with this analysis, and if so, how quickly can this > happen? > > If you have any ideas on workarounds that don't involve changing > ICU, I'd like to hear them. The workarounds must behave properly in > all locales. > > Please let me know if you have questions. > > Thanks, > Jeff > > On Aug 16, 2006, at 2:21 PM, Chris Kane wrote: > >> One thing here is that the "1.025" in the source code is not >> actually the value compiled into the test program, due to floating >> point precision issues. 1.025 requires an infinite repeating >> binary representation. The value that ends up in the test program >> is actually: >> >> 1.0249999999999999111821580299874767661094665527343750 >> >> So, in a sense, what you are asking for in the test program is for >> the double to be rounded to 3 places, then rounded again to 2 >> places. So for the test program, I think the "1.02" result is >> strictly correct. >> >> >> I would generally expect "HalfUp" to do what you want, when you >> have an exact value. If I change the value to 1.125 (1+1/8), >> which IS exactly representable, I still get "1.12". I get the >> same result on Tiger (10.4.7) and current Leopard. My guess would >> be that something down in the bowels of the value formatting is >> going wrong. >> >> >> If I add +1.0e-8 to the double (for example), then I get >> "1.13" (or "1.03" with the original value). So something like >> that would be a workaround. You will falsely round some values >> near 1.024999999999..., but after all you >are< only displaying to >> an accuracy of hundredths, and in your case, getting values like >> 1.025 "up" may be more important than rounding 1.02499999999 "up" >> unintentionally, particularly if values like 1.0249999 aren't >> likely to occur in practice. >> >> And of course, note that if "1.025" came from user input, you lost >> the "actual" value in the conversion to the double at that point. >> Even attempting to use NSDecimalNumber or something won't save you >> (so I can save you the trouble of trying), since that is also a >> floating-point precision type. >> >> >> Chris >> >> >> On Aug 8, 2006, at 9:56 AM, Jeff Hunter wrote: >> >>> Hello, >>> >>> I would like to use CFNumberFormatter to round numbers such that >>> if a 5 or greater follows the last decimal place, the number is >>> rounded up. E.g., >>> >>> 1.025 -> 1.03 >>> 1.035 -> 1.04 >>> etc. >>> >>> By default, CFNumberFormatter uses a rounding mode of >>> kCFNumberFormatterRoundHalfEven, which does not give me the >>> desired result. So I figured it would be as simple as setting the >>> rounding mode to kCFNumberFormatterRoundHalfUp, but that doesn't >>> seem to work. >>> >>> If you compile and run the attached file, which rounds the number >>> 1.025 to 2 decimal places using kCFNumberFormatterRoundHalfUp, >>> you'll see the following output: >>> >>> $ cc -Wall -framework Foundation CFNumberFormatterRounding.m >>> $ ./a.out >>> 2006-08-08 12:48:13.095 a.out[16299] 1.02 >>> >>> I am expecting to see output of 1.03. Is this a bug or am I >>> misunderstanding something? >>> >>> Thanks, >>> Jeff >>> >>> <CFNumberFormatterRounding.m> >> > |