Develop a Calculator in C - Part II

Develop a Calculator in C - Part II

Introduction

It's awesome when we put in the effort to build on our knowledge base and to work on advancing a project.

In the first part of our calculator project, we created functions to read input from the keyboard and printed everything onto the screen. It was just straight forward and it didn't involve much complexities.

However, the line of input has a string data type, and this makes it unfavorable to be used in Mathematical computations.

In this second part, we set on the quest for more discovery and advancement of our calculator. The complete source code can be found on GitHub.

Handling Errors

As in any endeavor, there's always the possibility that something will not work as expected. In our case, it may be an input of an unexpected data.

When this happens, we should have a message that will reveal what went wrong for the user to correct. As such, we'll generate an error code for some specific mistakes and have corresponding messages to display.

So, we'll arm our calculator with the following messages and error codes to shoot at the user when they let down their guards on our guiding principles. Each error message begins with a sad-face emoji to notify users that they're making our ASHCulator sad.

/* error messages */
#define ERR_INVALID_INPUT ":( Error: invalid input"
#define ERR_ZERO_DIVISION ":( Error: division by zero"
#define ERR_MISSING_PARENTHESIS ":( Error: missing closing parenthesis"

/* error codes */
#define ERR_INVALID_INPUT_CODE -5
#define ERR_ZERO_DIVISION_CODE -8
#define ERR_MISSING_PARENTHESIS_CODE -9

Updating Struct

In the part one of this tutorial, our struct had only two members; the args, which stores the line of input, and len member for its length.

When we generate an error code and message upon violation of our principle, we'll have to make them available to variables and functions that can access them and display them accordingly to the user.

So, let's update the struct with more members and initializers as shown below.

/* struct definition */
struct var_data
{
    char *args;
    size_t len;
    /* new members */
    char err_input;
    int err_code;
    char *err_msg;
    int index;
    double result;
};

/* initializer for struct */
#define INFO_INIT {NULL, 0, 0, 0, NULL, 0, 0.0}

Now, let's see what each of these new member will do. The err_input has a char data type and can store only one character at a time. The first character of an offending input will be assigned to this variable. The err_code, with int data type will be assigned the error code we generate whenever we encounter an invalid input.

The err_msg has a character string data type, and it's capable of storing words and sentences. This will be assigned an error message as defined above. The index will be used to track the position of each character in the line of input. This will enable us identify the beginning of an offending input, determine which character in the input can be operated on with the Mathematics operations among others.

Finally, the result member has double as data type. This will store the final answer for a computation and enable us display answers with decimal values.

Converting a Sub-string to Number

The readLine() we defined in the previous part reads a line of input from stdin and stores a reference to it in a character string, args in our case. This input has character string as data type. Unlikely, operators such as addition, subtraction and the others in our calculator cannot operate on string data types.

Therefore, we need to convert the string input to data types that these operators can operate on. We’ll start by converting each character to the preferred data type.

As our program executes in a loop, each iteration will convert a character at a time. The following function converts a section of the input to a number of type double.

#include <ctype.h>

/* convert values from string to double data type */
double handleSubstrToNumber(const char *args, int *index)
{
    double result = 0.0, frac = 0.1;
    int isNegative = 0;

    if (args[*index] == '-')
    {
        isNegative = 1;
        (*index)++;
    }

    /* handle digits before the dot */
    while (isdigit(args[*index]))
    {
        result = result * 10.0 + (args[*index] - '0');
        (*index)++;
    }

    /* handle digits after the dot in a floating point number */
    if (args[*index] == '.')
    {
        (*index)++;
        while (isdigit(args[*index]))
        {
            result += (args[*index] - '0') * frac;
            frac /= 10.0;
            (*index)++;
        }
    }

    if (isNegative)
        result = -result;

    return (result);
}

Okay, Let’s walk through the function.

Whenever we invoke this function, we'll pass pointers to the line of input and the index value to it. Inside, we first declare variables we’ll need. The result variable will hold the converted subsection. This is different from the struct member.

The frac will be used to handle inputs with decimal parts and the isNegative is a flag. This will track negative values. We could have used the word flag but, isNegative is more definitive of what it does, and so it’s better.

Moving on, we check the character at the current position for a negative sign. If it is, we set the isNegative flag to 1 and increment the index value by 1 to access the next character. We then proceed to convert the character to a number.

In a typical input problem, values are separated by operators and brackets. So, a substring is the value, other than the operators and brackets. In the conversion, we employ the isdigit() function from the standard library to check if the character at the current position falls within the numbers class.

Next, we first subtract the character zero '0' from the character at the current position, add the resulting difference to the product obtained from multiplying the result variable by 10.0, and reassign everything to the result variable. After this, we increment the index value by 1 to access the next character.

This computation repeats until the character at the index does not fall in the numbers class, which will likely be an operator or a bracket and break out of the loop. Upon exiting the loop, the resulting value will be the same as the input value, but now, with a double data type.

Amazing how this works, but we’re not done yet. Some inputs have decimal parts and we need to handle those too. We apply the same approach, but here we utilize the frac variable; we subtract the character zero '0' - same as earlier, from the input at the current position and multiply it by the frac value.

Following, we add the product obtained to the value stored in the result variable and reassign the total value to result variable. We then divide the frac by 10 to keep track of the number of digits after the decimal point and increment the index value by 1 to access the next character.

Finally, we check the isNegative flag. If its value is 1, it means the number we converted was negative and we assign the negative sign to it. Otherwise, we leave it as it is and return the converted value of type double.

Whew! That’s lots of talking. And that’s what programming is about; we’re to know what each line of code does and this is fun. And it's fun because, it’s difficult.

Adding Prototype to List

In the first part of this tutorial, we placed the prototype of the readLine() function at the top of the file, before the main() function. This is necessary as it enables the compiler to know in advance the data types of the function's arguments, if any and also, the data type of the value the function will return, if any.

Omitting this will cause the compiler to complain. So, let's add the prototype of the handleSubstrToNumber() function as shown below:

/* function prototypes */
ssize_t readLine(INFO *);
double handleSubstrToNumber(const char *, int *);

Evaluating the Input

So far, we’ve defined the following functions: main() , readLine() and handleSubstrToNumber() and we know what each does. These functions don’t come near executing the operations on the input problem.

So the next thing we’re going to do is define a function that will serve as the starting point for the application of the operators on the input values. The function is defined as shown below:

/* evaluate data and compute result */
double computeResult(INFO *info)
{
    char *p;

    for (p = info->args; *p != '\0'; p++)
    {
        if (*p == ' ' || *p == '\t')
        {
            info->err_code = ERR_INVALID_INPUT_CODE;
            info->err_input = *p;
            info->err_msg = ERR_INVALID_INPUT;

            return (1);
        }
    }

    return (handleSubstrToNumber(info->args, &info->index));
}

In this function, we use a string pointer variable to access each character in the input line in a loop. At each iteration, we check for a space or a tab character. If we find any, we assign an error code, the offending input and the associated error message to the struct members err_code , err_input and err_msg respectively. We then return 1 immediately to the caller of this function.

In the event there are no blanks, we invoke the function to convert a substring to number in the return statement, thus returning the result to the caller.

Adding Prototype to List

Next, we add this function’s prototype to the list of prototypes as show below:

/* function prototypes */
ssize_t readLine(INFO *);
double handleSubstrToNumber(const char *, int *);
double computeResult(INFO *);

Processing the Input

You may have noticed, by now that, the main function compares the exit\n instruction with the line of input and either exit or print the input on the screen.

To have ASHCulator to actually compute results, we need to process the line of input. And in this section, we construct the processing plant of our calculator as shown below:

/* process data input */
int processInput(INFO *info)
{
    char *p;

    /* get rid of the newline character */
    for (p = info->args; *p != '\0'; p++)
    {
        if (*p == '\n')
            *p = '\0';
    }

    if (strcmp(info->args, "exit") == 0)
        return (0);

    info->result = computeResult(info);
    if (info->err_code != 0)
        printf("%s [%c]\n", info->err_msg, info->err_input);
    else
        printf("[%ld]: %f\n", info->len, info->result);

    info->index = 0;
    info->err_code = 0;
    info->err_input = 0;
    info->err_msg = NULL;
    info->result = 0.0;
    info->len = 0;

    return (1);
}

We pass a pointer variable of type INFO struct as we can access all the information we need from there. Next, we evaluate every character in the input line in a for loop and replace the newline character with the null character.

Now, we can check for the exit instruction without the newline character appended to it. Thus, we’ll delete this block of code from the main() function. On the next line, we invoke the function to evaluate the line of input and assign its return value to the result member of the struct.

Next, we check for the value of err_code struct member. If its value is not zero, it means there was an error. We therefore print the error message and the offending input. On the other hand, a zero value means there was no error. We proceed to print the length of the input and the converted substring of the input.

The next set of lines reset the struct members to their default values and make room for a new line of input. Finally, we return the value of 1 to the caller to keep the calculator in the loop.

Adding Prototype to List

Next, we add this function’s prototype to the list of prototypes as show below:

/* function prototypes */
/* function prototypes */
ssize_t readLine(INFO *);
double handleSubstrToNumber(const char *, int *);
double computeResult(INFO *);
int processInput(INFO *info);

Updating Main Function

We are almost done with the second part. The final piece of this puzzle is to update the main() function to reflect the changes made. As indicated in the snippet below, we leave the starting point as "clean" as possible and push all the complexities to the other functions.

#include <ctype.h>

/* main - entry point */
int main(void)
{
    INFO info[] = { INFO_INIT };
    int status;

    do {
        printf("ashculate ?> ");
        info->len = readLine(info);
        status = processInput(info);
    } while (status);

    free(info->args);

    return (0);
}

Complete Code Listing

Putting everything together, our source code for the second part should be as the following.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

/* error messages */
#define ERR_INVALID_INPUT ":( Error: invalid input"
#define ERR_ZERO_DIVISION ":( Error: division by zero"
#define ERR_MISSING_PARENTHESIS ":( Error: missing closing parenthesis"

/* error codes */
#define ERR_INVALID_INPUT_CODE -5
#define ERR_ZERO_DIVISION_CODE -8
#define ERR_MISSING_PARENTHESIS_CODE -9

/* struct definition */
struct var_data
{
    /* user input data */
    char *args;
    size_t len;
    char err_input;
    int err_code;
    char *err_msg;
    int index;
    double result;
};
typedef struct var_data INFO;

/* initializer for struct */
#define INFO_INIT {NULL, 0, 0, 0, NULL, 0, 0.0}

/* function prototypes */
ssize_t readLine(INFO *);
double handleSubstrToNumber(const char *, int *);
double computeResult(INFO *);
int processInput(INFO *info);

/* main - entry point */
int main(void)
{
    INFO info[] = { INFO_INIT };
    int status;

    do {
        printf("ashculate ?> ");
        info->len = readLine(info);
        status = processInput(info);
    } while (status);

    free(info->args);

    return (0);
}

/* readLine - read user input */
ssize_t readLine(INFO *info)
{
    ssize_t len;

    len = getline(&info->args, &info->len, stdin);
    if (len == -1)
    {
        if (feof(stdin))
            exit(EXIT_SUCCESS);
        else
        {
            perror("readline");
            exit(EXIT_FAILURE);
        }
    }

    return (len);
}

/* convert values from string to number */
double handleSubstrToNumber(const char *args, int *index)
{
    double result = 0.0, frac = 0.1;
    int isNegative = 0;

    if (args[*index] == '-')
    {
        isNegative = 1;
        (*index)++;
    }

    /* handle digits before the dot */
    while (isdigit(args[*index]))
    {
        result = result * 10.0 + (args[*index] - '0');
        (*index)++;
    }

    /* handle digits after the dot in a floating point number */
    if (args[*index] == '.')
    {
        (*index)++;
        while (isdigit(args[*index]))
        {
            result += (args[*index] - '0') * frac;
            frac /= 10.0;
            (*index)++;
        }
    }

    if (isNegative)
        result = -result;

    return (result);
}

/* evaluate data and compute result */
double computeResult(INFO *info)
{
    char *p;

    for (p = info->args; *p != '\0'; p++)
    {
        if (*p == ' ' || *p == '\t')
        {
            info->err_code = ERR_INVALID_INPUT_CODE;
            info->err_input = *p;
            info->err_msg = ERR_INVALID_INPUT;

            return (1);
        }
    }

    return (handleSubstrToNumber(info->args, &info->index));
}

/* process data input */
int processInput(INFO *info)
{
    char *p;

    /* get rid of the newline character */
    for (p = info->args; *p != '\0'; p++)
    {
        if (*p == '\n')
            *p = '\0';
    }

    if (strcmp(info->args, "exit") == 0)
        return (0);

    info->result = computeResult(info);
    if (info->err_code != 0)
        printf("%s [%c]\n", info->err_msg, info->err_input);
    else
        printf("[%ld]: %f\n", info->len, info->result);

    info->index = 0;
    info->err_code = 0;
    info->err_input = 0;
    info->err_msg = NULL;
    info->result = 0.0;
    info->len = 0;

    return (1);
}

Compiling and Executing

Now, let's compile the code using the following:

$ gcc -Wall -Werror -Wextra -pedantic -std=gnu89 1_ashculate.c -o 1_ashculate

Next, we execute the resulting object as shown:

$ ./1_ashculate

Conclusion

Bravo! You've done an amazing job following along with this tutorial.

This concludes the second part of developing ASHCulator. We improved the code from the previous part by creating functions to convert a substring of the input to a number of type double, to process the input and to initiate the process for the application of the operators on the input.

In the next and final part, we will implement functions to perform addition, subtraction, multiplication, division and modulo operations on the input.

Thanks for reading this tutorials. Do you have any questions or suggestions, let me know your thoughts in the comments.