Introduction

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

If you use the excellent GPS 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 GPS, 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?

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 loop

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

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

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);

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

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);

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

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!


Navigation