Numerics
Modular Types
In the Introduction to Ada course, we've seen that Ada has two kinds of integer type: signed and modular types. For example:
package Num_Types is type Signed_Integer is range 1 .. 1_000_000; type Modular is mod 2**32; end Num_Types;
In this section, we discuss two attributes of modular types: Modulus
and Mod
. We also discuss operations on modular types.
In the Ada Reference Manual
Modulus
Attribute
The Modulus
attribute returns the modulus of the modular type as a
universal integer value. Let's get the modulus of the 32-bit Modular
type that we've declared in the Num_Types
package of the previous
example:
with Ada.Text_IO; use Ada.Text_IO; with Num_Types; use Num_Types; procedure Show_Modular is Modulus_Value : constant := Modular'Modulus; begin Put_Line (Modulus_Value'Image); end Show_Modular;
When we run this example, we get 4294967296, which is equal to 2**32
.
Mod
Attribute
Note
This section was originally written by Robert A. Duff and published as Gem #26: The Mod Attribute.
Operations on signed integers can overflow: if the result is outside the base
range, Constraint_Error
will be raised. In our previous example, we
declared the Signed_Integer
type:
type Signed_Integer is range 1 .. 1_000_000;
The base range of Signed_Integer
is the range of
Signed_Integer'Base
, which is chosen by the compiler, but is likely to
be something like -2**31 .. 2**31 - 1
. (Note: we discussed the
Base
attribute in this section.)
Operations on modular integers use modular (wraparound) arithmetic. For example:
with Ada.Text_IO; use Ada.Text_IO; with Num_Types; use Num_Types; procedure Show_Modular is X : Modular; begin X := 1; Put_Line (X'Image); X := -X; Put_Line (X'Image); end Show_Modular;
Negating X gives -1, which wraps around to 2**32 - 1
, i.e.
all-one-bits.
But what about a type conversion from signed to modular? Is that a signed
operation (so it should overflow) or is it a modular operation (so it should
wrap around)? The answer in Ada is the former — that is, if you try to
convert, say, Integer'(-1)
to Modular
, you will get
Constraint_Error
:
with Ada.Text_IO; use Ada.Text_IO; with Num_Types; use Num_Types; procedure Show_Modular is I : Integer := -1; X : Modular := 1; begin X := Modular (I); -- raises Constraint_Error Put_Line (X'Image); end Show_Modular;
To solve this problem, we can use the Mod
attribute:
with Ada.Text_IO; use Ada.Text_IO; with Num_Types; use Num_Types; procedure Show_Modular is I : constant Integer := -1; X : Modular := 1; begin X := Modular'Mod (I); Put_Line (X'Image); end Show_Modular;
The Mod
attribute will correctly convert from any integer type to a
given modular type, using wraparound semantics.
Historically
In older versions of Ada — such as Ada 95 —, the only way to do
this conversion is to use Unchecked_Conversion
, which is somewhat
uncomfortable. Furthermore, if you're trying to convert to a generic formal
modular type, how do you know what size of signed integer type to use? Note
that Unchecked_Conversion
might malfunction if the source and target
types are of different sizes.
The Mod
attribute was added to Ada 2005 to solve this problem.
Also, we can now safely use this attribute in generics. For example:
generic type Formal_Modular is mod <>; package Mod_Attribute is function F return Formal_Modular; end Mod_Attribute;package body Mod_Attribute is A_Signed_Integer : Integer := -1; function F return Formal_Modular is begin return Formal_Modular'Mod (A_Signed_Integer); end F; end Mod_Attribute;
In this example, F
will return the all-ones bit pattern, for
whatever modular type is passed to Formal_Modular
.
Operations on modular types
Modular types are particularly useful for bit manipulation. For example, we
can use the and
, or
, xor
and not
operators for
modular types.
Also, we can perform bit-shifting by multiplying or dividing a modular object
with a power of two. For example, if M
is a variable of modular type,
then M := M * 2 ** 3;
shifts the bits to the left by three bits.
Likewise, M := M / 2 ** 3
shifts the bits to the right. Note that the
compiler selects the appropriate shifting operator when translating these
operations to machine code — no actual multiplication or division will be
performed.
Let's see a simple implementation of the CRC-CCITT (0x1D0F) algorithm:
package Crc_Defs is type Byte is mod 2 ** 8; type Crc is mod 2 ** 16; type Byte_Array is array (Positive range <>) of Byte; function Crc_CCITT (A : Byte_Array) return Crc; procedure Display (Crc_A : Crc); procedure Display (A : Byte_Array); end Crc_Defs;with Ada.Text_IO; use Ada.Text_IO; package body Crc_Defs is package Byte_IO is new Modular_IO (Byte); package Crc_IO is new Modular_IO (Crc); function Crc_CCITT (A : Byte_Array) return Crc is X : Byte; Crc_A : Crc := 16#1d0f#; begin for I in A'Range loop X := Byte (Crc_A / 2 ** 8) xor A (I); X := X xor (X / 2 ** 4); declare Crc_X : constant Crc := Crc (X); begin Crc_A := Crc_A * 2 ** 8 xor Crc_X * 2 ** 12 xor Crc_X * 2 ** 5 xor Crc_X; end; end loop; return Crc_A; end Crc_CCITT; procedure Display (Crc_A : Crc) is begin Crc_IO.Put (Crc_A); New_Line; end Display; procedure Display (A : Byte_Array) is begin for E of A loop Byte_IO.Put (E); Put (", "); end loop; New_Line; end Display; begin Byte_IO.Default_Width := 1; Byte_IO.Default_Base := 16; Crc_IO.Default_Width := 1; Crc_IO.Default_Base := 16; end Crc_Defs;with Ada.Text_IO; use Ada.Text_IO; with Crc_Defs; use Crc_Defs; procedure Show_Crc is AA : constant Byte_Array := (16#0#, 16#20#, 16#30#); Crc_A : Crc; begin Crc_A := Crc_CCITT (AA); Put ("Input array: "); Display (AA); Put ("CRC-CCITT: "); Display (Crc_A); end Show_Crc;
In this example, the core of the algorithm is implemented in the
Crc_CCITT
function. There, we use bit shifting — for instance,
* 2 ** 8
and / 2 ** 8
, which shift left and right, respectively,
by eight bits. We also use the xor
operator.
Numeric Literals
Classification
We've already discussed basic characteristics of numeric literals in the Introduction to Ada course — although we haven't used this terminology there. There are two kinds of numeric literals in Ada: integer literals and real literals. They are distinguished by the absence or presence of a radix point. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Real_Integer_Literals is Integer_Literal : constant := 365; Real_Literal : constant := 365.2564; begin Put_Line ("Integer Literal: " & Integer_Literal'Image); Put_Line ("Real Literal: " & Real_Literal'Image); end Real_Integer_Literals;
Another classification takes the use of a base indicator into account.
(Remember that, when writing a literal such as 2#1011#
, the base is the
element before the first #
sign.) So here we distinguish between decimal
literals and based literals. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Decimal_Based_Literals is package F_IO is new Ada.Text_IO.Float_IO (Float); -- -- DECIMAL LITERALS -- Dec_Integer : constant := 365; Dec_Real : constant := 365.2564; Dec_Real_Exp : constant := 0.365_256_4e3; -- -- BASED LITERALS -- Based_Integer : constant := 16#16D#; Based_Integer_Exp : constant := 5#243#e1; Based_Real : constant := 2#1_0110_1101.0100_0001_1010_0011_0111#; Based_Real_Exp : constant := 7#1.031_153_643#e3; begin F_IO.Default_Fore := 3; F_IO.Default_Aft := 4; F_IO.Default_Exp := 0; Put_Line ("Dec_Integer: " & Dec_Integer'Image); Put ("Dec_Real: "); F_IO.Put (Item => Dec_Real); New_Line; Put ("Dec_Real_Exp: "); F_IO.Put (Item => Dec_Real_Exp); New_Line; Put_Line ("Based_Integer: " & Based_Integer'Image); Put_Line ("Based_Integer_Exp: " & Based_Integer_Exp'Image); Put ("Based_Real: "); F_IO.Put (Item => Based_Real); New_Line; Put ("Based_Real_Exp: "); F_IO.Put (Item => Based_Real_Exp); New_Line; end Decimal_Based_Literals;
Based literals use the base#number#
format. Also, they aren't limited to
simple integer literals such as 16#16D#
. In fact, we can use a radix
point or an exponent in based literals, as well as underscores. In addition, we
can use any base from 2 up to 16. We discuss these aspects further in the next
section.
Features and Flexibility
Note
This section was originally written by Franco Gasperoni and published as Gem #7: The Beauty of Numeric Literals in Ada.
Ada provides a simple and elegant way of expressing numeric literals. One of
those simple, yet powerful aspects is the ability to use underscores to
separate groups of digits. For example,
3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37510
is more
readable and less error prone to type than
3.14159265358979323846264338327950288419716939937510
. Here's the
complete code:
with Ada.Text_IO; procedure Ada_Numeric_Literals is Pi : constant := 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37510; Pi2 : constant := 3.14159265358979323846264338327950288419716939937510; Z : constant := Pi - Pi2; pragma Assert (Z = 0.0); use Ada.Text_IO; begin Put_Line ("Z = " & Float'Image (Z)); end Ada_Numeric_Literals;
Also, when using based literals, Ada allows any base from 2 to 16. Thus, we can write the decimal number 136 in any one of the following notations:
with Ada.Text_IO; procedure Ada_Numeric_Literals is Bin_136 : constant := 2#1000_1000#; Oct_136 : constant := 8#210#; Dec_136 : constant := 10#136#; Hex_136 : constant := 16#88#; pragma Assert (Bin_136 = 136); pragma Assert (Oct_136 = 136); pragma Assert (Dec_136 = 136); pragma Assert (Hex_136 = 136); use Ada.Text_IO; begin Put_Line ("Bin_136 = " & Integer'Image (Bin_136)); Put_Line ("Oct_136 = " & Integer'Image (Oct_136)); Put_Line ("Dec_136 = " & Integer'Image (Dec_136)); Put_Line ("Hex_136 = " & Integer'Image (Hex_136)); end Ada_Numeric_Literals;
In other languages
The rationale behind the method to specify based literals in the C
programming language is strange and unintuitive. Here, you have only three
possible bases: 8, 10, and 16 (why no base 2?). Furthermore, requiring
that numbers in base 8 be preceded by a zero feels like a bad joke on us
programmers. For example, what values do 0210
and 210
represent
in C?
When dealing with microcontrollers, we might encounter I/O devices that are memory mapped. Here, we have the ability to write:
Lights_On : constant := 2#1000_1000#;
Lights_Off : constant := 2#0111_0111#;
and have the ability to turn on/off the lights as follows:
Output_Devices := Output_Devices or Lights_On;
Output_Devices := Output_Devices and Lights_Off;
Here's the complete example:
with Ada.Text_IO; procedure Ada_Numeric_Literals is Lights_On : constant := 2#1000_1000#; Lights_Off : constant := 2#0111_0111#; type Byte is mod 256; Output_Devices : Byte := 0; -- for Output_Devices'Address -- use 16#DEAD_BEEF#; -- ^^^^^^^^^^^^^^^^^^^^^^^^^^ -- Memory mapped Output use Ada.Text_IO; begin Output_Devices := Output_Devices or Lights_On; Put_Line ("Output_Devices (lights on ) = " & Byte'Image (Output_Devices)); Output_Devices := Output_Devices and Lights_Off; Put_Line ("Output_Devices (lights off) = " & Byte'Image (Output_Devices)); end Ada_Numeric_Literals;
Of course, we can also use records with representation clauses to do the above, which is even more elegant.
The notion of base in Ada allows for exponents, which is particularly pleasant. For instance, we can write:
package Literal_Binaries is Kilobyte : constant := 2#1#e+10; Megabyte : constant := 2#1#e+20; Gigabyte : constant := 2#1#e+30; Terabyte : constant := 2#1#e+40; Petabyte : constant := 2#1#e+50; Exabyte : constant := 2#1#e+60; Zettabyte : constant := 2#1#e+70; Yottabyte : constant := 2#1#e+80; end Literal_Binaries;
In based literals, the exponent — like the base — uses the regular
decimal notation and specifies the power of the base that the based literal
should be multiplied with to obtain the final value. For instance
2#1#e+10
= 1 x 210 = 1_024
(in base 10), whereas
16#F#e+2
= 15 x 162 = 15 x 256 = 3_840
(in
base 10).
Based numbers apply equally well to real literals. We can, for instance, write:
One_Third : constant := 3#0.1#;
-- ^^^^^^
-- same as 1.0/3
Whether we write 3#0.1#
or 1.0 / 3
, or even 3#1.0#e-1
, Ada
allows us to specify exactly rational numbers for which decimal literals cannot
be written.
The last nice feature is that Ada has an open-ended set of integer and real types. As a result, numeric literals in Ada do not carry with them their type as, for example, in C. The actual type of the literal is determined from the context. This is particularly helpful in avoiding overflows, underflows, and loss of precision.
In other languages
In C, a source of confusion can be the distinction between 32l
and
321
. Although both look similar, they're actually very different from
each other.
And this is not all: all constant computations done at compile time are done in infinite precision, be they integer or real. This allows us to write constants with whatever size and precision without having to worry about overflow or underflow. We can for instance write:
Zero : constant := 1.0 - 3.0 * One_Third;
and be guaranteed that constant Zero
has indeed value zero. This is very
different from writing:
One_Third_Approx : constant :=
0.33333333333333333333333333333;
Zero_Approx : constant :=
1.0 - 3.0 * One_Third_Approx;
where Zero_Approx
is really 1.0e-29
— and that will show up
in your numerical computations. The above is quite handy when we want to write
fractions without any loss of precision. Here's the complete code:
with Ada.Text_IO; procedure Ada_Numeric_Literals is One_Third : constant := 3#1.0#e-1; -- same as 1.0/3.0 Zero : constant := 1.0 - 3.0 * One_Third; pragma Assert (Zero = 0.0); One_Third_Approx : constant := 0.33333333333333333333333333333; Zero_Approx : constant := 1.0 - 3.0 * One_Third_Approx; use Ada.Text_IO; begin Put_Line ("Zero = " & Float'Image (Zero)); Put_Line ("Zero_Approx = " & Float'Image (Zero_Approx)); end Ada_Numeric_Literals;
Along these same lines, we can write:
with Ada.Text_IO; with Literal_Binaries; use Literal_Binaries; procedure Ada_Numeric_Literals is Big_Sum : constant := 1 + Kilobyte + Megabyte + Gigabyte + Terabyte + Petabyte + Exabyte + Zettabyte; Result : constant := (Yottabyte - 1) / (Kilobyte - 1); Nil : constant := Result - Big_Sum; pragma Assert (Nil = 0); use Ada.Text_IO; begin Put_Line ("Nil = " & Integer'Image (Nil)); end Ada_Numeric_Literals;
and be guaranteed that Nil
is equal to zero.
Floating-Point Types
In this section, we discuss various attributes related to floating-point types.
In the Ada Reference Manual
Representation-oriented attributes
In this section, we discuss attributes related to the representation of floating-point types.
Attribute: Machine_Radix
Machine_Radix
is an attribute that returns the radix of the hardware
representation of a type. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Machine_Radix is begin Put_Line ("Float'Machine_Radix: " & Float'Machine_Radix'Image); Put_Line ("Long_Float'Machine_Radix: " & Long_Float'Machine_Radix'Image); Put_Line ("Long_Long_Float'Machine_Radix: " & Long_Long_Float'Machine_Radix'Image); end Show_Machine_Radix;
Usually, this value is two, as the radix is based on a binary system.
Attributes: Machine_Mantissa
Machine_Mantissa
is an attribute that returns the number of bits
reserved for the mantissa of the floating-point type. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Machine_Mantissa is begin Put_Line ("Float'Machine_Mantissa: " & Float'Machine_Mantissa'Image); Put_Line ("Long_Float'Machine_Mantissa: " & Long_Float'Machine_Mantissa'Image); Put_Line ("Long_Long_Float'Machine_Mantissa: " & Long_Long_Float'Machine_Mantissa'Image); end Show_Machine_Mantissa;
On a typical desktop PC, as indicated by Machine_Mantissa
, we have 24
bits for the floating-point mantissa of the Float
type.
Machine_Emin
and Machine_Emax
The Machine_Emin
and Machine_Emax
attributes return the minimum
and maximum value, respectively, of the machine exponent the floating-point
type. Note that, in all cases, the returned value is a universal integer. For
example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Machine_Emin_Emax is begin Put_Line ("Float'Machine_Emin: " & Float'Machine_Emin'Image); Put_Line ("Float'Machine_Emax: " & Float'Machine_Emax'Image); Put_Line ("Long_Float'Machine_Emin: " & Long_Float'Machine_Emin'Image); Put_Line ("Long_Float'Machine_Emax: " & Long_Float'Machine_Emax'Image); Put_Line ("Long_Long_Float'Machine_Emin: " & Long_Long_Float'Machine_Emin'Image); Put_Line ("Long_Long_Float'Machine_Emax: " & Long_Long_Float'Machine_Emax'Image); end Show_Machine_Emin_Emax;
On a typical desktop PC, the value of Float'Machine_Emin
and
Float'Machine_Emax
is -125 and 128, respectively.
To get the actual minimum and maximum value of the exponent for a specific
type, we need to use the Machine_Radix
attribute that we've seen
previously. Let's calculate the minimum and maximum value of the exponent for
the Float
type on a typical PC:
Value of minimum exponent:
Float'Machine_Radix ** Float'Machine_Emin
.In our target platform, this is 2-125 = 2.35098870164457501594 x 10-38.
Value of maximum exponent:
Float'Machine_Radix ** Float'Machine_Emax
.In our target platform, this is 2128 = 3.40282366920938463463 x 1038.
Attribute: Digits
Digits
is an attribute that returns the requested decimal precision of
a floating-point subtype. Let's see an example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Digits is begin Put_Line ("Float'Digits: " & Float'Digits'Image); Put_Line ("Long_Float'Digits: " & Long_Float'Digits'Image); Put_Line ("Long_Long_Float'Digits: " & Long_Long_Float'Digits'Image); end Show_Digits;
Here, the requested decimal precision of the Float
type is six digits.
Note that we said that Digits
is the requested level of precision,
which is specified as part of declaring a floating point type. We can retrieve
the actual decimal precision with Base'Digits
. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Base_Digits is type Float_D3 is new Float digits 3; begin Put_Line ("Float_D3'Digits: " & Float_D3'Digits'Image); Put_Line ("Float_D3'Base'Digits: " & Float_D3'Base'Digits'Image); end Show_Base_Digits;
The requested decimal precision of the Float_D3
type is three digits,
while the actual decimal precision is six digits (on a typical desktop PC).
Attributes: Denorm
, Signed_Zeros
, Machine_Rounds
, Machine_Overflows
In this section, we discuss attributes that return Boolean
values
indicating whether a feature is available or not in the target architecture:
Denorm
is an attribute that indicates whether the target architecture uses denormalized numbers.Signed_Zeros
is an attribute that indicates whether the type uses a sign for zero values, so it can represent both -0.0 and 0.0.Machine_Rounds
is an attribute that indicates whether rounding-to-nearest is used, rather than some other choice (such as rounding-toward-zero).Machine_Overflows
is an attribute that indicates whether aConstraint_Error
exception is (or is not) guaranteed to be raised when an operation with that type produces an overflow or divide-by-zero.
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Boolean_Attributes is begin Put_Line ("Float'Denorm: " & Float'Denorm'Image); Put_Line ("Long_Float'Denorm: " & Long_Float'Denorm'Image); Put_Line ("Long_Long_Float'Denorm: " & Long_Long_Float'Denorm'Image); Put_Line ("Float'Signed_Zeros: " & Float'Signed_Zeros'Image); Put_Line ("Long_Float'Signed_Zeros: " & Long_Float'Signed_Zeros'Image); Put_Line ("Long_Long_Float'Signed_Zeros: " & Long_Long_Float'Signed_Zeros'Image); Put_Line ("Float'Machine_Rounds: " & Float'Machine_Rounds'Image); Put_Line ("Long_Float'Machine_Rounds: " & Long_Float'Machine_Rounds'Image); Put_Line ("Long_Long_Float'Machine_Rounds: " & Long_Long_Float'Machine_Rounds'Image); Put_Line ("Float'Machine_Overflows: " & Float'Machine_Overflows'Image); Put_Line ("Long_Float'Machine_Overflows: " & Long_Float'Machine_Overflows'Image); Put_Line ("Long_Long_Float'Machine_Overflows: " & Long_Long_Float'Machine_Overflows'Image); end Show_Boolean_Attributes;
On a typical PC, we have the following information:
Denorm
is true (i.e. the architecture uses denormalized numbers);Signed_Zeros
is true (i.e. the standard floating-point types use a sign for zero values);Machine_Rounds
is true (i.e. rounding-to-nearest is used for floating-point types);Machine_Overflows
is false (i.e. there's no guarantee that aConstraint_Error
exception is raised when an operation with a floating-point type produces an overflow or divide-by-zero).
Primitive function attributes
In this section, we discuss attributes that we can use to manipulate floating-point values.
Attributes: Fraction
, Exponent
and Compose
The Exponent
and Fraction
attributes return "parts" of a
floating-point value:
Exponent
returns the machine exponent, andFraction
returns the mantissa part.
Compose
is used to return a floating-point value based on a fraction
(the mantissa part) and the machine exponent.
Let's see some examples:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Exponent_Fraction_Compose is begin Put_Line ("Float'Fraction (1.0): " & Float'Fraction (1.0)'Image); Put_Line ("Float'Fraction (0.25): " & Float'Fraction (0.25)'Image); Put_Line ("Float'Fraction (1.0e-25): " & Float'Fraction (1.0e-25)'Image); Put_Line ("Float'Exponent (1.0): " & Float'Exponent (1.0)'Image); Put_Line ("Float'Exponent (0.25): " & Float'Exponent (0.25)'Image); Put_Line ("Float'Exponent (1.0e-25): " & Float'Exponent (1.0e-25)'Image); Put_Line ("Float'Compose (5.00000e-01, 1): " & Float'Compose (5.00000e-01, 1)'Image); Put_Line ("Float'Compose (5.00000e-01, -1): " & Float'Compose (5.00000e-01, -1)'Image); Put_Line ("Float'Compose (9.67141E-01, -83): " & Float'Compose (9.67141E-01, -83)'Image); end Show_Exponent_Fraction_Compose;
To understand this code example, we have to take this formula into account:
Value = Fraction x Machine_RadixExponent
Considering that the value of Float'Machine_Radix
on a typical PC is
two, we see that the value 1.0 is composed by a fraction of 0.5 and a machine
exponent of one. In other words:
0.5 x 21 = 1.0
For the value 0.25, we get a fraction of 0.5 and a machine exponent of -1,
which is the result of 0.5 x 2-1 = 0.25.
We can use the Compose
attribute to perform this calculation. For
example, Float'Compose (0.5, -1) = 0.25
.
Note that Fraction
is always between 0.5 and 0.999999 (i.e < 1.0),
except for denormalized numbers, where it can be < 0.5.
Attribute: Scaling
Scaling
is an attribute that scales a floating-point value based on the
machine radix and a machine exponent passed to the function. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Scaling is begin Put_Line ("Float'Scaling (0.25, 1): " & Float'Scaling (0.25, 1)'Image); Put_Line ("Float'Scaling (0.25, 2): " & Float'Scaling (0.25, 2)'Image); Put_Line ("Float'Scaling (0.25, 3): " & Float'Scaling (0.25, 3)'Image); end Show_Scaling;
The scaling is calculated with this formula:
scaling = value x Machine_Radixmachine exponent
For example, on a typical PC with a machine radix of two,
Float'Scaling (0.25, 3) = 2.0
corresponds to
0.25 x 23 = 2.0
Round-up and round-down attributes
Floor
and Ceiling
are attributes that returned the rounded-down
or rounded-up value, respectively, of a floating-point value. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Floor_Ceiling is begin Put_Line ("Float'Floor (0.25): " & Float'Floor (0.25)'Image); Put_Line ("Float'Ceiling (0.25): " & Float'Ceiling (0.25)'Image); end Show_Floor_Ceiling;
As we can see in this example, the rounded-down value (floor) of 0.25 is 0.0, while the rounded-up value (ceiling) of 0.25 is 1.0.
Round-to-nearest attributes
In this section, we discuss three attributes used for rounding:
Rounding
, Unbiased_Rounding
, Machine_Rounding
In all cases, the rounding attributes return the nearest integer value (as a
floating-point value). For example, the rounded value for 4.8 is 5.0 because 5
is the closest integer value.
Let's see a code example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Roundings is begin Put_Line ("Float'Rounding (0.5): " & Float'Rounding (0.5)'Image); Put_Line ("Float'Rounding (1.5): " & Float'Rounding (1.5)'Image); Put_Line ("Float'Rounding (4.5): " & Float'Rounding (4.5)'Image); Put_Line ("Float'Rounding (-4.5): " & Float'Rounding (-4.5)'Image); Put_Line ("Float'Unbiased_Rounding (0.5): " & Float'Unbiased_Rounding (0.5)'Image); Put_Line ("Float'Unbiased_Rounding (1.5): " & Float'Unbiased_Rounding (1.5)'Image); Put_Line ("Float'Machine_Rounding (0.5): " & Float'Machine_Rounding (0.5)'Image); Put_Line ("Float'Machine_Rounding (1.5): " & Float'Machine_Rounding (1.5)'Image); end Show_Roundings;
The difference between these attributes is the way they handle the case when a value is exactly in between two integer values. For example, 4.5 could be rounded up to 5.0 or rounded down to 4.0. This is the way each rounding attribute works in this case:
Rounding
rounds away from zero. Positive floating-point values are rounded up, while negative floating-point values are rounded down when the value is between two integer values. For example:4.5 is rounded-up to 5.0, i.e.
Float'Rounding (4.5) = Float'Ceiling (4.5) = 5.0
.-4.5 is rounded-down to -5.0, i.e.
Float'Rounding (-4.5) = Float'Floor (-4.5) = -5.0
.
Unbiased_Rounding
rounds toward the even integer. For example,Float'Unbiased_Rounding (0.5) = 0.0
because zero is the closest even integer, whileFloat'Unbiased_Rounding (1.5) = 2.0
because two is the closest even integer.
Machine_Rounding
uses the most appropriate rounding instruction available on the target platform. While this rounding attribute can potentially have the best performance, its result may be non-portable. For example, whether the rounding of 4.5 becomes 4.0 or 5.0 depends on the target platform.If an algorithm depends on a specific rounding behavior, it's best to avoid the
Machine_Rounding
attribute. On the other hand, if the rounding behavior won't have a significant impact on the results, we can safely use this attribute.
Attributes: Truncation
, Remainder
, Adjacent
The Truncation
attribute returns the truncated value of a
floating-point value, i.e. the value corresponding to the integer part of a
number rounded toward zero. This corresponds to the number before the radix
point. For example, the truncation of 1.55 is 1.0 because the integer part of
1.55 is 1.
The Remainder
attribute returns the remainder part of a division. For
example, Float'Remainder (1.25, 0.5) = 0.25
. Let's briefly discuss the
details of this operations. The result of the division 1.25 / 0.5 is 2.5. Here,
1.25 is the dividend and 0.5 is the divisor. The quotient and remainder of this
division are 2 and 0.25, respectively. (Here, the quotient is an integer number,
and the remainder is the floating-point part that remains.)
Note that the relation between quotient and remainder is defined in such a way that we get the original dividend back when we use the formula: "quotient x divisor + remainder = dividend". For the previous example, this means 2 x 0.5 + 0.25 = 1.25.
The Adjacent
attribute is the next machine value towards another value.
For example, on a typical PC, the adjacent value of a small value —
say, 1.0 x 10-83 — towards zero is +0.0, while the adjacent
value of this small value towards 1.0 is another small, but greater value
— in fact, it's 1.40130 x 10-45. Note that the first parameter
of the Adjacent
attribute is the value we want to analyze and the
second parameter is the Towards
value.
Let's see a code example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Truncation_Remainder_Adjacent is begin Put_Line ("Float'Truncation (1.55): " & Float'Truncation (1.55)'Image); Put_Line ("Float'Truncation (-1.55): " & Float'Truncation (-1.55)'Image); Put_Line ("Float'Remainder (1.25, 0.25): " & Float'Remainder (1.25, 0.25)'Image); Put_Line ("Float'Remainder (1.25, 0.5): " & Float'Remainder (1.25, 0.5)'Image); Put_Line ("Float'Remainder (1.25, 1.0): " & Float'Remainder (1.25, 1.0)'Image); Put_Line ("Float'Remainder (1.25, 2.0): " & Float'Remainder (1.25, 2.0)'Image); Put_Line ("Float'Adjacent (1.0e-83, 0.0): " & Float'Adjacent (1.0e-83, 0.0)'Image); Put_Line ("Float'Adjacent (1.0e-83, 1.0): " & Float'Adjacent (1.0e-83, 1.0)'Image); end Show_Truncation_Remainder_Adjacent;
Attributes: Copy_Sign
and Leading_Part
Copy_Sign
is an attribute that returns a value where the sign of the
second floating-point argument is multiplied by the magnitude of the first
floating-point argument. For example, Float'Copy_Sign (1.0, -10.0)
is
-1.0. Here, the sign of the second argument (-10.0) is multiplied by the
magnitude of the first argument (1.0), so the result is -1.0.
Leading_Part
is an attribute that returns the approximated version of
the mantissa of a value based on the specified number of leading bits for the
mantissa. Let's see some examples:
Float'Leading_Part (3.1416, 1)
is 2.0 because that's the value we can represent with one leading bit.Note that
Float'Fraction (2.0) = 0.5
— which can be represented with one leading bit in the mantissa — andFloat'Exponent (2.0) = 2
.)
If we increase the number of leading bits of the mantissa to two — by writing
Float'Leading_Part (3.1416, 2)
—, we get 3.0 because that's the value we can represent with two leading bits.If we increase again the number of leading bits to five —
Float'Leading_Part (3.1416, 5)
—, we get 3.125.Note that, in this case
Float'Fraction (3.125) = 0.78125
andFloat'Exponent (3.125) = 2
.The binary mantissa is actually
2#110_0100_0000_0000_0000_0000#
, which can be represented with five leading bits as expected:2#110_01#
.We can get the binary mantissa by calculating
Float'Fraction (3.125) * Float (Float'Machine_Radix) ** (Float'Machine_Mantissa - 1)
and converting the result to binary format. The -1 value in the formula corresponds to the sign bit.
Attention
In this explanation about the Leading_Part
attribute, we're
talking about leading bits. Strictly speaking, however, this is actually a
simplification, and it's only correct if Machine_Radix
is equal to
two — which is the case for most machines. Therefore, in most cases,
the explanation above is perfectly acceptable.
However, if Machine_Radix
is not equal to two, we cannot use the
term "bits" anymore, but rather digits of the Machine_Radix
.
Let's see some examples:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Copy_Sign_Leading_Part_Machine is begin Put_Line ("Float'Copy_Sign (1.0, -10.0): " & Float'Copy_Sign (1.0, -10.0)'Image); Put_Line ("Float'Copy_Sign (-1.0, -10.0): " & Float'Copy_Sign (-1.0, -10.0)'Image); Put_Line ("Float'Copy_Sign (1.0, 10.0): " & Float'Copy_Sign (1.0, 10.0)'Image); Put_Line ("Float'Copy_Sign (1.0, -0.0): " & Float'Copy_Sign (1.0, -0.0)'Image); Put_Line ("Float'Copy_Sign (1.0, 0.0): " & Float'Copy_Sign (1.0, 0.0)'Image); Put_Line ("Float'Leading_Part (1.75, 1): " & Float'Leading_Part (1.75, 1)'Image); Put_Line ("Float'Leading_Part (1.75, 2): " & Float'Leading_Part (1.75, 2)'Image); Put_Line ("Float'Leading_Part (1.75, 3): " & Float'Leading_Part (1.75, 3)'Image); end Show_Copy_Sign_Leading_Part_Machine;
Attribute: Machine
Not every real number is directly representable as a floating-point value on a specific machine. For example, let's take a value such as 1.0 x 1015 (or 1,000,000,000,000,000):
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Float_Value is package F_IO is new Ada.Text_IO.Float_IO (Float); V : Float; begin F_IO.Default_Fore := 3; F_IO.Default_Aft := 1; F_IO.Default_Exp := 0; V := 1.0E+15; Put ("1.0E+15 = "); F_IO.Put (Item => V); New_Line; end Show_Float_Value;
If we run this example on a typical PC, we see that the expected value
1_000_000_000_000_000.0
was displayed as 999_999_986_991_000.0
.
This is because 1.0 x 1015 isn't
directly representable on this machine, so it has to be modified to a value that
is actually representable (on the machine).
This automatic modification we've just described is actually hidden, so to
say, in the assignment. However, we can make it more visible by using the
Machine (X)
attribute, which returns a version of X
that is
representable on the target machine. The Machine (X)
attribute rounds
(or truncates) X
to either one of the adjacent machine numbers for the
specific floating-point type of X
. (Of course, if the real value of
X
is directly representable on the target machine, no modification is
performed.)
In fact, we could rewrite the V := 1.0E+15
assignment of the code example
as V := Float'Machine (1.0E+15)
, as we're never assigning a real value
directly to a floating-pointing variable — instead, we're first
converting it to a version of the real value that is representable on the
target machine. In this case, 999999986991000.0 is a representable version of
the real value 1.0 x 1015. Of course, writing V := 1.0E+15
or
V := Float'Machine (1.0E+15)
doesn't make any difference to the actual
value that is assigned to V
(in the case of this specific target
architecture), as the conversion to a representable value happens automatically
during the assignment to V
.
There are, however, instances where using the Machine
attribute does
make a difference in the result. For example, let's say we want to calculate
the difference between the original real value in our example
(1.0 x 1015) and the actual value that is assigned to V
. We can
do this by using the Machine
attribute in the calculation:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Machine_Attribute is package F_IO is new Ada.Text_IO.Float_IO (Float); V : Float; begin F_IO.Default_Fore := 3; F_IO.Default_Aft := 1; F_IO.Default_Exp := 0; Put_Line ("Original value: 1_000_000_000_000_000.0"); V := 1.0E+15; Put ("Machine value: "); F_IO.Put (Item => V); New_Line; V := 1.0E+15 - Float'Machine (1.0E+15); Put ("Difference: "); F_IO.Put (Item => V); New_Line; end Show_Machine_Attribute;
When we run this example on a typical PC, we see that the difference is roughly 1.3009 x 107. (Actually, the value that we might see is 1.3008896 x 107, which is a version of 1.3009 x 107 that is representable on the target machine.)
When we write 1.0E+15 - Float'Machine (1.0E+15)
:
the first value in the operation is the universal real value 1.0 x 1015, while
the second value in the operation is a version of the universal real value 1.0 x 1015 that is representable on the target machine.
This also means that, in the assignment to V
, we're actually writing
V := Float'Machine (1.0E+15 - Float'Machine (1.0E+15))
, so that:
we first get the intermediate real value that represents the difference between these values; and then
we get a version of this intermediate real value that is representable on the target machine.
This is the reason why we see 1.3008896 x 107 instead of 1.3009 x 107 when we run this application.
Fixed-Point Types
In this section, we discuss various attributes and operations related to fixed-point types.
In the Ada Reference Manual
Attributes of fixed-point types
Attribute: Machine_Radix
Machine_Radix
is an attribute that returns the radix of the hardware
representation of a type. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Fixed_Machine_Radix is type T3_D3 is delta 10.0 ** (-3) digits 3; D : constant := 2.0 ** (-31); type TQ31 is delta D range -1.0 .. 1.0 - D; begin Put_Line ("T3_D3'Machine_Radix: " & T3_D3'Machine_Radix'Image); Put_Line ("TQ31'Machine_Radix: " & TQ31'Machine_Radix'Image); end Show_Fixed_Machine_Radix;
Usually, this value is two, as the radix is based on a binary system.
Attribute: Machine_Rounds
and Machine_Overflows
In this section, we discuss attributes that return Boolean
values
indicating whether a feature is available or not in the target architecture:
Machine_Rounds
is an attribute that indicates what happens when the result of a fixed-point operation is inexact:T'Machine_Rounds = True
: inexact result is rounded;T'Machine_Rounds = False
: inexact result is truncated.
Machine_Overflows
is an attribute that indicates whether aConstraint_Error
is guaranteed to be raised when a fixed-point operation with that type produces an overflow or divide-by-zero.
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Boolean_Attributes is type T3_D3 is delta 10.0 ** (-3) digits 3; D : constant := 2.0 ** (-31); type TQ31 is delta D range -1.0 .. 1.0 - D; begin Put_Line ("T3_D3'Machine_Rounds: " & T3_D3'Machine_Rounds'Image); Put_Line ("TQ31'Machine_Rounds: " & TQ31'Machine_Rounds'Image); Put_Line ("T3_D3'Machine_Overflows: " & T3_D3'Machine_Overflows'Image); Put_Line ("TQ31'Machine_Overflows: " & TQ31'Machine_Overflows'Image); end Show_Boolean_Attributes;
Attribute: Small
and Delta
The Small
and Delta
attributes return numbers that indicate the
numeric precision of a fixed-point type. In many cases, the Small
of a
type T
is equal to the Delta
of that type — i.e.
T'Small = T'Delta
. Let's discuss each attribute and how they distinguish
from each other.
The Delta
attribute returns the value of the delta
that was
used in the type definition. For example, if we declare
type T3_D3 is delta 10.0 ** (-3) digits D
, then the value of
T3_D3'Delta
is the 10.0 ** (-3)
that we used in the type
definition.
The Small
attribute returns the "small" of a type, i.e. the smallest
value used in the machine representation of the type. The small must be at
least equal to or smaller than the delta — in other words, it must
conform to the T'Small <= T'Delta
rule.
For further reading...
The Small
and the Delta
need not actually be small numbers.
They can be arbitrarily large. For instance, they could be 1.0, or 1000.0.
Consider the following example:
package Fixed_Point_Defs is S : constant := 32; Exp : constant := 128; D : constant := 2.0 ** (-S + Exp + 1); type Fixed is delta D range -1.0 * 2.0 ** Exp .. 1.0 * 2.0 ** Exp - D; pragma Assert (Fixed'Size = S); end Fixed_Point_Defs;with Fixed_Point_Defs; use Fixed_Point_Defs; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Fixed_Type_Info is begin Put_Line ("Size : " & Fixed'Size'Image); Put_Line ("Small : " & Fixed'Small'Image); Put_Line ("Delta : " & Fixed'Delta'Image); Put_Line ("First : " & Fixed'First'Image); Put_Line ("Last : " & Fixed'Last'Image); end Show_Fixed_Type_Info;
In this example, the small of the Fixed
type is actually quite
large: 1.5845632502852867529. (Also, the first and the last values
are large: -340,282,366,920,938,463,463,374,607,431,768,211,456.0 and
340,282,366,762,482,138,434,845,932,244,680,310,784.0, or approximately
-3.402838 and 3.402838.)
In this case, if we assign 1 or 1,000 to a variable F
of this type,
the actual value stored in F
is zero. Feel free to try this out!
When we declare an ordinary fixed-point data type, we must specify the delta. Specifying the small, however, is optional:
If the small isn't specified, it is automatically selected by the compiler. In this case, the actual value of the small is an implementation-defined power of two — always following the rule that says:
T'Small <= T'Delta
.If we want, however, to specify the small, we can do that by using the
Small
aspect. In this case, it doesn't need to be a power of two.
For decimal fixed-point types, we cannot specify the small. In this case, it's automatically selected by the compiler, and it's always equal to the delta.
Let's see an example:
package Fixed_Small_Delta is D3 : constant := 10.0 ** (-3); type T3_D3 is delta D3 digits 3; type TD3 is delta D3 range -1.0 .. 1.0 - D3; D31 : constant := 2.0 ** (-31); D15 : constant := 2.0 ** (-15); type TQ31 is delta D31 range -1.0 .. 1.0 - D31; type TQ15 is delta D15 range -1.0 .. 1.0 - D15 with Small => D31; end Fixed_Small_Delta;with Ada.Text_IO; use Ada.Text_IO; with Fixed_Small_Delta; use Fixed_Small_Delta; procedure Show_Fixed_Small_Delta is begin Put_Line ("T3_D3'Small: " & T3_D3'Small'Image); Put_Line ("T3_D3'Delta: " & T3_D3'Delta'Image); Put_Line ("T3_D3'Size: " & T3_D3'Size'Image); Put_Line ("--------------------"); Put_Line ("TD3'Small: " & TD3'Small'Image); Put_Line ("TD3'Delta: " & TD3'Delta'Image); Put_Line ("TD3'Size: " & TD3'Size'Image); Put_Line ("--------------------"); Put_Line ("TQ31'Small: " & TQ31'Small'Image); Put_Line ("TQ31'Delta: " & TQ31'Delta'Image); Put_Line ("TQ32'Size: " & TQ31'Size'Image); Put_Line ("--------------------"); Put_Line ("TQ15'Small: " & TQ15'Small'Image); Put_Line ("TQ15'Delta: " & TQ15'Delta'Image); Put_Line ("TQ15'Size: " & TQ15'Size'Image); end Show_Fixed_Small_Delta;
As we can see in the output of the code example, the Delta
attribute
returns the value we used for delta
in the type definition of the
T3_D3
, TD3
, TQ31
and TQ15
types.
The TD3
type is an ordinary fixed-point type with the the same delta as
the decimal T3_D3
type. In this case, however, TD3'Small
is not
the same as the TD3'Delta
. On a typical desktop PC, TD3'Small
is
2-10, while the delta is 10-3. (Remember that, for ordinary
fixed-point types, if we don't specify the small, it's automatically selected
by the compiler as a power of two smaller than or equal to the delta.)
In the case of the TQ15
type, we're specifying the small by using the
Small
aspect. In this case, the underlying size of the TQ15
type is 32 bits, while the precision we get when operating with this type is
16 bits. Let's see a specific example for this type:
with Ada.Text_IO; use Ada.Text_IO; with Fixed_Small_Delta; use Fixed_Small_Delta; procedure Show_Fixed_Small_Delta is V : TQ15; begin Put_Line ("V'Size: " & V'Size'Image); V := TQ15'Small; Put_Line ("V: " & V'Image); V := TQ15'Delta; Put_Line ("V: " & V'Image); end Show_Fixed_Small_Delta;
In the first assignment, we assign TQ15'Small
(2-31) to
V
. This value is smaller than the type's delta (2-15). Even
though V'Size
is 32 bits, V'Delta
indicates 16-bit precision, and
TQ15'Small
requires 32-bit precision to be represented correctly.
As a result, V
has a value of zero after this assignment.
In contrast, after the second assignment — where we assign
TQ15'Delta
(2-15) to V
— we see, as expected, that
V
has the same value as the delta.
Attributes: Fore
and Aft
The Fore
and Aft
attributes indicate the number of characters
or digits needed for displaying a value in decimal representation. To be more
precise:
The
Fore
attribute refers to the digits before the decimal point and it returns the number of digits plus one for the sign indicator (which is either-
or space), and it's always at least two.The
Aft
attribute returns the number of decimal digits that is needed to represent the delta after the decimal point.
Let's see an example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Fixed_Fore_Aft is type T3_D3 is delta 10.0 ** (-3) digits 3; D : constant := 2.0 ** (-31); type TQ31 is delta D range -1.0 .. 1.0 - D; Dec : constant T3_D3 := -0.123; Fix : constant TQ31 := -TQ31'Delta; begin Put_Line ("T3_D3'Fore: " & T3_D3'Fore'Image); Put_Line ("T3_D3'Aft: " & T3_D3'Aft'Image); Put_Line ("TQ31'Fore: " & TQ31'Fore'Image); Put_Line ("TQ31'Aft: " & TQ31'Aft'Image); Put_Line ("----"); Put_Line ("Dec: " & Dec'Image); Put_Line ("Fix: " & Fix'Image); end Show_Fixed_Fore_Aft;
As we can see in the output of the Dec
and Fix
variables at the
bottom, the value of Fore
is two for both T3_D3
and TQ31
.
This value corresponds to the length of the string "-0" displayed in the output
for these variables (the first two characters of "-0.123" and "-0.0000000005").
The value of Dec'Aft
is three, which matches the number of digits after
the decimal point in "-0.123". Similarly, the value of Fix'Aft
is 10,
which matches the number of digits after the decimal point in "-0.0000000005".
Attributes of decimal fixed-point types
The attributes presented in this subsection are only available for decimal fixed-point types.
Attribute: Digits
Digits
is an attribute that returns the number of significant decimal
digits of a decimal fixed-point subtype. This corresponds to the value that we
use for the digits
in the definition of a decimal fixed-point type.
Let's see an example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Decimal_Digits is type T3_D6 is delta 10.0 ** (-3) digits 6; subtype T3_D2 is T3_D6 digits 2; begin Put_Line ("T3_D6'Digits: " & T3_D6'Digits'Image); Put_Line ("T3_D2'Digits: " & T3_D2'Digits'Image); end Show_Decimal_Digits;
In this example, T3_D6'Digits
is six, which matches the value that we
used for digits
in the type definition of T3_D6
. The same logic
applies for subtypes, as we can see in the value of T3_D2'Digits
. Here,
the value is two, which was used in the declaration of the T3_D2
subtype.
Attribute: Scale
According to the Ada Reference Manual, the Scale
attribute "indicates
the position of the point relative to the rightmost significant digits of
values" of a decimal type. For example:
If the value of
Scale
is two, then there are two decimal digits after the decimal point.If the value of
Scale
is negative, that implies that theDelta
is a power of 10 greater than 1, and it would be the number of zero digits that every value would end in.
The Scale
corresponds to the N used in the delta 10.0 ** (-N)
expression of the type declaration. For example, if we write
delta 10.0 ** (-3)
in the declaration of a type T
, then the value
of T'Scale
is three.
Let's look at this complete example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Decimal_Scale is type TM3_D6 is delta 10.0 ** 3 digits 6; type T3_D6 is delta 10.0 ** (-3) digits 6; type T9_D12 is delta 10.0 ** (-9) digits 12; begin Put_Line ("TM3_D6'Scale: " & TM3_D6'Scale'Image); Put_Line ("T3_D6'Scale: " & T3_D6'Scale'Image); Put_Line ("T9_D12'Scale: " & T9_D12'Scale'Image); end Show_Decimal_Scale;
In this example, we get the following values for the scales:
TM3_D6'Scale = -3
,T3_D6'Scale = 3
,T9_D12 = 9
.
As you can see, the value of Scale
is directly related to the delta
of the corresponding type declaration.
Attribute: Round
The Round
attribute rounds a value of any real type to the nearest
value that is a multiple of the delta of the decimal fixed-point type,
rounding away from zero if exactly between two such multiples.
For example, if we have a type T
with three digits, and we use a value
with 10 digits after the decimal point in a call to T'Round
, the
resulting value will have three digits after the decimal point.
Note that the X
input of an S'Round (X)
call is a universal real
value, while the returned value is of S'Base
type.
Let's look at this example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Decimal_Round is type T3_D3 is delta 10.0 ** (-3) digits 3; begin Put_Line ("T3_D3'Round (0.2774): " & T3_D3'Round (0.2774)'Image); Put_Line ("T3_D3'Round (0.2777): " & T3_D3'Round (0.2777)'Image); end Show_Decimal_Round;
Here, the T3_D3
has a precision of three digits. Therefore, to fit this
precision, 0.2774 is rounded to 0.277, and 0.2777 is rounded to 0.278.
Big Numbers
As we've seen before, we can define numeric types in Ada with a high degree of
precision. However, these normal numeric types in Ada are limited to what
the underlying hardware actually supports. For example, any signed integer
type — whether defined by the language or the user — cannot have a
range greater than that of System.Min_Int .. System.Max_Int
because
those constants reflect the actual hardware's signed integer types. In certain
applications, that precision might not be enough, so we have to rely on
arbitrary-precision arithmetic.
These so-called "big numbers" are limited conceptually only by available
memory, in contrast to the underlying hardware-defined numeric types.
Ada supports two categories of big numbers: big integers and big reals —
both are specified in child packages of the Ada.Numerics.Big_Numbers
package:
Category |
Package |
---|---|
Big Integers |
|
Big Reals |
|
In the Ada Reference Manual
Overview
Let's start with a simple declaration of big numbers:
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Ada.Numerics.Big_Numbers.Big_Integers; use Ada.Numerics.Big_Numbers.Big_Integers; with Ada.Numerics.Big_Numbers.Big_Reals; use Ada.Numerics.Big_Numbers.Big_Reals; procedure Show_Simple_Big_Numbers is BI : Big_Integer; BR : Big_Real; begin BI := 12345678901234567890; BR := 2.0 ** 1234; Put_Line ("BI: " & BI'Image); Put_Line ("BR: " & BR'Image); BI := BI + 1; BR := BR + 1.0; Put_Line ("BI: " & BI'Image); Put_Line ("BR: " & BR'Image); end Show_Simple_Big_Numbers;
In this example, we're declaring the big integer BI
and the big real
BR
, and we're incrementing them by one.
Naturally, we're not limited to using the +
operator (such as in this
example). We can use the same operators on big numbers that we can use with
normal numeric types. In fact, the common unary operators
(+
, -
, abs
) and binary operators (+
, -
,
*
, /
, **
, Min
and Max
) are available to us.
For example:
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Ada.Numerics.Big_Numbers.Big_Integers; use Ada.Numerics.Big_Numbers.Big_Integers; procedure Show_Simple_Big_Numbers_Operators is BI : Big_Integer; begin BI := 12345678901234567890; Put_Line ("BI: " & BI'Image); BI := -BI + BI / 2; BI := BI - BI * 2; Put_Line ("BI: " & BI'Image); end Show_Simple_Big_Numbers_Operators;
In this example, we're applying the four basic operators (+
, -
,
*
, /
) on big integers.
Factorial
A typical example is the factorial: a sequence of the factorial of consecutive small numbers can quickly lead to big numbers. Let's take this implementation as an example:
function Factorial (N : Integer) return Long_Long_Integer;function Factorial (N : Integer) return Long_Long_Integer is Fact : Long_Long_Integer := 1; begin for I in 2 .. N loop Fact := Fact * Long_Long_Integer (I); end loop; return Fact; end Factorial;with Ada.Text_IO; use Ada.Text_IO; with Factorial; procedure Show_Factorial is begin for I in 1 .. 50 loop Put_Line (I'Image & "! = " & Factorial (I)'Image); end loop; end Show_Factorial;
Here, we're using Long_Long_Integer
for the computation and return type
of the Factorial
function. (We're using Long_Long_Integer
because
its range is probably the biggest possible on the machine, although that is not
necessarily so.) The last number we're able to calculate
before getting an exception is 20!, which basically shows the limitation of
standard integers for this kind of algorithm. If we use big integers instead,
we can easily display all numbers up to 50! (and more!):
pragma Ada_2022; with Ada.Numerics.Big_Numbers.Big_Integers; use Ada.Numerics.Big_Numbers.Big_Integers; function Factorial (N : Integer) return Big_Integer;function Factorial (N : Integer) return Big_Integer is Fact : Big_Integer := 1; begin for I in 2 .. N loop Fact := Fact * To_Big_Integer (I); end loop; return Fact; end Factorial;pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Factorial; procedure Show_Big_Number_Factorial is begin for I in 1 .. 50 loop Put_Line (I'Image & "! = " & Factorial (I)'Image); end loop; end Show_Big_Number_Factorial;
As we can see in this example, replacing the Long_Long_Integer
type by
the Big_Integer
type fixes the problem (the runtime exception) that we
had in the previous version.
(Note that we're using the To_Big_Integer
function to convert from
Integer
to Big_Integer
: we discuss these conversions next.)
Note that there is a limit to the upper bounds for big integers. However, this limit isn't dependent on the hardware types — as it's the case for normal numeric types —, but rather compiler specific. In other words, the compiler can decide how much memory it wants to use to represent big integers.
Conversions
Most probably, we want to mix big numbers and standard numbers (i.e. integer and real numbers) in our application. In this section, we talk about the conversion between big numbers and standard types.
Validity
The package specifications of big numbers include subtypes that ensure that a actual value of a big number is valid:
Type |
Subtype for valid values |
---|---|
Big Integers |
|
Big Reals |
|
These subtypes include a contract for this check. For example, this is the
definition of the Valid_Big_Integer
subtype:
subtype Valid_Big_Integer is Big_Integer
with Dynamic_Predicate =>
Is_Valid (Valid_Big_Integer),
Predicate_Failure =>
(raise Program_Error);
Any operation on big numbers is actually performing this validity check (via a
call to the Is_Valid
function). For example, this is the addition
operator for big integers:
function "+" (L, R : Valid_Big_Integer)
return Valid_Big_Integer;
As we can see, both the input values to the operator as well as the return
value are expected to be valid — the Valid_Big_Integer
subtype
triggers this check, so to say. This approach ensures that an algorithm
operating on big numbers won't be using invalid values.
Conversion functions
These are the most important functions to convert between big number and standard types:
Category |
To big number |
From big number |
---|---|---|
Big Integers |
|
|
Big Reals |
|
|
|
|
In the following sections, we discuss these functions in more detail.
Big integer to integer
We use the To_Big_Integer
and To_Integer
functions to convert
back and forth between Big_Integer
and Integer
types:
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Ada.Numerics.Big_Numbers.Big_Integers; use Ada.Numerics.Big_Numbers.Big_Integers; procedure Show_Simple_Big_Integer_Conversion is BI : Big_Integer; I : Integer := 10000; begin BI := To_Big_Integer (I); Put_Line ("BI: " & BI'Image); I := To_Integer (BI + 1); Put_Line ("I: " & I'Image); end Show_Simple_Big_Integer_Conversion;
In addition, we can use the generic Signed_Conversions
and
Unsigned_Conversions
packages to convert between Big_Integer
and
any signed or unsigned integer types:
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Ada.Numerics.Big_Numbers.Big_Integers; use Ada.Numerics.Big_Numbers.Big_Integers; procedure Show_Arbitrary_Big_Integer_Conversion is type Mod_32_Bit is mod 2 ** 32; package Long_Long_Integer_Conversions is new Signed_Conversions (Long_Long_Integer); use Long_Long_Integer_Conversions; package Mod_32_Bit_Conversions is new Unsigned_Conversions (Mod_32_Bit); use Mod_32_Bit_Conversions; BI : Big_Integer; LLI : Long_Long_Integer := 10000; U_32 : Mod_32_Bit := 2 ** 32 + 1; begin BI := To_Big_Integer (LLI); Put_Line ("BI: " & BI'Image); LLI := From_Big_Integer (BI + 1); Put_Line ("LLI: " & LLI'Image); BI := To_Big_Integer (U_32); Put_Line ("BI: " & BI'Image); U_32 := From_Big_Integer (BI + 1); Put_Line ("U_32: " & U_32'Image); end Show_Arbitrary_Big_Integer_Conversion;
In this examples, we declare the Long_Long_Integer_Conversions
and the
Mod_32_Bit_Conversions
to be able to convert between big integers and
the Long_Long_Integer
and the Mod_32_Bit
types, respectively.
Note that, when converting from big integer to integer, we used the
To_Integer
function, while, when using the instances of the generic
packages, the function is named From_Big_Integer
.
Big real to floating-point types
When converting between big real and floating-point types, we have to
instantiate the generic Float_Conversions
package:
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Ada.Numerics.Big_Numbers.Big_Reals; use Ada.Numerics.Big_Numbers.Big_Reals; procedure Show_Big_Real_Floating_Point_Conversion is type D10 is digits 10; package D10_Conversions is new Float_Conversions (D10); use D10_Conversions; package Long_Float_Conversions is new Float_Conversions (Long_Float); use Long_Float_Conversions; BR : Big_Real; LF : Long_Float := 2.0; F10 : D10 := 1.999; begin BR := To_Big_Real (LF); Put_Line ("BR: " & BR'Image); LF := From_Big_Real (BR + 1.0); Put_Line ("LF