Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Floating Point Types

In the previous chapter, we have seen that despite their ease of use, arithmetic operations on integers are prone to programming errors due to overflow and truncation. We have also seen that integers cannot have values with fractional parts, as in 1.25.

Floating point types are designed to support fractional parts. The "point" in their name comes from the radix point, which separates the integer part from the fractional part, and "floating" refers to a detail in how these types are implemented: the decimal point floats left and right as appropriate. (This detail is not important when using these types.)

We must cover important details in this chapter as well. Before doing that, I would like to give a list of some of the interesting aspects of floating point types:

Although floating point types are more useful in some cases, they have peculiarities that every programmer must know. Compared to integers, they are very good at avoiding truncation because their main purpose is to support fractional values. Like any other type, being based on a certain number of bits, they too are prone to overflow, but compared to integers, the range of values that they can support is vast. Additionally, instead of being silent in the case of overflow, they get the special values of positive and negative infinity.

As a reminder, the floating point types are the following:

Type Number of Bits Initial Value
float 32 float.nan
double 64 double.nan
real at least 64, maybe more
(e.g. 80, depending on hardware support)
real.nan
Floating point type properties

Floating point types have more properties than other types:

Other properties of floating point types are used less commonly. You can see all of them at Properties for Floating Point Types at dlang.org.

The properties of floating point types and their relations can be shown on a number line like the following:

   +     +─────────+─────────+   ...   +   ...   +─────────+─────────+     +
   │   -max       -1         │         0         │         1        max    │
   │                         │                   │                         │
-infinity               -min_normal          min_normal               infinity

Other than the two special infinity values, the line above is to scale: the number of values that can be represented between min_normal and 1 is equal to the number of values that can be represented between 1 and max. This means that the precision of the fractional parts of the values that are between min_normal and 1 is very high. (The same is true for the negative side as well.)

.nan

We have already seen that this is the default value of floating point variables. .nan may appear as a result of meaningless floating point expressions as well. For example, the floating point expressions in the following program all produce double.nan:

import std.stdio;

void main() {
    double zero = 0;
    double infinity = double.infinity;

    writeln("any expression with nan: ", double.nan + 1);
    writeln("zero / zero            : ", zero / zero);
    writeln("zero * infinity        : ", zero * infinity);
    writeln("infinity / infinity    : ", infinity / infinity);
    writeln("infinity - infinity    : ", infinity - infinity);
}

.nan is not useful just because it indicates an uninitialized value. It is also useful because it is propagated through computations, making it easier and earlier to detect errors.

Specifying floating point values

Floating point values can be built from integer values without a decimal point, like 123, or created directly with a decimal point, like 123.0.

Floating point values can also be specified with the special floating point syntax, as in 1.23e+4. The e+ part in that syntax can be read as "times 10 to the power of". According to that reading, the previous value is "1.23 times 10 to the power of 4", which is the same as "1.23 times 104", which in turn is the same as 1.23x10000, being equal to 12300.

If the value after e is negative, as in 5.67e-3, then it is read as "divided by 10 to the power of". Accordingly, this example is "5.67 divided by 103", which in turn is the same as 5.67/1000, being equal to 0.00567.

The floating point format is apparent in the output of the following program that prints the properties of the three floating point types:

import std.stdio;

void main() {
    writeln("Type                    : ", float.stringof);
    writeln("Precision               : ", float.dig);
    writeln("Minimum normalized value: ", float.min_normal);
    writeln("Minimum value           : ", -float.max);
    writeln("Maximum value           : ", float.max);
    writeln();

    writeln("Type                    : ", double.stringof);
    writeln("Precision               : ", double.dig);
    writeln("Minimum normalized value: ", double.min_normal);
    writeln("Minimum value           : ", -double.max);
    writeln("Maximum value           : ", double.max);
    writeln();

    writeln("Type                    : ", real.stringof);
    writeln("Precision               : ", real.dig);
    writeln("Minimum normalized value: ", real.min_normal);
    writeln("Minimum value           : ", -real.max);
    writeln("Maximum value           : ", real.max);
}

The output of the program is the following in my environment. Since real depends on the hardware, you may get a different output:

Type                    : float
Precision               : 6
Minimum normalized value: 1.17549e-38
Minimum value           : -3.40282e+38
Maximum value           : 3.40282e+38

Type                    : double
Precision               : 15
Minimum normalized value: 2.22507e-308
Minimum value           : -1.79769e+308
Maximum value           : 1.79769e+308

Type                    : real
Precision               : 18
Minimum normalized value: 3.3621e-4932
Minimum value           : -1.18973e+4932
Maximum value           : 1.18973e+4932

Note: Although double and real have more precision than float, writeln prints all floating point values with 6 digits of precision. (Precision is explained below.)

Observations

As you will remember from the previous chapter, the maximum value of ulong has 20 digits: 18,446,744,073,709,551,616. That value looks small when compared to even the smallest floating point type: float can have values up to the 1038 range, e.g. 340,282,000,000,000,000,000,000,000,000,000,000,000. The maximum value of real is in the range 104932, a value with more than 4900 digits!

As another observation, let's look at the minimum value that double can represent with 15-digit precision:

    0.000...(there are 300 more zeroes here)...0000222507385850720
Overflow is not ignored

Despite being able to take very large values, floating point types are prone to overflow as well. The floating point types are safer than integer types in this regard because overflow is not ignored. The values that overflow on the positive side become .infinity, and the values that overflow on the negative side become ‑.infinity. To see this, let's increase the value of .max by 10%. Since the value is already at the maximum, increasing by 10% would overflow:

import std.stdio;

void main() {
    real value = real.max;

    writeln("Before         : ", value);

    // Multiplying by 1.1 is the same as adding 10%
    value *= 1.1;
    writeln("Added 10%      : ", value);

    // Let's try to reduce its value by dividing in half
    value /= 2;
    writeln("Divided in half: ", value);
}

Once the value overflows and becomes real.infinity, it remains that way even after being divided in half:

Before         : 1.18973e+4932
Added 10%      : inf
Divided in half: inf
Precision

Precision is a concept that we come across in daily life but do not talk about much. Precision is the number of digits that is used when specifying a value. For example, when we say that the third of 100 is 33, the precision is 2 because 33 has 2 digits. When the value is specified more precisely as 33.33, then the precision is 4 digits.

The number of bits that each floating type has, not only affects its maximum value, but also its precision. The greater the number of bits, the more precise the values are.

There is no truncation in division

As we have seen in the previous chapter, integer division cannot preserve the fractional part of a result:

    int first = 3;
    int second = 2;
    writeln(first / second);

Output:

1

Floating point types don't have this truncation problem; they are specifically designed for preserving the fractional parts:

    double first = 3;
    double second = 2;
    writeln(first / second);

Output:

1.5

The accuracy of the fractional part depends on the precision of the type: real has the highest precision and float has the lowest precision.

Which type to use

Unless there is a specific reason not to, you can choose double for floating point values. float has low precision but due to being smaller than the other types it may be useful when memory is limited. On the other hand, since the precision of real is higher than double on some hardware, it would be preferable for high precision calculations.

Cannot represent all values

We cannot represent certain values in our daily lives. In the decimal system that we use daily, the digits before the decimal point represent ones, tens, hundreds, etc. and the digits after the decimal point represent tenths, hundredths, thousandths, etc.

If a value is created from a combination of these values, it can be represented exactly. For example, because the value 0.23 consists of 2 tenths and 3 hundredths it is represented exactly. On the other hand, the value 1/3 cannot be exactly represented in the decimal system because the number of digits is always insufficient, no matter how many are specified: 0.33333...

The situation is very similar with the floating point types. Because these types are based on a certain number of bits, they cannot represent every value exactly.

The difference with the binary system that the computers use is that the digits before the decimal point are ones, twos, fours, etc. and the digits after the decimal point are halves, quarters, eighths, etc. Only the values that are exact combinations of those digits can be represented exactly.

A value that cannot be represented exactly in the binary system used by computers is 0.1, as in 10 cents. Although this value can be represented exactly in the decimal system, its binary representation never ends and continuously repeats four digits: 0.0001100110011... (Note that the value is written in binary system, not decimal.) It is always inaccurate at some level depending on the precision of the floating point type that is used.

The following program demonstrates this problem. The value of a variable is being incremented by 0.001 a thousand times in a loop. Surprisingly, the result is not 1:

import std.stdio;

void main() {
    float result = 0;

    // Adding 0.001 for a thousand times:
    int counter = 1;
    while (counter <= 1000) {
        result += 0.001;
        ++counter;
    }

    if (result == 1) {
        writeln("As expected: 1");

    } else {
        writeln("DIFFERENT: ", result);
    }
}

Because 0.001 cannot be represented exactly, that inaccuracy affects the result at every iteration:

DIFFERENT: 0.999991

Note: The variable counter above is a loop counter. Defining a variable explicitly for that purpose is not recommended. Instead, a common approach is to use a foreach loop, which we will see in a later chapter.

Unorderedness

The same comparison operators that we have covered with integers are used with floating point types as well. However, since the special value .nan represents invalid floating point values, comparing .nan to other values is not meaningful. For example, it does not make sense to ask whether .nan or 1 is greater.

For that reason, floating point values introduce another comparison concept: unorderedness. Being unordered means that at least one of the values is .nan.

The following table lists all the floating point comparison operators. All of them are binary operators (meaning that they take two operands) and used as in left == right. The columns that contain false and true are the results of the comparison operations.

The last column indicates whether the operation is meaningful if one of the operands is .nan. For example, even though the result of the expression 1.2 < real.nan is false, that result is meaningless because one of the operands is real.nan. The result of the reverse comparison real.nan < 1.2 would produce false as well. The abreviation lhs stands for left-hand side, indicating the expression on the left-hand side of each operator.


Operator

Meaning
If lhs
is greater
If lhs
is less
If both
are equal
If at least
one is .nan
Meaningful
with .nan
==is equal to falsefalsetruefalseyes
!=is not equal to truetruefalsetrueyes
>is greater than truefalsefalsefalseno
>=is greater than or equal to truefalsetruefalseno
<is less than falsetruefalsefalseno
<=is less than or equal to falsetruetruefalseno

Although meaningful to use with .nan, the == operator always produces false when used with a .nan value. This is the case even when both values are .nan:

import std.stdio;

void main() {
    if (double.nan == double.nan) {
        writeln("equal");

    } else {
        writeln("not equal");
    }
}

Although one would expect double.nan to be equal to itself, the result of the comparison is false:

not equal
isNaN() for .nan equality comparison

As we have seen above, it is not possible to use the == operator to determine whether the value of a floating point variable is .nan:

    if (variable == double.nan) {    // ← WRONG
        // ...
    }

isNaN() function from the std.math module is for determining whether a value is .nan:

import std.math;
// ...
    if (isNaN(variable)) {           // ← correct
        // ...
    }

Similarly, to determine whether a value is not .nan, one must use !isNaN() because otherwise the != operator would always produce true.

Exercises
  1. Instead of float, use double (or real) in the program above which added 0.001 a thousand times:
        double result = 0;
    

    This exercise demonstrates how misleading floating point equality comparisons can be.

  2. Modify the calculator from the previous chapter to support floating point types. The new calculator should work more accurately with that change. When trying the calculator, you can enter floating point values in various formats, as in 1000, 1.23, and 1.23e4.
  3. Write a program that reads 5 floating point values from the input. Make the program first print twice of each value and then one fifth of each value.

    This exercise is a preparation for the array concept of the next chapter. If you write this program with what you have seen so far, you will understand arrays more easily and will better appreciate them.