Whenever I see someone handling currency in floats, something inside me wither and die a small death.
Whenever I see someone handling currency in floats, something inside me wither and die a small death.
Meh. When used correctly in the right circumstances it is acceptable to use floats.
Here's an example. Suppose you are pricing bonds, annuities, or derivatives. All the intermediate calculations make essential use of floating point operation. The Black–Scholes model for example requires the logarithm, the exponential, the square root, and the CDF of the normal distribution. None of that is doable without floats.
Even for simpler examples it is sometimes okay to use floats. If you only ever need to store an exact number of cents, you can totally store the number of cents in a double. Integer operations are exact using IEEE-754 double operations when they are smaller than 2^53-1 or so. There's usually no benefit of doing so, but hey it's possible.
0.3 is exactly representable in radix-10 floating point but not radix-2 FP (would be rounded to a maximum of 0.5 ulp error as seen in the title), for instance, just as 1/3 = 0.3333... is exactly representable in radix-3 floating point but neither radix-2 or radix-10 FP, etc.
> It's easy to write a program that sums up 0.01 until the result is not equal to n * 0.01.
It's not easy to do that if you use a floating point decimal type, like I recommended. For instance, using C#'s decimal, that will take you somewhere in the neighborhood of 10 to the 26 iterations. With a binary floating point number, it's less than 10.
Precisely. That's why I specified ~ 10^26 addition operations.
Many languages have no decimal support built in or at least it is not the default type. With a binary type the rounding becomes already visible after 10959 additions of 1 cent.
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
bool compare(int cents, float sum) {
char buf[20], floatbuf[24];
int len;
bool result;
len = sprintf(buf, "%d", cents / 100 ) ;
sprintf(buf + len , ".%02d" , cents % 100 ) ;
sprintf(floatbuf, "%0.2f", sum) ;
result = ! strcmp(buf, floatbuf) ;
if (! result)
printf( "Cents: %d, exact: %s, calculated %s\n", cents, buf, floatbuf) ;
return result;
}
int main() {
float cent = 0.01f, sum = 0.0f;
for (int i=0 ; compare(i, sum) ; i++) {
sum += cent;
}
return 0;
}
Result: Cents: 10959, exact: 109.59, calculated 109.60
This is on my 64 bit Intel, Linux, gcc, glibc. But I guess most machines use IEEE floating point these days so it should not vary a lot.Like commonly happens doing financial calculations, especially doing interest calculations.
But they still can't precisely represent quantities like 1/3 or pi.
No, it's not. The widely recommended way of solving this problem is to use fixed-point numbers. Or, if one's language/platform does not support fixed-point numbers, then the widely recommended way of solving this problem is to emulate fixed-point numbers with integers.
There is zero legitimate reason to use floating-point numbers in this context, regardless of whether those numbers are in base-2 or base-10 or base-pi or whatever. The absolute smallest unit of currency any (US) financial institution is ever likely to use is the mill (one tenth of a cent), and you can represent 9,223,372,036,854,775,807 of them in a 64-bit signed integer. That's more than $9 quadrillion, which is 121-ish times the current gross world product; if you're really at the point where you need to represent such massive amounts of money (and/or do arithmetic on them), then you can probably afford to design and fabricate your own 128-bit computer to do those calculations instead of even shoehorning it onto a 64-bit CPU, let alone resorting to floating-point.
Regardless of all that, my actual point (pun intended) is that there are plenty of big ERP systems (e.g. NetSuite) that use binary floating point numbers for monetary values, and that's phenomenally bad.
Currency, taxes, rebates, etc. handling is NEVER done with floating point.
Whatever you do with money you need predictable, reproducible results. It is norm that calculations are checked by software at two companies on both sides of transaction. Any discrepancies are alarms, bug reports, unhappy customers.
Every significant operation is exactly specified with rounding rules, etc.
For card payments and especially on terminals usually BCD is used.
For everything else usually some kind of arbitrary length decimal library (BigInteger, BigDecimal).
Nonsense. I’ve seen real banking code at reputable banks that uses floats.
> Whatever you do with money you need predictable, reproducible results.
Floats aren’t random. They’re perfectly deterministic, predictable and reproducible. If you do the same operation in two places you get the same result.
When people talk about non-determinism of floating point, what they usually mean is non-associativity, that is (x+y)+z may not be exactly equal to x+(y+z).
Good example of this, in Python 3:
>>> (0.1 + 0.2) + 0.3
0.6000000000000001
>>> 0.1 + (0.2 + 0.3)
0.6It's not an optimisation if it changes the result! And if you use non-standard flags that's your problem.
MP3 is an optimization of WAV, yet it changes the result.
Some applications are ok with reducing precision of calculations because they are not sensitive enough to small inaccuracies or they take effort to control inaccuarcies.
For example, graphics applications are typically heavy in FP calculations and yet they tend to not care much about precision and much more about performance. For those applications reducing accuracy for slight performance increase is likely win.
That's not exactly true in real hardware, or at least it wasn't until ~10 years ago. With the x87 FPU, internal precision was 80 bits, while the x86 registers were at most 64 bits. So, depending on the way the program would transfer data between the CPU and FPU your could get different results. It is very likely that different compilers and different optimization decisions could change the way these operations were implemented, so you would get slight differences between different versions of the software.
There are/were also several global FP flags that could get changed by other programs running on the same CPU/FPU that could impact the result of calculations. So, if you want 100% reproducible FP, you would have to either audit all software running on the same machine to ensure it doesn't touch those flags, or set the flags yourself for every FP calculation in your your program.
Wonder if JS does something similar or not.
Also, changes applied to the FP co-processor by other processes on the machine could impact your process, regardless of your own compilation settings.
That's ancient history. Compilers don't use that instruction set any more in normal operation.
GCC, Java, LLVM, etc, will normally emit SSE2 in order to be standards compliant. They will only relax this if you tell them to, then it's your problem.
I believe there is still quite a bit of cautionary discussion of floating point numbers that was written in the age of the x87, so it's important to understand that people were not just misunderstanding IEEE754, even though their concerns are no longer applicable to modern hardware.
That's something FP does not provide and it makes it completely unusable for accounting.
That seems like a completely arbitrary requirement. Do accounting laws prohibit sort? Does 1 + 1 have to equal green on Tuesdays?
Each operation is accounted on two opposite sides of various account in a way that always keeps sides balanced (ie. they must sum up to the same value).
When you go to your bank account, for example, you have various sums on both sides of your account. Yet when you sum them up they MUST agree or you will be crying blood and suing your bank.
Secondly, on mainstream implementations, strictfp is already documented the same as default! They're planning to remove it anyway as it's a no-op in almost all cases.
See JEP 306.
If you use it. Which is not the default. Your original claim remains false.
Poor souls that use FP for accounting are scourge of the industry and source of jokes.
For anything imprecise and scientific, doubles will normally work well.
Accounting rules regarding truncation and rounding as specified, seems unaccounted for by most until they meet such stringent reqs.
That said, yeah, when working with money in situations where money matters, some sort of decimal or rational datatype should be the rule, not the exception.
Unless you’re doing, what, massively parallel GPU algos on batches of independent amounts? But even then you could use the float as an int in that way... Honestly when is float ever actually good for money? Not for speed, not for correctness, ...
More typically, mills[1] (tenth of a cent).
https://aws.amazon.com/emr/pricing/
Azure has some hourly prices with ten-thousandths of a cent ($0.0102/hour):
https://azure.microsoft.com/en-ca/pricing/details/virtual-ma...
Microsoft should use gas station 9/10 pricing conventions to just barely undercut Amazon's lowest price $0.011 with $0.0109.
https://www.marketplace.org/2018/10/11/why-do-gas-prices-end...
>“They found out that if you priced your gas 1/10 of a cent below a break point, let’s say 40 cents a gallon, ‘.399’ just looked to the public like 39 cents…”
A gentleman and a scholar.
(I do some work with predictive simulations about money, but outside finance, and there we care that the result has accurate order of magnitude. Floats were used extensively in the project; I actually upgraded them to doubles for the sake of handling larger order of magnitude spans.)
Performing arithmetic operations against money in floating point is the dangerous part, as error can accumulate beyond an atomic unit.
Well, it's not just a display issue. In accounting, associativity and commutativity are important. People do care that `a + b + c - a == c + b` should evaluate to “true”.
A good example of this is trying to compute the sales tax on $21.15 given a tax rate of 10%. The exact answer would be $2.115, which should round to $2.12.
IEEE 64-bit floating point gives 2.1149999999999998, which is hard to get to round to 2.12 without breaking a bunch of other cases.
Here are three functions that try to compute tax in cents given an amount and a rate, in ways that seem quite plausible:
def tax_f1(amt, rate):
tax = round(amt * rate,2)
return round(tax * 100)
def tax_f2(amt, rate):
return round(amt*rate*100)
def tax_f3(amt, rate):
return round(amt*rate*100+.5)
On these four problems: 1% of $21.50
3% of $21.50
6% of $21.50
10% of $21.15
the right answers are 22, 65, 129, and 212. Here are what those give: tax_f1: 21 65 129 211
tax_f2: 22 64 129 211
tax_f3: 22 65 130 212
Note that none of the get all four right.I did some exhaustive testing and determined that storing a money amount in floating point is fine. Just convert to integer cents for computation. Even though the floating point representation in dollars is not exact, it is always close enough that multiplying by 100 and rounding works.
Similar for tax rates. Storing in floating point is fine, but convert to an integer by multiplying by an appropriate power of 10 first. In all the jurisdictions I have to deal with, tax rate x 10000 will always be an integer so I use that.
Give amt and rate, where amt is the integer cents and rate is the underlying rate x 10000, this works to get the tax in cents:
def tax(amt, rate):
tax = (amt * rate + 5000)//10000
return tax
I'm not fully convinced that you cannot do all the calculations in floating point, but I am convinced that I can't figure it out.Your issue is on how to print the float, not with the precision of fp. For instance, `21.15 * 0.1` can be print both as 2.115 or 1.12 depending on how many decimal digits of precision you set your print function. I manage to get those results with printf using `%.3f` and `%.2f`, respectively.
To produce one cent (0.0x) error with the default FP rounding, it takes more than 1 Quadrillion of operation. Each operation can only introduce 1*10^17/2 error.
The "you shouldn't be using float to do monetary computation" is likely one the most spread float point misinformation.
The issues with your others examples is that you are rounding the data (therefore, discarding information). If you don't do any manual round, the result should be correct (I haven't test thought).
I get 2.115 with %.3f and 2.11 with %.2f. Here's my test program. Same result on my Mac with clang and my Debian 8 server with gcc.
#include <stdio.h>
double tax_on(double amt, double rate);
int main(void)
{
double amt = 21.15;
double rate = 0.1;
double tax = tax_on(amt, rate);
printf("%.3f\n", tax);
printf("%.2f\n", tax);
return 0;
}
double tax_on(double amt, double rate)
{
return amt * rate;
}That some monetary calculation works out to $2.115 (and is left that way) instead of being correctly rounded $2.12 doesn't add up to a valid argument against using floating-point for money.
I think piadodjanho does have a point there in the grandparent comment; "don't use floating-point for money" may just be a repeated mantra that doesn't entirely hold water. If extremely accurate engineering and scientific calculations can be done with floating-point, surely we can get floating-point values to measure stacks of pennies with the proper care in the programming.
That was for a long time my position. I definitely have commented before either here or in /r/programming to the effect that floating point is fine for money as long as you are aware that it is not exact and not associative, and take that into account when doing your calculations.
Any intermediate result in a calculation chain might be off a tiny amount from the exact value, but if you just rounded to the nearest 0.01 before you accumulated enough error to not < 0.005 off, you'd be fine.
I think that's probably true for addition of money amounts. If you have a large number of costs to add up, for example, you should be able to add thousands of them, round to nearest 0.01, and get the right result.
But for tax calculations, such as 10% of $21.15, 0.1 x 21.15 = 2.1149999999999998 in 64-bit IEEE floating point, and rounding the nearest 0.01 gives 2.11, not the 2.12 that we want. A call to fesetround(FE_UPWARD) makes that come out 2.115, and then rounding to the nearest 0.01 gives the desired 2.12.
Will FE_UPWARD make this work for all amounts and tax rates, or are there amounts and rates where we need FE_TONEAREST or FE_DOWNWARD? If so, how do we tell which one we need? Like I said earlier:
> I'm not fully convinced that you cannot do all the calculations in floating point, but I am convinced that I can't figure it out.
PS: calculating tax in cents given double amt, rate, using this method:
tax = amt * rate;
cents_tax = round(100 * tax);
almost works if the rounding mode is FE_UPWARD. For all amounts from 0.01 through 99.99, and all tax rates from 0.01% through 10.99% in increments of 0.01% it works except for 3.75% of $67.60 and 7.5% of $33.80.And in run-of-the-mill, everyday finance, there simply isn't enough calculation stuffed in between the concrete monetary points that are recorded in the ledger.
> If you have a large number of costs to add up, for example, you should be able to add thousands of them, round to nearest 0.01, and get the right result.
Exactly.
> But for tax calculations, such as 10% of $21.15, 0.1 x 21.15 = 2.1149999999999998 in 64-bit IEEE floating point, and rounding the nearest 0.01 gives 2.11, not the 2.12 that we want.
This problem will be there even if we use integers for the currency amounts, but floating-point only for these fractional calculations.
Luckily for us Canadians, I'm pretty sure the Canada Customs and Revenue Agency won't care which way you call this rounding. They also don't collect or refund overall discrepancies of less than around two dollars in a single tax return. I think I've been mostly rounding taxes down over the years, and tax credits up. E.g. if a tax credit is $235.981..., I make it 235.99.
The myth that has been foisted on programmers is that if you use floating-point for numbers, the actual ledgers won't balance, and sum totals of columns of figures will appear incorrect if verified by pencil-and-paper arithmetic. That will certainly be true if the math is done very carelessly; and it's true that it's easier to get it right with less care using integers.
A percentage calculation whose rounding is called the wrong direction will, in and of itself, not cause such a problem. E.g. if we split some sum of money into two complementary percentages, we can do it such that the two add up to the original.
You have to be careful not to do this as two independent percentages. Like, dont take 10% of 21.15 and then 90% of 21.15, individually round them to a penny, and then expect them to add up to 21.15. It has to be centround(21.15 - centround(.1 * 21.15)) to get the 90% residue.
def tax_f4(amt, rate):
tax = round(amt * rate * 1000)
return tax // 10 + (tax % 10 > 4)However, for 10.14% of $21.15, it gives 215, but it should be 214. Another example is 3.5% of $60.70, for which it gives 213 but correct is 212.
import math
def tax_f5(amt, rate):
t = amt * rate * 1000
return round(t) // 10 + ((math.fmod(t, 10.0) - 5.0) > -1e-7)
But then since there's now an epsilon, it raises the question of how many digits of precision the tax rates typically need. This is indeed a difficult problem. unsigned long tax = (unsigned long)round(amt * rate * 1000000);
return tax/(10000) + (fmod(tax, (double)(10000)) - (double)(5000) > -1e-5 ? 1 : 0);
(Yes, I see that I goofed in translation your code to C and typed -1e-5 instead of -1e-7. It looks like the results are the same with -1e-7).I also tested that up through $9999.99 with taxes up to 12%, and no problems.
Adding another 0 to the 1000000, the two 10000's, and the 5000 works. And another, and another. Past that it starts to fail, but not the simple off-by-one failures you get when you don't use enough digits. These are way way off, so I'm guessing its running into some new class of problem. I haven't looked to see what that is yet.
Imagine you work at a hedge fund, and you have a model that predicts the true value of some option. Assume the option is trading for $3.00. You do not really care if your model spits out $3.5 or $3.5000000001, you are going to buy either way. And your model probably involves a bunch of transcendental functions or maybe even non-deterministic machine learning, so it's not really meaningful to expect it to be “exact” to some decimal or even rational value.
Even more saliently, you probably don't care whether your model outputs 2.9999999 or 3.000000 or 3.000001, either, because in any of those cases the actual correct interpretation is “we’re just not sure whether to buy or not”.
I think a good first-order characterization of domains where floating point can safely be used is “when the difference between < and <= is not very meaningful” (in calculus terms: when “how meaningful is a difference of `x`” is a continuous function of `x`).
Yes, they could have used fixed point. I am guessing that what happened is that someone who had thought way more deeply about this than I ever needed to (I worked on the accounting side, where, yep, we always used decimals) either determined that, where the modeling was concerned, floating point errors were not worth worrying about, or estimated that the expected cost to the company stemming from bugs due to to fixed point math being easier to goof up on would have been smaller than the expected cost to the company due to floating point error.
If you are working with such huge numbers, 0.1 cents is probably a cost you are willing to pay to avoid expending thousands in a software solution. The saving with power saving using a floating point is likely greater than power your computers will have to expend to get a precise solution.
fn main() {
let x: f64 = 9007199254740992.0;
assert_eq!(x + 1.0, x);
}When adding numbers with large magnitudes differences (around 10^17 I think) it might exceed the format precision. I should have taken that in account when defining the error boundaries.
In dollars, you start having issues with cents when working with a tens of trillions.
For the vast majority of people this won't be an issue.
I'm too lazy to figure out a specific example, but sets of numbers where doubles round up and decimals round down (or vice versa) aren't terribly uncommon.
Most people don't realize that the IEEE-754 single precision floating point represent real numbers with 9 decimal digits (or 23 binary digits). The double, on the other hand, represents the real numbers with 17 decimal digits.
This means that the double error UPPER BOUND is (0.00000000000000001)/2 per operation. But in reality the error is lower because of the rounding operations.
Also, it is posssible to extend the range using denormals, but most (all?) compilers disable them when compiling with anything other than O0 to avoid performance degradation.
The overheads associate with dealing with non-float types for most applications might not be worth it the cost and risk. If course, if the language are working with provides a currency type, go for it. But if doesn't , there is no need to worry.
$ ruby -e 'pp 87e12 + 0.01'
87000000000000.02
If you're certain that your software will never handle national-economy-scale or hyperinflationary use cases, then sure, you may be able to get away with 64-bit floats, but I think "no need to worry" is overstating your case. Please do worry about precision until you've proven you don't need to.For the vast majority of person, there is no need to worry so much about using fp to represent currencies. There are other issues with float that will bite you in your back before precision became one of them.
The problem is that rounding is kind of a big deal in certain financial contexts, and the process of rounding can greatly magnify floating point's decimal precision problems when you're dealing with numbers that are close to the .5's.
When I said up above that there are some contexts where IEEE floats are fine, those contexts are largely ones where you never have to round, or where you can guarantee that an accountant is never going to see or care how you rounded. So, to an approximation: Go ahead and fearlessly implement the Black-Scholes formula using doubles, but never, ever use them to do something simple like calculating an invoice.
I really see no reason to use any other representation for currency than decimal fixed point. Store the amount as mils or whatever unit suits your use case.
No, they don't. They merely can be converted back to decimal with those numbers of significant digits without loss of information.
That is important because (a) if this matters, you have to make sure you actually control the number of significant digits when converting to decimal, or you might end up with a different decimal, and (b) the operations that you do on the floats do not reliably behave as if there was the supposedly represented decimal number stored in them.
Now, sure, you can use floats for currency, if you know what you are doing, but the point of the warning against it is that you have to know what you are doing, and chances are you don't, or if you do, then you know where you can ignore it anyway.
(That is, unless you mean nothing more than that you can encode the information contained in an n-digit decimal in a float/double--which of course is true, but not particular to floating point numbers, as any state with a certain number of bits can, of course, encode any information of no more than that many bits, somehow.)
Floating Points are hard. There is a study done with academics that shows that even researches that works with float point everyday forget about the format intricacies. And the study didn't even look into the compiler mess.
But I agree with you, some (a lot?) of caution is needed when working with float point is good.
I worked with a CSV containing, among other things, phone numbers. A coworker called and complained that the phone numbers were all wrong. He'd edited the thing in MS Excel, which promptly converted the phone numbers to floating point with a loss in precision. When he saved it, those new numbers were happily written back to the disk.
There can be two companies with 100M market cap. Corp A has issued 10M shares @ 10 each, Corp B has 10B shares priced at 0.01
A +/-0.001 change in Corp A share price is just 0.01% and moves the market cap by +/-10k, so probably nothing significant. The same nominal change in Corp B amounts to 10%, or +/- 10M in the company value, which is quite a big deal.
Also I think there may be some money to be made in changes at the 7th decimal place with large enough volume of high frequency transactions.
In trading, it's super common to use floating point arithmetic for decision logic since it's very fast and straightforward to write. The actual trade execution, however, almost always relies on integer arithmetic because then money is actually being used (and hence must be tracked properly).
It's not therefore inherently incorrect to do currency conversions with floats in some situations provided that the actual transaction execution relies on fixed precision or decimal arithmetic.
He had decades of experience in the software development industry and I got the feeling that he'd seen the effect of this issue personally.
I still remember that warning well.
That is not correct. Stock settlement transactions often list four decimal places.
That's not a significant difference compared to two decimal places, so brundolf's point still stands. There's no need for arbitrary precision.
Just store all dollars in PIPs so 5$ will be stored as 50000.
http://blogs.reuters.com/ben-walsh/2013/11/18/do-stocks-real...
I guess it's time for someone to write an "Assumptions Programmers make about money" post.
Additionally, fractional cents are often presented to the consumer when purchasing gas/fuel.
i.e. they track your account balance to more than 2 digits, they just only show you 2 digits.
It goes without saying that half pennies are dead. A mill seems to be from the Coinage Act of 1792, which is perhaps a tad outdated.
> A mill ... is perhaps a tad outdated.
Yet you use mills every time you pay for gas. Pointless in that case? Probably. Still used all the time? Certainly.
All 10 of them?
EDIT: Just a note, there's nothing special about the number 100000; pick the largest exponent of 10 that you can get away with a reasonable assurance that no overflow is possible. For a vast majority of money applications, I seriously doubt you're going to be hitting the limits of int64, so you could probably even get away with something like 1000000000.
Edit: And they forbid equality comparisons for rationals. For some reason even >= is not allowed.
For instance if one needs to apply discounts, add taxes, split in equal parts, all of the above one after the other, there will be a more precise intermediary representation before rounding everything in a way that keeps the total amount consistent with the original amount.
COBOL has a built in fixed point integer type, which makes defining a 4 digit decimal and doing math on it easy. (IBM designed it from the ground up to cater to people with a lot of money, who spend a lot of money, to work with lots of money, ie banks) Java has the BigDecimal type, which is a class in the class library, which means you need to import it. And because Java lacks operator overloading, doing calculations is tedious.
In the 90s, there was a huge push to replace COBOL with <something else>, and Java was the Rust of its day, so that's what everyone got behind. However, 4 digit COBOL decimals apparently round differently than 4 digit Java BigDecimals, so all the tests failed. And all the stuff like a\x+b had to be written like BigDecimal.add(BigDecimal.multiply(a,x),b) so development was taking forever.
Eventually they said "fuck it" and 20 years later we're still stuck with COBOL and everyone who remembers the original death march says "never again".
I have a feeling a lot of the problems came down to computer science people thinking money has two decimal digits but domain knowledge people knowing it has four. We programmers, as a group, make a lot of assumptions about other peoples' domains and we're wrong a lot*.
What do you mean this person has no surname? That's unpossible, surname is never null, error error.
Hm, are you sure? I don't believe "rational" types which encode numbers as a numerator and denominator are typically used for currency/money.
If they were, would the denominator always be 100 or 1000? I guess you could use a rational type that way, although it'd be a small subset of what rational data types are intended for. But I guess it'd be "safe"? Not totally sure actually, one question would be if rounding works how you want when you do things like apply an interest percentage to a monetary amount. (I am not very familiar with rational data types, and am not sure how rounding works -- or even if they do rounding at all, or just keep increasing the magnitude of the denominator for exact precision, which is probably _not_ what you'd want with currency, for reasons not only of performance but of desired semantics).
You are correct an IEEE-754 floating point type is inappropriate for currency. I believe for currency you would generally use a fixed-point type (rather than floating point type), or non-IEEE "arbitrary precision floating point" type like ruby's BigDecimal (ruby also offers a Rational type. https://ruby-doc.org/core-2.5.0/Rational.html . This is a different thing than the arbitrary-precision BigDecimal. I have never used Rational or seen it used. It is not generally used for money/currency.) Or maybe even a binary-coded decimal value? (Not sure if that's the same thing as "arbitrary-precision floating point" of ruby's BigDecimal or not).
There are several possible correct and appropriate data encodings/types for currency, that will have the desired precision and calculation semantics... I am not sure if rational data type is one of them, and I don't believe it is common (and it would probably be much less performant than the options taht are common). Postgres, for instance, does not have a "rational" type built in, although there appears to be a third-party extension for it. Yet postgres is obviously frequently used to store currency values! I believe many other popular rdbms have no rational data type support at all.
I'm not actually sure what domains rational data types are good for. Probably not really anything scientific measurement based either (the IEEE-754 floating point types ARE usually good for that, that is their use case!) The wikipedia page sort of hand-wavily says "algebraic computation", which I'm not enough about math to really know what that means. I have never myself used rational data types, I don't think! Although I was aware of them; they are neat.
For currency, business side should decide the rules (* 100 or * 1000000), and where to funnel the pennies ;) Fixed-point has it's own sort of gotchas, ie. multiplication, power, division, sqrt, etc. So there are fancy techniques to work with the numbers, like https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
If you're not a bank or similar but just dealing with currency to buy and sell things like for ecommerce, the default semantics of a fixed point type (like postgres 'money' type) or "arbitrary precision floating point" type like ruby's BigDecimal or are probably good enough, and just fine in a way that IEEE-754 floating point definitely are NOT. And probably don't require any additional business side decisions or involve any significant gotchas. Just using them instead of IEEE-754 floating point and not thinking too hard after that is probably just fine.
https://ruby-doc.org/stdlib-2.5.1/libdoc/bigdecimal/rdoc/Big...
https://www.postgresql.org/docs/9.6/datatype-money.html
If you ARE a bank or something similar -- I wouldn't know, I haven't done that! A relevant question: Am I concerned with specifying exactly how fractional pennies get rounded?
There are accounting, balancing, laws, regulations and reconciliation issues where for the really serious stuff, you use whatever fit spec and requirements, not the other way around. Ruby's BigDecimal will be fine, if you implement the detailed specification about how to calculate each operation every step of the way, with designated precision at various steps, together with truncations along the way that may not make much sense to the developer (or anyone else, but are required to get correct numbers).
Point is, sometimes other parties need to be able to replicate the exact numbers, unrelated to any internal library or coding standards. Code using just plain integers could be easier to certify than a library dependency.
In such cases, you don't just round to make numbers prettier, but may even keep the truncated part of the equation. It's then nice to use simple stuff that are proven to work and not change over time.
Have you worked on ecommerce where you've been given such detailed specs? I have worked on ecommerce, I never have been.
I don't even understand exactly what you mean by "implementing detailed specifications about how to calculate each operation every step of the way" with ruby BigDecimal. Can you provide an example? Ordinarily, you just use ruby `+` and `*` etc operators with BigDecimal values. (Or SQL/postgres arithmetic operators with postgres money type). I don't even know what a "specification about how to calculate each operation every step of the way" would look like. This is something you've had to do with basic ecommerce apps?
I'm not sure what solution you are suggesting could be characterized as "simple stuff that are proven to work and not change over time"
Seriously, most everyone just uses something like BigDecimal or postgres Money type, and it's fine. (IEEE-754 float is NOT though. Neither, probably, is the rational type that you initially suggested... )
The numerator and denominator get automatically reduced to lowest terms (just like you learned in elementary school, so 15/100 becomes 3/20) internally by every implementation I know of. This comes at a performance cost for every operation, but it helps keeping the numerator and denominator from blowing up.
>not sure how rounding works -- or even if they do rounding at all
They do not. The point of a Rational type is to keep precise values, so it's up to the programmer to decide when and how values are rounded.
>"arbitrary precision floating point" type like ruby's BigDecimal
Not sure how Ruby implements BigDecimal, but Java internally represents it as an BigInteger of digits, and a second integer that represents where in the number the decimal point should go. This means that BigDecimal still can't truly represent a value such as 1/3, since you can't have an infinite amount of 3's, but a Rational can.
>I'm not actually sure what domains rational data types are good for.
I'll be honest and say I've never had to use them either, but it's nice to know they exist. The intended use case is when you need to perform calculations and maintain as much precision and accuracy as possible in the intermediate values and such accuracy is more important than speed.
Only if you never use anything but addition and subtraction. So, no currency coversions, interest rates, complex taxes or rebate schemes or the like.
Which specific places have you seen it used in?