AntiPattern Name:
FloatingPointFractions
Type: Design
Problem: We need to represent values which are fractions, but our language does not have a native type for them
Context: We need to store intervals of time, or currency. So we store "hours", or "dollars". Then someone says "hey", "our customer needs these values accurate to the quarter-hour", or "the cent". So rather than recode everything, the underlying datatype is changed to
float or
double.
Forces: Inertia,
IllusionOfSimplicity
Supposed Solution: use floating point arithmetic
Resulting Context:
Bwahaha! You fool! How many times can a modern CPU commit a roundoff error in a single second? Have you ever seen code like this:
if(Math.abs(timeA-timeB) < 1e-6) {
msg("you cannot have timeA = timeB");
}
if(amtremaining < 2.9999) {
msg("you have less than 3 dollars remaining");
}
Say it until you get it: "the number in your head is almost never the number in a floating point store.".
best practice for time and currency
Integers are perfectly accurate.
For time and currency
- always store integer values representing the lowest unit: cents, minutes or even seconds
- always name your methods and data fields so as to mention the unit. getRemainingMinutes() or getRemainingClockTicks(), never getRemainingTime().
Trust me, I've been there.
Why not just have getRemainingTime() return an instance of a class in the time type hierarchy? So it can return a Minute, or a Second, or a Picosecond... and presumably your implementation of operation on Time subclasses handles conversions as appropriate. -- AdamBerger
- Yep - this is another solution.
Appropriate Usage:
For quantities such as weight or length, the round-off error caused by picking the nearest floating-point number is insignificant compared to the normal errors in measurement. For calculations where tiny errors can rapidly build up, compute with intervals (pairs of floating-point numbers) that bracket the exact value(s). But anything that people want tallied with perfect accuracy (ie: most business values) cannot be held in a
float or
double.
- Not so. The difference is between abstract things and numbers that are actual measurements of a physical quantity. On a diagram or specification - a blueprint, for instance', ⅓ of a foo + ½ a foot + 1/6 of a foot is exactly one foot.
Applicable Positive Patterns:
- fixed-point numbers (also called FixedPrecisionIntegers ) (the "lowest unit" described above is one kind of fixed-point number). Sometimes people even use them when they are *less* accurate than floating-point numbers -- see FixedPointCartesianGeometryPrimitives .
- structures containing many small integers:
- century, year, month, day-of-month, hour, minute, second, millisecond, microsecond.
- BCD coded fractions with a fixed number of decimal places
- BCD coded fractions with a variable number of decimal places, such as java.math.BigDecimal .
- Arbitrary fractions (ratios) can be stored accurately if you store them as a pair of integers. For instance, the aspect ratio of two rectangles can be compared accurately by a.height*b.width == b.height*a.width.
- Otherwise known as Rationals. See BigInt.
There are 2 ways of thinking about a particular floating-point value:
- A given floating-point number represents one particular rational value, an integer multiple of 1/(2^N). Every time you do a calculation and store the result in a floating-point variable, that variable holds some value that is (almost always) the floating-point number that is closer to the exact result than any other floating-point number. I like the java doco for "Math.PI" - "that double-precision floating point value which is closer to Pi than any other". What surprises many people is <code>double tenth = 1.0 / 10.0;</code> also gives "that double-precision floating point value which is closer to 1/10 than any other, but still not exactly equal to 1/10.".
- A given floating point number represents an infinite number of rational (not to mention even more irrational) numbers in a small smeared-out interval between it and the "next" and "previous" floating point numbers.
In your role as developer/designer, it's generally best to assume that *every* floating point variable contains a value plus a small error, and every calculation accumulates more error. Design your program to (hypothetically) still give the accurate-enough results even with this worst-case error injected at every step, and you will be pleasantly surprised when the program calculates results with even better accuracy (in some cases, it may calculate the exact result).
As you might guess, the customer wanting the values accurate to the quarter-hour is a true story. Ten-minute intervals of an hour (1/6) does not have an accurate fp representation and you can't do computing with them unless you handle roundoff every second line.
The note about variable names, btw, is more important than it seems. Our job was redeveloping a previous system, and in that system time was displayed to the user as a decimal number of hours. So it was v. important to keep track of what the variables really contained.
Oh, and the number of hours displayed was one decimal digit (bangs head on desk). The previous system was riddled with bugs like "I had three hours credit. I did an hour, and then another hour, so I should still have an hour left - so why is it saying that I have less than an hour (.9) remaining?"
A difficulty with all this is that some databases
do implement accurate fixed-point arithmetic
NUMBER(9,2). If you pull back the values as
java.math.BigDecimal, then everything can be done right - but naive coders just whack it in a
double and expect it to work. It won't.
It is worth noting that "fixed point" is not the same as "floating point".
- Of course, there's a good reason why naive coders (and even ones who should know better) don't like replacing intrinsic types (int or double) with BigDecimal, BigInt, or anything else: There's no operator overloading. Every instance of expressions like if (a < b*c + d) {..} need to be turned into code like if (a.lessThan(b.multiply(c).add(d))){..}. I suppose (being evil) that this mess of parentheses is one way that Java has come to resemble Lisp, but I don't think that's what Greenspun or Graham has in mind. :)
I observe, as an example, that there are examples in C where
(a+b)+c != a+(b+c) Most programmers do not expect this behaviour, and therein lay many potential bugs.
An infamous mistake was made in Algol 68 to try to fix one aspect of this sort of thing; they thought it would be friendly to make A = B true if they differed by a small enough non-zero delta (e.g. if A = B + 0.0000001 then they're considered equal). Problem is that this meant that often A=B and B=C but A != C, which made things worse overall than the original issue -- the cure was worse than the disease. See SecondSystemEffect.
Quite simply, there are no true shortcuts to obviate numerical analysis, although certain good practices can help (e.g. computing with intervals that bracket the desired approximation).
("In C"? Do you mean "In every programming language that uses
IeeeSevenFiftyFour" ?)
I can't believe no one has mentioned
BankersRounding. This alone has single-handedly destroyed millions of man-hours across all languages. Try rounding 2.5 and 3.5 in you favorite language and notice the wacko results. I was praying C# would've implemented a better rounding system, but they didn't.
What wacko results? Perhaps I am getting too used to it rounding this way. Are you saying that BankersRounding *should* be the default? Or something else? (If so, what?)
In
WyCash we eventually refactored all imprecise and rounded arithmetic out. What a relief. If we saw a disagreement in the 10th decimal place someone would say, "It must be rounding." I would say, "It can't be rounding, we don't round anything." Then we would get on with tracking down the problem. Our implementation used unlimited precision integers for most numbers, but sometimes stored prices or ratios as rational numbers (an unlimited numerator over an unlimited denominator). The cost was unmeasureable in speed or space, a fact that surprised me. It seems even computationally intense business applications simply don't do enough arithmetic to justify anything short of exact. --
WardCunningham
See also:
CategoryDevelopmentAntiPattern