Develop a Calculator in C - Part III

Develop a Calculator in C - Part III

Introduction

Oh hail, the moments of relief when signs of accomplishments lit our sights.

Our rewards await our persistence; the pride of a gained value from another milestone conquered.

Welcome to the the third and last part in our quest to develop a calculator using the C programming language.

In parts one and two, we created functionalities to read input, process the input, handled some edge cases, and converted a substring to a number of type double. Finally,we sent the processed value to the standard output - stdout in C terminology, which got displayed on the screen.

In this final part, we'll create functions to execute computations based on the choice operators and to handle parenthesis. Let’s not forget that, ASHCulator utilizes the BODMAS Mathematics principle in computing results.

Let’s get started :).

Handling Addition and Subtraction

ASHCulator will recursively invoke several functions as it computes the results. Therefore, it has to start from somewhere; where it will always return to complete its cycle of computation.

So, let’s define the function to implement addition and subtraction operations to be the starting point.

/* implement addition and subtraction computations */
double handleAddSub(INFO *info, const char *args, int *index)
{
    double operand, ans;
    char op;

    ans = handleMulDivMod(info, args, index);

    while (args[*index] == '+' ||
           args[*index] == '-')
    {
        op = args[(*index)++];
        operand = handleMulDivMod(info, args, index);
        if (op == '+')
            ans += operand;
        else
            ans -= operand;
    }

    return (ans);
}

Anytime we invoke this function, we pass pointers to the struct, the line of input and the index value as arguments. Although the struct has members to hold the line of input and index, we still pass them separately for convenience.

Moving on, we first invoke the function to handle multiplication, division and modulo operations to obtain the first operand; we'll define this function in a few moments. The rationale behind this, is to ensure that, our calculator adheres to the BODMAS principle. We then check for the addition and subtraction operators in a loop.

At each iteration, we assign the operator to a variable and increment the index value by 1. We then obtain the next operand from the invocation of the function that handles multiplication, division and modulo operations.

We continue to compute the result by either adding or subtracting and return it to the calling function.

Add Prototype

Next, we add the prototype of this function to the list of prototypes at the top of the file as shown below:

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

Handling Multiplication, Division and Modulo

Now, let’s proceed to define the function to handle multiplication, division and modulo operations.

/* implement multiplication, division and modulo computations */
double handleMulDivMod(INFO *info, const char *args, int *index)
{
    double operand, ans;
    char op;

    ans = handleBrackets(info, args, index);

    while (args[*index] == '*' ||
           args[*index] == '/' ||
           args[*index] == '%')
    {
        op = args[(*index)++];
        operand = handleBrackets(info, args, index);

        switch (op)
        {
            case '*':
                ans *= operand;
                break;
            case '/':
                if (operand == 0.0)
                {
                    info->err_code = ERR_ZERO_DIVISION_CODE;
                    info->err_input = '0';
                    info->err_msg = ERR_ZERO_DIVISION;

                    return (0);
                }

                ans /= operand;
                break;
            case '%':
                if (operand == 0.0)
                {
                    info->err_code = ERR_ZERO_DIVISION_CODE;
                    info->err_input = '0';
                    info->err_msg = ERR_ZERO_DIVISION;

                    return (0);
                }

                ans = handleModDiv(ans, operand);
                break;
            default:
                break;
        }
    }

    return (ans);
}

Just like the previous function, we pass pointers to the struct, the line of input and the index value as arguments to this function upon invocation. Then again, we first call the function to handle brackets. We’ll define this later.

Using the same technique as we did in the previous function, we look out for the multiplication, division and modulo operators in a loop. We obtain the operator and increment the index value by 1.

We proceed to obtain the next operand from the re-invocation of the function to handle brackets. With the help of the switch statement, we compute the result based on the respective operator. We’re careful to handle errors that might arise.

You might have realized that we defined a function to handle the modulo operation. The standard library has a similar function, the fmod() which can be used to handle this operation. I’ll talk more about this in moment.

Adding Prototype

Now, let’s add the prototype of this function to the list of prototypes at the top of the file as shown below:

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

Handling of Custom Modulo

In the previous section, we invoked a custom function to handle the modulo operation. The function is defined as shown below.

/* implement modulo division */
double handleModDiv(double value, double operand)
{
    double quo, div, ans;

    quo = value / operand;
    div = (double)((long) quo);
    ans = value - operand * div;

    return (ans);
}

According to the manual, the return value of the fmod() function is computed as x – n * y where x is the first parameter, y is the second. n is obtained by dividing x by y. We use this approach to define the above function.

Adding Prototype

Next, we add the prototype of this function to the list of prototypes at the top of the file as shown below:

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

fmod() from the Standard Library

You can, however use the fmod() from the standard library. If you use it, you must add the -lm flag to your compilation statement as indicated below:

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

This will inform the compiler to link the maths object from the standard library to your program, otherwise, it'll fail to compile.

Handling Brackets

At last, we’ve gotten to the function that will handle brackets. So, let's dive in.

/* handle parenthesis */
double handleBrackets(INFO *info, const char *args, int *index)
{
    double result;

    if (args[*index] == '(')
    {
        (*index)++;

        result = handleAddSub(info, args, index);

        if (args[*index] == ')')
        {
            (*index)++;
            return (result);
        }
        else
        {
            info->err_code = ERR_MISSING_PARENTHESIS_CODE;
            info->err_input = ')';
            info->err_msg = ERR_MISSING_PARENTHESIS;

            return (0);
        }
    }
    else
        return (handleSubstrToNumber(info, args, index));
}

The arguments to be passed to this function upon invocation are similar to the previous functions as explained above.

First, we check for an opening parenthesis. If there is one, we increment the index value by 1 to skip it. We then invoke the function to handle addition and subtraction for the result. If there’s a closing parenthesis, we again increment the index value by 1 to skip it and return the result. Otherwise, we report an error message to the user.

On the other hand, if there’s no opening parenthesis, we convert the substring to a number and return the converted value.

Adding Prototype

Let’s add the prototype of this function to the list of prototypes at the top of the file as shown below:

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

Updating Other Functions

We're almost done with the calculator. We’ve successfully created all the major functions needed for the calculator to solve problems related to Mathematics.

However, with these new function definitions, we need to do a few updates to reflect the changes we’ve made so far.

First, let's change the return statement of the computeResult() function from handleSubstrToNumber() to handleAddSub() as shown below:

double computeResult(INFO *info)
{
    ...

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

The next function to update is the handleSubstrToNumber() . Add the following block of code just before the return statement as shown below:

double handleSubstrToNumber(const char *args, int *index)
{
    ...

    if (args[*index] == '(')
        result *= handleBrackets(info, args, index);

    return (result);
}

When done, add INFO *info as the first argument of the handleSubstrToNumber() function as shown below:

double handleSubstrToNumber(INFO *info, const char *args, int *index)
{
    ...
}

Do the same for its prototype at the top of the file.

double handleSubstrToNumber(INFO *, const char *, int *);

Compiling and Testing

Wow, that's a whole lot. You can stretch and yawn a bit, but we ain't done yet. We need to compile and test the calculator to see how far it has evolved.

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

Now execute,

$ ./2_ashculate

and test with the following sample problems.

  • ashculate ?> 2+2

    [4] 4.000000

  • ashculate ?> 2*2

    [4] 4.000000

  • ashculate ?> 1.5-3

    [6] -1.500000

  • ashculate ?> 1.005*0.02

    [11] 0.020100

  • ashculate ?> 69 +3.003

    :( Error: invalid input [ ]

  • ashculate ?> 3(5-9*2)/3

    [11] -13.000000

Additional Improvements

Our calculator is complete but it can still be improved. You might have observed the following as you were testing it:

  1. It prints the length of the input along with the result of the computation. The length is confusion, and not needed either.

  2. Computations that result in single digit values get printed with floating-point values with unwanted zeros. Example 2+2 gives 4.000000 instead of just 4.

So, we're going to fix these issues.

Let's focus our attention on the processInput() function. Update the function to get what is displayed below:

/* process data input */
int processInput(INFO *info)
{
    double dec_part;
    int int_part, state;
    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 
    {
    /* changes are in this else block. Make sure to declare variables first*/
        int_part = (int) info->result;
        dec_part = info->result - int_part;

        state = checkDecDigits(dec_part);
        if (state == 1)
            printf(":) %f\n", info->result);
        else
            printf(":) %d\n", int_part);
    }

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

    return (1);
}

The only changes we made were the part that prints the results of the computation.

This is what we are doing in the update.

We break the final result of the computation into two parts, int_part and dec_part. We check the values in the dec_part by invoking the checkDecDigits() function.

This function returns 1 if any of the digits in the dec_part is greater than zero (0). On the other hand, it returns zero (0) if all the digits are zeros.

Based on the return value of this function, we either present the result with floating-point values or only the int_part as the final answer.

Defining checkDecDigits Function

The following is the definition of the function that helps us determine whether the decimal part of the computed result has all zero (0) values or not.

/* check values after the dot in a floating-point number */
int checkDecDigits(double value)
{
    while (value != (int) value)
    {
        value *= 10;
        if (value > 0)
            return (1);
    }

    return (0);
}

Adding Prototype

Let's add its prototype to the list of prototypes as indicated below:

/* function prototypes */
ssize_t readLine(INFO *);
double handleSubstrToNumber(const char *, int *);
double computeResult(INFO *);
int processInput(INFO *info);
double handleAddSub(INFO *, const char *, int *);
double handleMulDivMod(INFO *, const char *, int *);
double handleModDiv(double, double);
double handleBrackets(INFO *, const char *, int *);
int checkDecDigits(double);

Complete Source Code

At the of the tutorials, the following is the complete source code for ASHCulator. Ensure that yours matches exactly as shown below.

#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(INFO *, const char *, int *);
double computeResult(INFO *);
int processInput(INFO *info);
double handleAddSub(INFO *, const char *, int *);
double handleMulDivMod(INFO *, const char *, int *);
double handleModDiv(double, double);
double handleBrackets(INFO *, const char *, int *);
int checkDecDigits(double);

/* 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(INFO *info, 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;

    if (args[*index] == '(')
            result *= handleBrackets(info, args, index);

    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 (handleAddSub(info, info->args, &info->index));
}

/* process data input */
int processInput(INFO *info)
{
    double dec_part;
    int int_part, state;
    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
    {
        int_part = (int) info->result;
        dec_part = info->result - int_part;

        state = checkDecDigits(dec_part);
        if (state == 1)
            printf(":) %f\n", info->result);
        else
            printf(":) %d\n", int_part);
    }

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

    return (1);
}

/* implement addition and subtraction computations */
double handleAddSub(INFO *info, const char *args, int *index)
{
    double operand, ans;
    char op;

    ans = handleMulDivMod(info, args, index);

    while (args[*index] == '+' ||
           args[*index] == '-')
    {
        op = args[(*index)++];
        operand = handleMulDivMod(info, args, index);
        if (op == '+')
            ans += operand;
        else
            ans -= operand;
    }

    return (ans);
}

/* implement multiplication, division and modulo computations */
double handleMulDivMod(INFO *info, const char *args, int *index)
{
    double operand, ans;
    char op;

    ans = handleBrackets(info, args, index);

    while (args[*index] == '*' ||
           args[*index] == '/' ||
           args[*index] == '%')
    {
        op = args[(*index)++];
        operand = handleBrackets(info, args, index);

        switch (op)
        {
            case '*':
                ans *= operand;
                break;
            case '/':
                if (operand == 0.0)
                {
                    info->err_code = ERR_ZERO_DIVISION_CODE;
                    info->err_input = '0';
                    info->err_msg = ERR_ZERO_DIVISION;

                    return (0);
                }

                ans /= operand;
                break;
            case '%':
                if (operand == 0.0)
                {
                    info->err_code = ERR_ZERO_DIVISION_CODE;
                    info->err_input = '0';
                    info->err_msg = ERR_ZERO_DIVISION;

                    return (0);
                }

                ans = handleModDiv(ans, operand);
                break;
            default:
                break;
        }
    }

    return (ans);
}

/* implement modulo division */
double handleModDiv(double value, double operand)
{
    double quo, div, ans;

    quo = value / operand;
    div = (double)((long) quo);
    ans = value - operand * div;

    return (ans);
}

/* handle parenthesis */
double handleBrackets(INFO *info, const char *args, int *index)
{
    double result;

    if (args[*index] == '(')
    {
        (*index)++;

        result = handleAddSub(info, args, index);

        if (args[*index] == ')')
        {
            (*index)++;
            return (result);
        }
        else
        {
            info->err_code = ERR_MISSING_PARENTHESIS_CODE;
            info->err_input = ')';
            info->err_msg = ERR_MISSING_PARENTHESIS;

            return (0);
        }
    }
    else
        return (handleSubstrToNumber(info, args, index));
}

/* check values after the dot in a floating-point number */
int checkDecDigits(double value)
{
    while (value != (int) value)
    {
        value *= 10;
        if (value > 0)
            return (1);
    }

    return (0);
}

Compiling and Executing

Finally, compile and execute the application. You can test with these test cases. You should have similar results as shown in the image below.

Conclusion

Congratulations!!! You've completed this tutorial with persistence and determination. I hope you enjoyed the lesson and are inspired to create an application in C or other programming language yourself.

AHSCulator, however still lacks certain features that can make it more versatile than it is now. If you've observed carefully:

  • It lacks history. You can't scroll through the list of past computations and select one to recompute; you'll have to type everything again.

  • It's limited in its capability to handle more operators.

  • You can't move the cursor. Moving the cursor is an important feature as demonstrated in word documents and text editors. In such editors, you can easily click anywhere to move the cursor or use the arrow keys. These can't be achieved in our calculator.

However, I'm hopeful of including some of these features, if not all during the next development phase.

Once again, thanks for clinging along with these tutorials. You're amazing. It has been an exciting journey and I had so much fun during the Christmas holidays as I developed this calculator.

You can find the organized source code on GitHub.

Do you have any questions or suggestions, let me know your thoughts in the comments.