0

A sequence of arithmetic operations (+,-,*,/,round) are performed on only monetary values of 1 trillion dollars or less (1e12 USD), rounded to the nearest penny. What is the minimum number double-precision floating point operations mirroring these operations to result in a one-penny or more rounding error on the value?

In practice, how many operations are safe to perform when computing results when rounding double-precision numbers?

This question is related to Why not use Double or Float to represent currency? but seeks a specific example of a problem with using double-precision floating point not currently found in any of the answers to that question.

Of course, double values MUST be rounded before comparisons such as ==, <, >, <=, >=, etc. And double values MUST be rounded for display. But this question asks how long you can keep double-precision values unrounded without risking a rounding error with realistic constraints on the sorts of calculations being performed.

This question is similar to the question, Add a bunch of floating-point numbers with JavaScript, what is the error bound on the sum?, but is less-constrained in that multiplication and division are allowed. Frankly, I may have constrained the question too little, because I'm really hoping for an example of a rounding error that is plausible to occur in ordinary business.


It has become clear in the extended discussion on the first answer that this question is ill-formulated because of the inclusion of "round" in the operations.

I feel the ability to occasionally round to the nearest cent is important, but I'm not sure how best to define that operation.

Similarly, I think rounding to the nearest dollar could be justified, e.g., in a tax environment where such rounding is (for who knows what reason) actually encouraged though not required in US Tax law.

Yet I find the current first answer to be dissatisfying because it feels as if cent rounding followed by banker's rounding would still produce the correct result.

Josiah Yoder
  • 3,321
  • 4
  • 40
  • 58
  • What kind of multiplications/divisions do you want to allow? `1.00$*1.00000000001e99 - 1.00$ * 1e99`? – chtz Jan 11 '22 at 16:16
  • @chtz 1e99 is larger than a trillion, so it would not be allowed. I updated the text to clarify this just now. – Josiah Yoder Jan 11 '22 at 18:46
  • The question does not specify the floating-point format used (likely IEEE-754 binary64) or the units used (likely dollars). It is not correct that “double values MUST be rounded before comparisons”; there is no universal solution for comparing numbers that have prior rounding errors. – Eric Postpischil Jan 11 '22 at 19:07
  • @JosiahYoder But if you multiply dollars by dollars, you have square-dollars, which probably doesn't make sense. Are you saying that "other factors" should also not be larger than `1e12`? Or that no intermediate result should be larger than `1e12` dollars? Are currency conversion factors allowed? This could immediately round into the "wrong" direction. – chtz Jan 11 '22 at 19:53
  • 1
    Your question is unclear. You first define that `round` can be used in the sequence of floating-point operations, but then you state *"how long you can keep double-precision values unrounded without risking a rounding error"*. So, are intermediary round operations allowed or not? If they are, then it becomes trivial: just do a penny divided by two, round (which by banker's rounding gives you zero) then multiply by 2, that is, `round(0.01 / 2, 2) * 2`, where the second parameter tells to round to integer pennies, then it resuls zero. But that does not seem to be the spirit of your question. – Arc Jan 12 '22 at 00:08
  • Actually, this question is not a question on computer programming, but rather is a question on numerical analysis, so it belongs, most likely, to [Mathematics](https://math.stackexchange.com/). We can't even write Latex-like equations on here, so its just not worth to try to model the problem here. – Arc Jan 12 '22 at 01:40
  • @Arc Actually, I think that comment IS in the spirit of this question. Could you please turn it into an answer? – Josiah Yoder Jan 12 '22 at 15:44
  • @JosiahYoder, Real life example: It took a while in [Vancover](https://en.wikipedia.org/wiki/Vancouver_Stock_Exchange#:~:text=Rounding%20errors%20on%20its%20Index%20price,-The%20history%20of&text=In%20January%201982%20the%20index,around%2025%20points%20per%20month.) – chux - Reinstate Monica Jan 12 '22 at 15:56
  • @chux-ReinstateMonica, yes that what I was thinking too, I believe it does deserve a math.stackexchange question with stricter rounding rules. See more rounding disasters [here](https://web.ma.utexas.edu/users/arbogast/misc/disasters.html). – Arc Jan 12 '22 at 16:01
  • @JosiahYoder "Of course, double values MUST be rounded before comparisons such as ==, <, >, <=, >=, etc. " --> Disagree - but not really relevant to the issue. – chux - Reinstate Monica Jan 12 '22 at 16:01
  • "are performed on only monetary values of 1 trillion dollars or less (1e12 USD), rounded to the nearest penny" has a problem as only `xxx...xx.yy` values where `.yy` is `.00`, `.25`, `.50`, `.75` can meet that requirement with `double`. All other values are not truly _rounded to the nearest penny_, just something close. Is that OK to start with values not rounded to the nearest penny (0.01)? – chux - Reinstate Monica Jan 12 '22 at 16:21
  • Given the spirit of the question, I guess the answer from @EricPostpischil is "more correct" than mine, because it relates more to the floating-point issue. – Arc Jan 12 '22 at 16:42

3 Answers3

3

At most three.

Presumably, IEEE-754 binary64, also known as “double precision” is used.

.29 rounds to 0.289999999999999980015985556747182272374629974365234375. Multiplying by 50 produces 14.4999999999999982236431605997495353221893310546875, after which round produces 14. However, with real-number arithmetic, .29•50 would be 14.5 and would round to 15. (Recall the round function is specified to round half-way cases away from zero.)

The preceding uses rounding to an integer. Here is an example using rounding to the nearest “cent,” that is, to two digits after the decimal point. A C implementation using IEEE-754 binary64 semantics with round-to-nearest ties-to-even with this program:

#include <math.h>
#include <stdio.h>


int main(void)
{
    printf(".55 -> %.99g.\n", .55);
    printf(".55/2 -> %.99g.\n", .55/2);
    printf("Rounded to two digits after decimal point -> %.2f.\n", .55/2);
    printf("1.15 -> %.99g.\n", 1.15);
    printf("1.15/2 -> %.99g.\n", 1.15/2);
    printf("Rounded to two digits after decimal point -> %.2f.\n", 1.15/2);
}

produces this output:

.55 -> 0.5500000000000000444089209850062616169452667236328125.
.55/2 -> 0.27500000000000002220446049250313080847263336181640625.
Rounded to two digits after decimal point -> 0.28.
1.15 -> 1.149999999999999911182158029987476766109466552734375.
1.15/2 -> 0.5749999999999999555910790149937383830547332763671875.
Rounded to two digits after decimal point -> 0.57.

The real-number results of the divisions would be .275 and .575. Any ordinary tie-breaker rule for round-to-nearest would round these in the same direction (upward produces .28 and .58, downward produces .27 and .57, to-even produces .28 and .58). But the IEEE-754 binary64 results produce results rounded in different directions, one up and one down. Therefore one of the floating-point results does not match the desired real-number result regardless of which tie-breaker rule is chosen.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • 2
    I guess the OP refers to "rounded to the nearest penny" not the `round` function. So, the multiplication should round to 2 decimal places, to 14.50, not 14. – Arc Jan 11 '22 at 19:45
  • 1
    @Arc: OP explicitly lists the `round` function in the operations, in the first sentence. – Eric Postpischil Jan 11 '22 at 19:47
  • 1
    Yes, but since it's about money, and the OP cites round to nearest penny, perhaps that is a "pennywise" round operator. Note that the OP's objective is "an example of a rounding error that is plausible to occur in ordinary business". – Arc Jan 11 '22 at 19:49
  • @Arc: It is really irrelevant; the same error will occur on whatever scale whether it is round to integer dollars or round to nearest penny, with some fractions of a penny produced by an interest rate rounded to an incorrect penny amount (that is, an amount different from what would be obtained using real-number arithmetic). – Eric Postpischil Jan 11 '22 at 19:57
  • 1
    I agree that floating-point arithmetic should never be used for financial applications, but I believe the bottom line of the question is: how long before binary floating-point arithmetic breaks the ledger. – Arc Jan 11 '22 at 20:20
  • @Arc: And the answer is three operations. As I wrote, the scale is irrelevant. If you convert .0029 to `double`, multiply by 50, and round to the nearest “penny” (.01), e.g., by printing with rounding to two digits after the decimal point, the result is .14, when it ought to be .15 (if round-ties-to-away is used). I am sure I can produce similar examples using, say, bank account balances multiplied by some interest rate, with the result rounded to the nearest penny (even by a choice of rounding methods). Three operations suffice: Round decimal to `double`, multiply, round to the nearest penny. – Eric Postpischil Jan 11 '22 at 20:34
  • @Arc: Fundamentally, rounding, whether to an integer or a hundredth or to whatever the scale of a penny is, is a discontinuous operation (it **must** have a jump in the output around a rounding point in the input). By definition, any non-zero error, no matter how tiny, in the input to a discontinuous operation can cause an “erroneous” result. – Eric Postpischil Jan 11 '22 at 20:36
  • 2
    @Arc: Also, it is not true that floating-point should never be used for financial applications. E.g., for Black-Scholes evaluation and other financial modeling, it is entirely appropriate. – Eric Postpischil Jan 11 '22 at 20:37
  • 1
    I beg to disagree with your answer but, as its already warned by the "avoid" message, I'm forced to move this discussion to chat, as I has already been [instructed](https://meta.stackoverflow.com/questions/415189/very-important-discussion-deleted-from-comments-a-possible-bug-in-glibcs-mallo) by moderators. Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240976/discussion-between-arc-and-eric-postpischil). – Arc Jan 12 '22 at 00:11
  • Thank you for the update. The updates end up being exactly what I used in class (in Python) to illustrate this phenomenon to my students. – Josiah Yoder Jan 12 '22 at 20:15
  • Also, perhaps you should update the opening to state "at most two" since * 0.5 and rounding to nearest penny illustrate the phenomenon. – Josiah Yoder Jan 12 '22 at 20:18
  • 1
    @JosiahYoder: I count that as three operations. Some decimal number is converted to the floating-point format, which is one operation. Then it is divided by 2 (multiplied by .5), which is another operation. (This treats 2 or .5 as a pure constant, not obtained by a conversion from decimal.) Then it is rounded to the nearest one-hundredth, a third operation. Did you have something else in mind? – Eric Postpischil Jan 13 '22 at 21:46
1

Just do a penny divided by two, round (which, by banker's rounding gives you zero) then multiply by 2, that is,

round(0.01 / 2, 2) * 2 

where the second parameter to round tells to round to integer pennies, then it resuls zero.

Note that there have been some disasters (see also here), due to incorrect rounding, including the index crash of the Vancouver Stock Exchange.

Furthermore, note that sub-penny bookkeeping is required in some financial applications, for example, in some stock exchanges as low as $0.0001, as this filing. Some additional info in this Quantitative Finance question, and this on this site.

Arc
  • 412
  • 2
  • 16
  • Right, I corrected. – Arc Jan 12 '22 at 16:26
  • Right again! I was using Octave, which does not print as many digits as `printf`. – Arc Jan 12 '22 at 16:31
  • No, this `round` is not from C, its from Excel, but the OP did not mention a specific language. Actually I commented and flag the question, because it does not seem computer programming related to me, its more like numerical analysis. – Arc Jan 12 '22 at 16:45
  • This round also works in Python with exact copy paste :-) – Josiah Yoder Jan 12 '22 at 20:14
  • I like this solution because of the simplicity of the Python/Octave code in illustrating the phenomenon. This simplicity really helped it click for me. – Josiah Yoder Jan 12 '22 at 20:19
1

A sequence of arithmetic operations (+,-,*,/,round) are performed on only monetary values of 1 trillion dollars or less (1e12 USD), rounded to the nearest penny.

Premise has a problem as only $xxx,xxx,xxx,xxx,xxx.yy values where .yy is .00, .25, .50, .75 can meet that requirement with double. All other values are not truly rounded to the nearest penny, just something close. Let us assume money variables are always rounded to the nearest 0.01 as best they can be represented by double.

With + or - of money in the $trillion range using 1.0 as $1.00, the unit in the last place for 1.0e12 (1 trillion US $) is .0001220703125. Values that are to be to the penny could then be as much as 0.00006103515625 off or about 1/164 of a cent. It is easy to reason that adding up about 164 such values could incur a off-by-1 cent error as compared to decimal math.

With * of money, it make little sense to multiple 2 moneys, but money by a factor, say interest rate. Given an interest rate could be any double, a simple round_to_the_cent(money * rate) could readily be off by 1 cent as compared to money as a decimal.


Example off-by $0.01 with 1 multiply and 1 round

Consider a money calculation involving some M * rate that, on paper, has the product of $xxxxxx.yy5 and M and rate are not exactly representable with a double. On paper it rounds to $xxxxxx.yy0 or $xxxxxx.yy0 + 0.01. With double, it is a coin flip that it will match to the penny.

int main() {
  double money = 1000.05;
  double rate = 1.90; // 170 %
  double product = money * rate;
  printf("Decimal precise  : $1900.095\n");
  printf("Computer precise : $%.17f\n", product);
  printf("Decimal rounded  : $1900.10\n");  // Ties to even, or ties away
  printf("Computer rounded : $%.2f\n", product);
}

Output

Decimal precise  : $1900.095
Computer precise : $1900.09499999999979991
Decimal round    : $1900.10
Computer round   : $1900.09

IAC, wait a few years. Supposedly C2x will provide decimal floating point types.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • [Boost](https://stackoverflow.com/questions/28133826/c-decimal-arithmetic-libraries) already provides arithmetic with decimal floating-point types. – Arc Jan 12 '22 at 17:11
  • @arc As have various compilers and languages over the many years (at least 40 from experience and much more from history). Still look forward to its C standardization. – chux - Reinstate Monica Jan 12 '22 at 17:17
  • As well as [Python](https://stackoverflow.com/questions/20354423/clarification-on-the-decimal-type-in-python). – Arc Jan 12 '22 at 17:18
  • Doesn't [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) already provides two decimal types? I guess nobody implemented it because it sucks. Hope the [C2x](https://en.wikipedia.org/wiki/C2x) don't follow it. – Arc Jan 12 '22 at 17:20
  • [IEEE 754-2019](https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats) defines 3 decimal FP types using either of 2 encodings. Darwin pressures will eventually lead to one encoding. C2X will likely allow all sorts of decimal FP, just like it does with binary FP where IEEE 754 is not _required_. – chux - Reinstate Monica Jan 12 '22 at 17:26
  • As far as I understand the *cohort* stuff in the decimal specification is a puny means to provide significance arithmetic. Perhaps a nice idea at the time, but now we have better methods for that. – Arc Jan 12 '22 at 17:40
  • I like this answer because it does not appeal to rounding, which would strengthen the claim. But I think it needs concrete numbers to make it complete. – Josiah Yoder Jan 12 '22 at 20:20
  • Although I agree that multiplication of money does not make sense, I think it is important that the multipliers also be constrained to be decimal numbers with at most two places after the decimal -- or an argument for realistic real-world interest multipliers would also be reasonable. – Josiah Yoder Jan 12 '22 at 20:21
  • @JosiahYoder Calculations with money, besides simple money add/subtract involve tax rates beyond 2 digits like [6.875%](https://www.salestaxinstitute.com/resources/rates) and mortgage interest rates like 5.05%/year applied per month at 0.420833333...%. How about [daily compound interest](https://www.wallstreetmojo.com/daily-compound-interest/) which has a `(1 + rate/365)^365` factor in it. Consider division problems: Simply put, divide $100.00 three ways. A penny is lost someplace. – chux - Reinstate Monica Jan 12 '22 at 20:57
  • I concede that real-world decimals can go beyond two places. But 0.420833333...% is truly 5.05/12, correct? Thank you for your updated example. It complements the others nicely. – Josiah Yoder Jan 12 '22 at 21:25
  • And the example of dividing 1.15/2 to split it between two parties is exactly what I used in class earlier today. I think it illustrates the challenges well. – Josiah Yoder Jan 12 '22 at 21:26
  • But I think if I could extend your basic argument that only 164 or so such operations COULD give a rounding error without these sorts of truncations, that would also highlight the dangers -- and so a numeric example there would be much appreciated! – Josiah Yoder Jan 12 '22 at 21:28