Ada Programming/Input Output/Text Example
Introduction
editThose of us with American friends often have to deal with the Fahrenheit temperature scale. While the rest of the world has adopted the Celsius temperature scale, America and a few other countries have stayed true to Fahrenheit.
While this is both a small and insignificant problem when dealing with friends, it is nevertheless a problem worthy of having its own program: A Fahrenheit to Celsius converter.
The necessary files and directories
editBefore we start, we need to do a little preparation. First create the following directory/file structure somewhere on your computer:
$ mkdir FtoC
$ cd FtoC
$ mkdir exe objects
$ touch ftoc.adb ftoc.gpr
The above should leave you with the following contents in the FtoC
directory:
exe/ objects/ ftoc.adb ftoc.gpr
We will use these two directories and files in the following manner:
- exe/ This is the directory where the
ftoc
executable is placed when compiling the program - objects/ When compiling a program, the compiler creates ALI files, object files and tree files. These files are placed in this directory.
- ftoc.adb The main Ada source file for the ftoc program.
- ftoc.gpr This is an Ada project file. This file controls various properties of the program, such as how to compile it, what sources to include, where to put things, and so on.
The Project File
editAdd this to the ftoc.gpr
file:
project ftoc is
for Source_Dirs use (".");
for Main use ("ftoc.adb");
for Exec_Dir use "exe";
for Object_Dir use "objects";
package Ide is
for Compiler_Command ("ada") use "/usr/gnat/bin/gnatmake";
end Ide;
package Compiler is
Common_Options := ("-gnatwa",
"-gnaty3abcdefhiklmnoprstux",
"-Wall",
"-O2");
for Default_Switches ("Ada") use Common_Options;
end Compiler;
end ftoc;
Please go here for an explanation on the different parts of the above project file.
The Actual Program
editWith the project file out of the way, we turn our attention to the actual ftoc
code. Add this to the ftoc.adb
file:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
procedure FtoC is
subtype Fahrenheit_Degree_Range is Natural range 0 .. 212;
-- Now, that is a Natural Range for Ada, but I can tell you that Americans
-- are not nearly so happy with "0" and not even an Icelander likes it at "212"!
Fahr : Fahrenheit_Degree_Range := Fahrenheit_Degree_Range'First;
Factor : constant := 5.0 / 9.0;
Offset : constant := 32;
Step : constant := 1;
begin
loop
Put (Item => Fahr, Width => Fahrenheit_Degree_Range'Width);
Put (Item => Factor* Float (Fahr - Offset),
Fore => 4,
Aft => 2,
Exp => 0);
New_Line;
exit when Fahr = Fahrenheit_Degree_Range'Last;
Fahr := Fahr + Step;
end loop;
end FtoC;
Save the file, and lets move on to the final step: Compiling.
Compile And Run FtoC
editIf you use the excellent GNAT Studio IDE, the compiling is a simple matter of pressing F4
to compile the project, and Shift+F2
to execute it. If you're not using this IDE, then do this instead:
$ gnatmake -P ftoc.gpr
You should see some output scroll by:
gcc -c -gnatwa -gnaty3abcdefhiklmnoprstux -Wall -O2 -I- -gnatA /home/thomas/FtoC/ftoc.adb gnatbind -I- -x /home/thomas/FtoC/objects/ftoc.ali gnatlink /home/thomas/FtoC/objects/ftoc.ali -o /home/thomas/FtoC/exe/ftoc
You now have an executable in the exe
/ directory. When you run it, you get this (somewhat abbreviated here):
0 -17.78 1 -17.22 2 -16.67 ... 26 -3.33 27 -2.78 28 -2.22 29 -1.67 30 -1.11 31 -0.56 32 0.00 33 0.56 34 1.11 35 1.67 ... 48 8.89 49 9.44 50 10.00 51 10.56 52 11.11 ... 67 19.44 68 20.00 69 20.56 70 21.11 71 21.67 ... 84 28.89 85 29.44 86 30.00 87 30.56 ... 210 98.89 211 99.44 212 100.00
Now we know that 0 degrees Fahrenheit equals -17.78 Celsius and at 212 degrees Fahrenheit water boils, which to us Celsius users is known as 100 degrees Celsius. The program works! Lets go over it in detail, to figure out how it works.
Note: All output in the following examples will be heavily abbreviated, because printing 213 lines of output for every little example is madness.
How Does The FtoC Program Work?
editLets start with the first three lines:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
These are called with
clauses and use
declarations. An Ada with
clause can be likened to a C #include
. When you see with Ada.Text_IO
, it means the utilities of the Ada.Text_IO
package are being made available to the program. When next you see the declaration use Ada.Text_IO
, it means that the utilities of Ada.Text_IO
are directly visible to the program, ie. you do not have to write
Ada.Text_IO.Put ("No use clause");
to call the Put
procedure. Instead you simply do
Put ("With use clause");
In the FtoC program we utilize, and make visible, three packages: Ada.Text_IO
, Ada.Integer_Text_IO
and Ada.Flot_Text_IO
. These packages provide exactly what their names imply: Text IO capabilities for different types.
Looking at the source, you might wonder where the need for Ada.Text_IO
comes in, as we're only outputting numbers. We will get to that in a minute, so stay tuned.
The FtoC declarations
editNext we have this code:
procedure FtoC is
subtype Fahrenheit_Degree_Range is Natural range 0 .. 212;
Fahr : Fahrenheit_Degree_Range := Fahrenheit_Degree_Range'First;
Factor : constant := 5.0 / 9.0;
Offset : constant := 32;
Step : constant := 1;
begin
This is called the declarative part of an Ada program. It is here we declare our variables, constants, types and whatnot. In the case of FtoC we have 5 declarations. Lets talk a bit about the first two:
subtype Fahrenheit_Degree_Range is Natural range 0 .. 212;
Fahr : Fahrenheit_Degree_Range := Fahrenheit_Degree_Range'First;
That right there is one of the biggest selling points of Ada: The ability to create your own types, with your own constraints. It might not seem like a big deal but, believe me, it is. In this case we've created a new subtype of the built-in subtype Natural (3.5.4 [Annotated]). We've constrained the type's range to 0 .. 212
, meaning that objects declared as a Fahrenheit_Degree_Range
can never go below 0 or above 212. If a value outside this range is assigned to an object of the Fahrenheit_Degree_Range
type, a Constraint_Error
exception is raised.
With the Fahrenheit_Degree_Range
type in place, we direct our attention to the declaration and assignment of the Fahr
variable. If you've never seen this syntax before or it makes no sense to you, please read the Constants article on this Wiki, and then return here.
The Fahr
variable is of the Fahrenheit_Degree_Range
type and it's initial value is 0. Why 0? Because we assign it the value Fahrenheit_Degree_Range'First
, and the 'First
part equals the first value in the range constraint of the type, in this case 0. Consequently the value for Fahrenheit_Degree_Range'Last
is 212.
Let's take a look at the final three object declarations:
Factor : constant := 5.0 / 9.0;
Offset : constant := 32;
Step : constant := 1;
What's going on here? Why are there no type
associated with any of those declarations? Well, if you've read the Constants article, you will know that these constants are called named numbers and their type
is called an universal type (3.4.1 [Annotated]). Named numbers can be of any size and precision.
The Factor
and Offset
constants are used in the Fahrenheit-to-Celsius calculation, and Step
define how many conversions we do in the program.
The FtoC body
editWith the declarations out of the way, we turn our attention to the body of the program:
begin
loop
Put (Item => Fahr, Width => Fahrenheit_Degree_Range'Width);
Put (Item => Factor* Float (Fahr - Offset),
Fore => 4,
Aft => 2,
Exp => 0);
New_Line;
exit when Fahr = Fahrenheit_Degree_Range'Last;
Fahr := Fahr + Step;
end loop;
end FtoC;
The reserved word begin
signifies the beginning of the body
and that same body ends with the final end FtoC
. Between those two, we have a bunch of statements.
The loop
editThe first one is the loop
statement. Loops come in many different shapes in Ada, each of which obeys the basic premise of
loop
-- some statements
end loop;
Loops can be terminated using the exit
keyword:
loop
-- some statements
if X = Y then
exit;
end if;
end loop;
This construction is so common that a shorter version has been made available:
loop
-- some statements
exit when X = Y;
end loop;
It is this last version we use in the FtoC program and we exit the loop when Fahr
equals the 'Last
value of the Fahrenheit_Degree_Range
type.
FtoC output - putting integers on the screen
editImmediately after the loop
statement we encounter the Put
procedure:
Put (Item => Fahr, Width => Fahrenheit_Degree_Range'Width);
We know from the declaration that Fahr
is a subtype of Natural
, which in turn is a subtype of Integer
, so the call to Put
on this line actually calls Ada.Integer_Text_IO.Put
. As you can see, we give Put
two parameters: Item
and Width
. The meaning of Item
should be obvious: It's the integer we want to output, in our case the Fahr
variable.
Width
on the other hand does not make as much sense. The Width
parameter gives the minimum number of characters required to output the integer type or, in our case, subtype, as a literal, plus one for a possible negative sign. If the Width
parameter is set too high, the integer literal is padded with whitespace. If it's set too low, it is automatically expanded as necessary. Setting the Width
parameter to 0, results in the field being the minimum width required to contain the integer.
The 'Width
attribute returns the maximum width of the type, so if we change the Fahrenheit_Degree_Range
later on, we wouldn't have to do a single thing about this call to Put
; it would simply adjust itself accordingly.
Let's do some tests with various Width
parameters:
Put (Item => Fahr); -- Default Width parameter
The output now looks like this:
0 -17.78 1 -17.22 2 -16.67 3 -16.11 4 -15.56 ...
Lots of wasted space there. This is because Put
now sets aside space for the Integer
, which is the type Fahrenheit_Degree_Range
is derived from. Let's try with 0:
Put (Item => Fahr, Width => 0); -- Minimum required characters for the integer
And the output:
0 -17.78 1 -17.22 2 -16.67 3 -16.11 9 -12.78 10 -12.22 11 -11.67 99 37.22 100 37.78 101 38.33 ...
Unfortunately for readability, the Fahr
integer literal is no longer right-justified. Each number is given the exact width necessary to hold it and no more.
Let's try it with a Width
that is wide enough for some of the Fahr
values, but not all of them:
Put (Item => Fahr, Width => 2); -- Minimum width of 2. Expands if necessary
This outputs:
0 -17.78 1 -17.22 2 -16.67 9 -12.78 10 -12.22 11 -11.67 98 36.67 99 37.22 100 37.78 101 38.33 102 38.89 103 39.44 ...
As you can see, the single digit values are right-justified and padded with 1 space, the two-digit values come out even, but the rest of the results are expanded to hold the third character.
FtoC output - now with floats
editWith Put
for integer types out of the way, we move on to the next Put
>
Put (Item => Factor* Float (Fahr - Offset),
Fore => 4,
Aft => 2,
Exp => 0);
The Item
parameter for this call to Put
is a float, because Factor
is a named number that contains a decimal point and the expression Fahr - Offset
is converted to a float using the Float (Fahr - Offset)
expression. So when calling this Put
, we're actually calling Ada.Float_Text_IO.Put
.
The Fore
parameter gives the minimum character count necessary to output the value preceding the decimal point. As with Width
for the integer types, Fore
will automatically expand if necessary. Aft
sets the precision after the decimal point, in this case 2. And finally Exp
sets the exponent field size. A value of Exp => 0
signifies that no exponent will be output. Anything other than zero will output the exponent symbol "E", a +/-, and the digit(s) of the exponent. Note: The value of Exp
should not be less than zero!
Let's try a few different combinations:
Put (Item => Factor* Float (Fahr - Offset),
Fore => 10,
Aft => 4,
Exp => 0);
The output:
0 -17.7778 1 -17.2222 15 -9.4444 16 -8.8889 17 -8.3333 26 -3.3333 27 -2.7778 100 37.7778 101 38.3333 102 38.8889 103 39.4444 104 40.0000 ...
Or how about this:
Put (Item => Factor* Float (Fahr - Offset),
Fore => 4,
Aft => 2,
Exp => 1);
And the output:
0 -1.78E+1 1 -1.72E+1 2 -1.67E+1 8 -1.33E+1 9 -1.28E+1 10 -1.22E+1 11 -1.17E+1 18 -7.78E+0 37 2.78E+0 98 3.67E+1 99 3.72E+1 100 3.78E+1 101 3.83E+1 ...
And finally:
Put (Item => Factor* Float (Fahr - Offset),
Fore => 0,
Aft => 1,
Exp => 0);
This outputs:
0-17.8 8-13.3 9-12.8 10-12.2 11-11.7 31-0.6 320.0 499.4 9836.7 9937.2 10037.8 10138.3 10238.9 ...
Which obviously isn't very pretty to look at.
A new line, an exit strategy, and a step
editThe last four lines of the FtoC program finish our formatting, get us out of here if we are done, and, if not, set the next value of Fahr
to be converted:
New_Line;
exit when Fahr = Fahrenheit_Degree_Range'Last;
Fahr := Fahr + Step;
end FtoC;
The single call to New_Line
is the reason we have to make Ada.Text_IO
available to the FtoC program using with Ada.Text_IO
. What New_Line
does is output a single line feed. Running the program without this call to New_Line
would result in output looking like this:
0 -17.78 1 -17.22 2 -16.67 3 -16.11 4 -15.56 5 -15.00 6 -14.44 7 -13.89 8 -13.33 9 -12.78 10 -12.22 11 -11.67 ...
The New_Line
procedure accepts a Spacing
parameter, meaning you can do this to output consecutive line feeds:
New_Line (Spacing => 5);
Or simply
New_Line (5);
We've already discussed the exit when
method of terminating a loop, and the final statement is merely a simple counter. The value of Fahr
is incremented with Step
on each iteration of the loop. When Fahr
equals Fahrenheit_Degree_Range'Last
, the loop is terminated.
The final line, end Ftoc;
, signifies the end of the program. There's nothing more to do and nothing more to see. Control is handed back to whatever called the program in the first place, and life goes on.
Conclusion
editI hope you've enjoyed this little tutorial on building a Fahrenheit to Celsius conversion table. It is left to the reader to figure out how to add Kelvin to the mix. Have fun!