0

I've seen a couple of posts relating to carrying out calculations on real numbers, Etc., but how about simply extracting the fractional component of a real number without encountering floating point errors?

For example, in PowerShell:

PS C:\tmp> (1.4 % 1) -ne 0.4
True

... and:

PS C:\tmp> (1.4 - 1) -ne 0.4
True

So, how can you bin-off the integer component, leave the fractional component, but maintain precision?

Any thoughts?

phuclv
  • 37,963
  • 15
  • 156
  • 475
Simon Catlin
  • 2,141
  • 1
  • 13
  • 15

2 Answers2

1

There is no problem extracting the fractional part of a floating-point number; this can always be done exactly. Whenever two double numbers close to each other are subtracted, the result is always exact. And whenever the modulus of two double numbers is not greater in magnitude than either operand1, the result is always exact.

Thus, in your examples, the results of 1.4 % 1 and 1.4 - 1 are each exact; there is no arithmetic error in the operation.

The reason the results are not equal to 0.4 is that, before the operations, there are already rounding errors in 1.4 and 0.4. The operation that converts the decimal numerals “1.4” and “0.4” to double must round its result, because 1.4 and 0.4 are not representable in the double format.

In the double format, all numbers are represented as a sign and a 53-bit integer multiplied by some power of two.2 Using 1.4 in PowerShell results in +3152519739159347•2−52, which 1.399999999999999911182158029987476766109466552734375. Using 0.4 results in 7205759403792794•2−54, which equals 0.40000000000000002220446049250313080847263336181640625.

As you can see, subtracting 1 from 1.399999999999999911182158029987476766109466552734375 gives 0.399999999999999911182158029987476766109466552734375, which is clearly not equal to 0.40000000000000002220446049250313080847263336181640625. Thus (1.4 - 1) -ne 0.4 is true. Similarly 1.4 % 1 gives the same result, so (1.4 % 1) -ne 0.4 is also true.

There is no operation on 1.4 that will “extract” just the fraction part and give 0.40000000000000002220446049250313080847263336181640625, because its fraction part is not 0.40000000000000002220446049250313080847263336181640625. The problem is not in the extraction operation; the problem is that 1.4 has already lost accuracy and no longer records the complete .4 part.

In contrast, because 0.4 is lower in magnitude, it can be approximated using a scale of 2−54 instead of the 2−52 required for 1.4. That smaller scale means its approximation can be finer, so it is more accurate—0.4 is closer to 0.4 than 1.4 is to 1.4.

There is no general fix for this. Floating-point arithmetic is designed to approximate real-number arithmetic, and it generally should not be used in situations where you want to get exact real-number arithmetic. So limited exact arithmetic can be done in certain situations, but they must be carefully designed.

Footnotes

1 This is always true for a symmetric modulus, where x % y returns a value in [−|y/2|, +|y/2|]. I suspect Microsoft uses that in PowerShell, but their documentation does not say. An asymmetric modulus, such as one that returns a value in [0, |y|), can have a rounding error.

2 There are other descriptions of the double format in which a number is a sign, a bit, a radix-point (written as a period), and 52 more bits multiplied by a power of two. These descriptions are mathematically equivalent because the bounds on the allowed powers of two are adjusted correspondingly. The integer-based description is generally easier for number-theoretic work, although the IEEE-754 standard uses the fraction-based description.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • A general fix does exist; use a decimal instead of a double or float when you need to accurately represent base decimal numbers, up to 28 digits of precision. – Graham Jul 26 '20 at 03:13
  • @Graham: That is not a general fix. The problem a decimal floating-point type fixes is matching human thinking in simple arithmetic, due to the fact that humans are taught decimal arithmetic in elementary school. It does not fix the problem that decimal arithmetic cannot represent fractions like one-third or irrational numbers such as arise in logarithmic or trigonometric function, or that fixed widths such as 28 digits are nonetheless overwhelmed by routine things such as compound interest. – Eric Postpischil Jul 26 '20 at 10:31
  • It's a general fix within the bounds I stated. More importantly, it operates intuitively is a way most PowerShell users expect (which, frankly, is base 10). It should be worth noting in the real world, even manual computation of compound interest is rounded at each iteration, despite this introducing deviation from the purely mathematical result. And, for what it is worth, a decimal is used is those calculations. – Graham Jul 27 '20 at 00:28
  • @Graham: A general fix “within bounds” is not a general fix; it is a specific fix. So my statement that there is no general fix stands. – Eric Postpischil Jul 27 '20 at 00:29
-1

By default PowerShell will use a Double if not expressly specified. Doubles and Floats can produce unexpected results (for base 10 thinking people) during use, particularly with seemingly simple calculations. This contrasts to Decimal, which trades off precision for accuracy, and also produces for expected results. (further reading).

([decimal]1.4 - 1) -ne 0.4 # false

Here the explicit cast force an implicit cast of the 1 and the 0.4.

Some further explanation for people who forget stare too close to the tree and forget about the forest:

As noted elsewhere, 1.4 and .4 (base 10) can not be accurately represented in base 2; it is 1.0110... (0110 repeating) and .0110..., respectively. In contrast to other assertions, this means that you can not extract the fractional portion exactly using a double or float, as they have a finite number of digits. This is why may base 10 decimals are fuzzy by nature when represented in base 2, you're only storing close approximations.

However, if you stored these number as 14/10 and 4/10, which is functionally what a decimal is doing. This means the exact base 10 number can be stored (up to a point) exactly. Thus the general fix for this is to use the decimal type.

As phuclv notes, you can indicate the number you are typing is a decimal by adding a 'd' to the end (e.g. 1.4d). In many cases, however, entering expressly as decimal (using the d) or casting after the fact (e.g. [decimal]1.4) will produce exact same result. This means if you write a function that takes a decimal is passed in that smaller than ~15 digits (right and left of the decimal) you'll be fine. That said, when typing a number into your code, just use the 'd'.

For example, the following both return true:

1.000000000000001d -ne [decimal]1.000000000000001 # True: Not Equal
1.00000000000001d -eq [decimal]1.00000000000001 #True: Equal

Lastly, it should be noted that using decimal over float or double does come as a performance cost, which is relevant when processing a large number of values, but can be ignored for a small number.

Graham
  • 609
  • 6
  • 9
  • 1
    Re “By default PowerShell will use a Double is not expressly specified, which are inherently fuzzy”: That sentence appears to have two verbs, “will use” and “is not”. What are you trying to say? Also, the `Double` type is not “inherently fuzzy.” Each floating-point number **is** one number exactly. Approximations occur in floating-point arithmetic, not in the numbers. This distinction is important for designing and analyzing floating-point software and proving theorems about it. – Eric Postpischil Jul 23 '20 at 19:57
  • The answer was updated for clarity, however many PowerShell users do not, and will not, care about "designing and analyzing floating-point software and proving theorems about it". – Graham Jul 23 '20 at 20:24
  • `[decimal]1.4` won't solve the problem because 1.4 is still a double being converted to decimal. You need `1.4d` or `[decimal]"1.4"` – phuclv Jul 24 '20 at 06:21
  • @phuclv During the conversion there is a loss of precision which leads to a rounded that is functional equivalent to 1.4, despite that being an irrational number in base 2. As long as operations are not operation near the limits of it's precision, it will produce an accurate result. This solves the problem for the level of precision required in a significant number of use cases, particularly those were PowerShell is the preferred means of resolving. – Graham Jul 24 '20 at 15:49
  • no, it may work for this particular case but it won't work for most other cases. You must use the d suffix or a string in order to get a real decimal value without any loss of precision from the start – phuclv Jul 24 '20 at 15:51
  • @phuclv Can you support your "most" assertion: give a couple of examples of numbers at, say, 10 decimal places of precision in which the conversion produces a different result, as well as the next closest number that exhibits the same issue. – Graham Jul 24 '20 at 16:07
  • 1.4 already exhibits the issue: `[decimal]1.4 -ne 1.4d` and `([decimal]1.4 - 1) -ne 0.4d` both return false. It works just because you compare with 0.4 with has the same error in binary. If you do the correct comparison in decimal it'lll never works. In fact it never works with almost all values that aren't exactly representable in binary floating-point types – phuclv Jul 25 '20 at 00:32
  • "1.4 already exhibits the issue: [decimal]1.4 -ne 1.4d and ([decimal]1.4 - 1) -ne 0.4d both return false." 1.4 - 1 <> .4 IS false, which was my point. It's when it returns true (that they are not equal) that it's a problem for the OP. In fact, I gave you what should be an impossible problem at 10 digits of precision (up to 12, if I recall). There is no positive number below 10 with 10 digits beyond the decimal that will differ if entered as a decimal versus entered as a double then converted to a decimal. Thus and such number will not fail the inequality test and return true. – Graham Jul 26 '20 at 02:16