Table of Contents
Those 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
Before 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
Add 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
With 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
$ 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?
Lets 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
Next 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. 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 Variables and 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 Variables and Constants article, you will know that these constants are called named numbers and their type is called an universal type. 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
With 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 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
Immediately 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
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
With 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);
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 …
Put (Item => Factor * Float (Fahr - Offset), Fore => 0, Aft => 1, Exp => 0);
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
The 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);
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.
I 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!