Introduction
It's that time again. A new semester is starting and CIS students all over the country are beginning to program. It can be an overwhelming experience trying to decipher syntax while learning to code an assignment effectively. There are many practices I see that cause beginning programming students to suffer through their assignments. I'm going to outline those problems in this article.
I'll use a short typical programming assignment from a first semester class to show the student how to analyze the problem and break it into manageable parts. This assignment converts from one temperature to another, Kelvin, Celsius and Farenheit.
Analysis 101
The biggest mistake I see, besides not checking return codes, is students starting to code without any analysis of the problem. They'll write some code, figure it works, and write some more. Soon a monolithic program is ready to compile and they wonder why there are so many errors. They followed their instructors guidelines to the letter. They've copied the code from the worksheet the instructor has been using as an example for the last 20 semesters. Never mind that the example has nothing to do with the current assignment. Or the specification is deliberately vague in some parts to make the student think about the task. Get used to vague specifications, they are a way of life in development. In a real project a developer will clarify any ambiguities with the client. In academia that client is your instructor. Ask him/her to clarify anything that seems unclear. There's no dishonor in asking questions. Assumption is the mother of all dead programs.
Save yourself the grief of the monolithic program and analyze the problem before coding. Identify the expected output first. This is the end game. This is what we're working toward. Next, identify the data you'll need to complete the task. Then, how is the data entered; read from storage, direct user input or can it be hard coded? Next, decide what tasks get from raw data to the expected output. Finally, break down the tasks into small functions/methods that complete one portion of the task.
The End Game
The specification says convert one temperature scale to another. It doesn't say how the data is entered or whether we convert to all the other scales or not. We'll clarify this later, right now the end game will be this:
- Given a Kelvin temperature be able to convert to Farenheit and Celsius
- Given a Celcius temperature be able to convert to Farenheit and Kelvin
- Given a Farenheit temperature be able to convert to Celsius and Kelvin
The calculations for each of these conversions is well known so I won't outline them here. If you need some help with the math look up Celsius on Wiki.
What's Data Got To Do With It
First we need the temperature, then we need the scale to convert from and then the scale to convert to. The specification isn't clear on these points so we ask the instructor a series of questions:
- Where is the data coming from, user, storage or hard coded values?
- What is the format of the input: 247.67K or 247.67 K etc...?
- Do we ask which scale to convert to or just convert to all the other scales?
For this particular assignment the instructor wants user input. Input should be checked for correctness, no negative Kelvins and no values less than absolute zero. The values for input Temperature will be of the form 247.67 K, 34.56 C and 56.56 F. Data input can be seperated for Temperature and Scale. The letters need to be output in uppercase to match standard conventions but the user should be free to enter lowercase letters. Don't do same scale conversions like Kelvins to Kelvins.
How Do We Get There From Here
The tasks we'll need to complete are:
- Get user entry of Temperature and Scale
- Validate Scale entry
- Validate Temperature entry
- Get user entry of Scale to convert to
- Validate Scale entry
- Convert Temperature
- Display Results
Here is where another mistake is made by beginning developers. The input and output formats are known, the conversions are known, let's start coding. Generally this approach winds up with all the code stuffed into the Main section of the program with no breakdown of tasks, therefore no reuse except by cut and paste. Testing becomes a headache because the developer has to wade through all this code just to identify a problem area. Changes mean all paths have to be tested to ensure nothing gets broken. The best motto to follow at this point is "Code Small, Test Small". What this means is write compact fully self contained functions or modules that do ONE task, do it well and then move on.
Break It Down, Break It Down
Notice from the tasks in the previous section that validation is done twice for Scale entries. In a monolithic program duplication of code is the result. Make this a function that validates only the correct Scale entries C,F and K. With a little work a generic function that can be reused can be created for this purpose. One solution is to pass in the Scale entry and a string of valid entries. Another solution might also include the return code from the data entry function as a parameter to ensure all the data we expected is entered correctly.
Next tackle the validation for Temperature. Make a function that checks for valid temperatures in different scales. One solution passes in Temperature and Scale and with the use of some simple control statements can complete the task. Adding the return code could be an option as well.
Now for the data entry. The programmer might decide to write two functions, one for Temperature and Scale, the other for just Scale. This is an acceptable solution but a more generic solution might take one more parameter and be able to get either Temperature or Scale or Both in a single function. The function should return the number of successful entries completed.
The conversion process might look daunting at first, a programmer might think that six different functions need to be created. Upon closer inspection though only four are needed:
- KelvinToCelsius - Takes a Kelvin temperature and returns Celsius
- CelsiusToKelvin - Takes a Celsius temperature and returns Kelvin
- CelsiusToFarenh eit - Takes a Celsius temperature and returns Farenheit
- FarenheitToCels ius - Takes a Farenheit temperature ans returns Celsius
The Kelvin functions are so trivial that they could be inline calculations, just adding or subtracting 273.15, thus reducing the conversion functions to just two.
This is Only a Test
The last problem encountered by beginners is not testing each individual task. Many times a programmer will integrate some function without testing it, only to see a program break. Use a simple test Main to check a function before integrating it into a bigger program. Unit testing frameworks are great as well.
Dawn of the Decomposition
Our program has the following functions:
- Function MainFunction
- GetTemperatureS cale( Out Temperature, Out Scale, In Which ) As Integer
- Function IsValidTemperat ureScale( In Temperature, In Scale, In Which ) As Boolean
- Function ConvertTemperat ureScale( In TemperatureFrom , In ScaleFrom, In TemperatureTo, In ScaleTo) As Real
- Function CelsiusToFarenh eit( In Celsius ) As Real
- Function FarenheitToCels ius( In Farenheit ) As Real
- Function ShowResult( In Temp1, In Scale1, In Temp2, In Scale2 )
Note: The pseudocode above uses Out for pass by reference and In for pass by value. The As clause is the return type. We also decided to inline the Kelvin conversions.
Summary
In the Temperature example above we identified the result needed, identified the data, created tasks to go from raw data to result. We also worked out some ambiguous requirements with our client by asking a few simple questions before beginning to code a solution. The tasks identified were coded in such a manner that reuse in another program is simple. Testing in small increments meant less code to worry about as we progressed from task to task. The completed program is easier to follow and debug in the long term.
Solutions
Expand|Select|Wrap|Line Numbers
- ///////////////////////////////////////
- /// \file main.c
- /// \brief Main module for Temperature program
- ///
- /// \author John `Ghost' Wicks
- /// \version 1.0.0
- /// \date 2008-08-08
- ///
- ///////////////////////////////////////
- #include <stdio.h>
- #include <ctype.h>
- int getTemperatureScale( double* temp, char* scale, int which );
- int isValidTemperatureScale( double temp, char scale, int which, int howMany );
- double convertTemperatureScale( double tempFrom, char scaleFrom, char scaleTo );
- double celsiusToFarenheit( double temp );
- double farenheitToCelsius( double temp );
- void showResults( double tempFrom, char scaleFrom, double tempTo, char scaleTo );
- /////////////////////////
- /// \fn main(int argc, char** argv)
- /// \brief Main module for Temperature conversion program.
- ///
- /// \param argc - integer number of arguments passed from system to program
- /// \param argv - pointer to array of characters of arguments passed to program from system
- ///
- /// \return integer value for success or failure
- /////////////////////////
- int main( int argc, char* argv[] )
- {
- int retVal = 0;
- int r = 0;
- double tempFrom = 0.0, tempTo = 0.0;
- char scaleFrom, scaleTo;
- do{
- r = getTemperatureScale( &tempFrom, &scaleFrom, 2 );
- }while(!isValidTemperatureScale( tempFrom, scaleFrom, 2, r ));
- do{
- r = getTemperatureScale( &tempTo, &scaleTo, 0 );
- }while(!isValidTemperatureScale( tempTo, scaleTo, 0, r ));
- showResults( tempFrom, scaleFrom, tempTo, scaleTo );
- return retVal;
- }
- /////////////////////////
- /// \fn getTemperatureScale( double* temp, char* scale, int which )
- /// \brief Gets data from user regarding temperature
- ///
- /// \param temp - pointer to storage for temperature value
- /// \param scale - pointer to storage for temperature scale
- /// \param which - allows multiuse of function for just scale (0) or temperature (1) or both (2)
- ///
- /// \return integer returned from scanf
- /////////////////////////
- int getTemperatureScale( double* temp, char* scale, int which )
- {
- int retVal = 0;
- switch( which ){
- case 2:
- printf("Enter the Temperature and Scale (CFK) (Example 270.15 K): ");
- retVal = scanf_s(" %lf %c^%*", temp, scale);
- break;
- case 1:
- printf("Enter Temperature : ");
- retVal = scanf_s(" %lf^%*", temp);
- break;
- default:
- printf("Enter Scale : ");
- retVal = scanf_s(" %c^%*", scale);
- break;
- }
- return retVal;
- }
- /////////////////////////
- /// \fn isValidTemperatureScale( double temp, char scale, int which, int howMany )
- /// \brief Checks if scale or temperature are invalid
- ///
- /// \param temp - temperature value to check
- /// \param scale - scale to check
- /// \param which - allows multiuse of function for just scale (0) or temperature (1) or both (2)
- /// \param howMany - check to see if all expected values were correctly entered from scanf function
- ///
- /// \return integer value for success (1) or failure (0) of validation check
- /////////////////////////
- int isValidTemperatureScale( double temp, char scale, int which, int howMany )
- {
- int retVal = 0;
- switch( which ){
- case 2:
- if( howMany == which ){
- switch( toupper(scale) ){
- case 'C':
- if( temp >= -273.15 ) retVal = 1;
- break;
- case 'F':
- if( temp >= -459.67 ) retVal = 1;
- break;
- default: // Kelvin
- if( temp >= 0 ) retVal = 1;
- break;
- }
- }
- break;
- case 1:
- //Can't check Temperature without knowing scale
- break;
- default:
- if( howMany == 1 ){
- switch( toupper(scale) ){
- case 'C':
- case 'F':
- case 'K':
- retVal = 1;
- break;
- default:
- break;
- }
- }
- break;
- }
- return retVal;
- }
- /////////////////////////
- /// \fn convertTemperatureScale( double tempFrom, char scaleFrom, char scaleTo )
- /// \brief Converts from one temperature scale to another
- ///
- /// \param tempFrom - temperature value to convert from
- /// \param scaleFrom - scale to convert from
- /// \param scaleTo - scale to convert to
- ///
- /// \return double value of temperature converted to
- /////////////////////////
- double convertTemperatureScale( double tempFrom, char scaleFrom, char scaleTo )
- {
- double retVal = 0.0;
- switch( toupper(scaleTo) ){
- case 'C':
- if( toupper(scaleFrom) == 'K') retVal = tempFrom - 273.15;
- else
- retVal = farenheitToCelsius( tempFrom );
- break;
- case 'F':
- if( toupper(scaleFrom) == 'K') retVal = celsiusToFarenheit(tempFrom - 273.15);
- else
- retVal = celsiusToFarenheit( tempFrom );
- break;
- case 'K':
- if( toupper(scaleFrom) == 'C' ) retVal = tempFrom + 273.15;
- else
- retVal = farenheitToCelsius( tempFrom ) + 273.15;
- break;
- default:
- break;
- }
- return retVal;
- }
- /////////////////////////
- /// \fn celsiusToFarenheit( double temp )
- /// \brief Converts from Celsius temperature scale to Farenheit scale
- ///
- /// \param temp - Celsius value to convert
- ///
- /// \return double value in Farenheit scale temperature
- /////////////////////////
- double celsiusToFarenheit( double temp )
- {
- double retVal = 0.0;
- retVal = 9/5 * temp+32;
- return retVal;
- }
- /////////////////////////
- /// \fn farenheitToCelsius( double temp )
- /// \brief Converts from Farenheit temperature scale to Celsius scale
- ///
- /// \param temp Farenheit value to convert
- ///
- /// \return double value of Celsius scale temperature
- /////////////////////////
- double farenheitToCelsius( double temp )
- {
- double retVal = 0.0;
- retVal = (temp - 32) * 5/9;
- return retVal;
- }
- /////////////////////////
- /// \fn showResults( double tempFrom, char scaleFrom, double tempTo, char scaleTo )
- /// \brief Shows the converted temperature value
- ///
- /// \param tempFrom - temperature value converted from
- /// \param scaleFrom - scale to converted from
- /// \param tempTo - temperature value converted to
- /// \param scaleTo - scale to converted to
- ///
- /// \return none
- /////////////////////////
- void showResults( double tempFrom, char scaleFrom, double tempTo, char scaleTo )
- {
- printf("Original Temperature: %.2lf %c\n", tempFrom, toupper(scaleFrom));
- printf("Scale To: %c\n", toupper(scaleTo));
- if( toupper(scaleFrom) != toupper(scaleTo) ){
- printf("Converting...\n");
- tempTo = convertTemperatureScale( tempFrom, scaleFrom, scaleTo );
- printf("Converted Temperature: %.2lf %c\n", tempTo, toupper(scaleTo));
- }
- else{
- printf("No conversion needed...\n");
- }
- }