2
\$\begingroup\$

My program takes input from the user in two ways:

  • By passing input as an argument whilst calling the command
  • By taking any number of inputs after calling the command

The program is a terminal calculator that recognizes parentheses, operators, and errors as soon as you pass an expression. If you happen to come across an (non-internal) error, it will always return a suitable error message. Optionally, the user may pass a number alongside the expression to round the answer to a certain number of digits.

The program mainly centers around the manipulation of strings. As far as I know, there aren't any bugs or memory leaks. However, I've been coding for a few months, and I am in no way an expert. Because of my limited knowledge of C, I'm afraid the methods used in my program are unnecessarily complex. Constructive criticism of any kind is welcome.

Note: I used the goto keyword many times, mostly as a way to organize error handling. I know that use of such is frowned upon in most cases, but I thought this would be a good implementation of it.

main.c

#include <float.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "eval.h"

bool CMD_LINE; // Using command-line interface?

void print_help(void);

int main(int argc, char *argv[]) {
    char *expr = NULL, *swap = NULL;
    size_t bufsize = 999,   // Maximum input size
    ndec;                   // Number of decimal places
    double result;

    CMD_LINE = true;
    if (argc == 1) {
        CMD_LINE = false;
        goto interactive;
    }

    command_line:
    if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { // Interpret help flag if present
        if (argc == 2)
            print_help();
        else
            fail("Incorrect flag usage");
    }
    if ((expr = simplify(argv[1], 0)) == NULL) {
        putchar('\n');
        exit(EXIT_FAILURE);
    }
    if (argc == 3) {    // Interpret decimal count if present
        if (strspn(argv[2], VAL_CHRS + 10) == strlen(argv[2]))  /* Decimal count contains only digits    */
            ndec = atoi(argv[2]);                               /* VAL_CHRS[11->20] = '1', ..., '9', '0' */
        else
            goto invdec_err;
        if (ndec > DBL_DIG)
            goto invdec_err;
    }
    #ifdef DEBUG
    puts("\n\e[4mresult\e[24m");
    #endif
    result = stod(expr);
    free(expr);
    puts(expr = dtos(result, argc == 3 ? ndec : 6));    // 6 = default number of decimals shown
    free(expr);
    return EXIT_SUCCESS;

    interactive:
    for (;;) {
        printf("> ");
        expr = calloc(bufsize + 1, sizeof(char));
        getline(&expr, &bufsize, stdin);
        if (!strcmp(expr, "\n")) {
            free(expr);
            break;
        }
        swap = expr;    // Swap causes 'still reachable' error in valgrind
        expr = simplify(expr, 0);
        free(swap);
        if (expr != NULL) {
            result = stod(expr);
            free(expr);
            puts(expr = dtos(result, 6));
            free(expr);
        }
    }
    return EXIT_SUCCESS;

    invdec_err:
        fail("Invalid decimal count");
}

void print_help(void) {
    printf("Usage: %s [EXPRESSION] [ROUND]\n", PROG_NAME);
    puts("High-accuracy terminal calculator");
    puts("Encapsulation within apostrophes (') is recommended");
    puts("This software falls under the GNU Public License v3.0\n");

    puts("++, --     ++x, --x         Increment, decrement");
    puts("!, !!      !x, y!!x         Square root, other root        ↑ Higher precedence");
    puts("^          x^y              Exponent");
    puts("*, /, %    x*y, x/y, x%y    Multiply, divide, remainder    ↓ Lower Precedence");
    puts("+, -       x+y, x-y         Add, subtract\n");

    puts("           (x + y)          Control precedence");
    puts("           x(y)             Multiply terms\n");

    puts("GitHub repository: https://github.com/crypticcu/eval");
    puts("Report bugs to:    [email protected]");
    exit(EXIT_SUCCESS);
}

eval.c

#include <ctype.h>
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "eval.h"

/* NOTES **********************************************************

 - Parameters contain a leading underscore
 - Variables shared between files are uppercase
 - Variables named "nchr" hold index positions
    o Cannot be negative, so use size_t
 - An "obstruction" is an invalid character found to be in between
   operator and operand
 - In terms of operation, a "limit" is the furthest right- or
   left-hand index position, relative to an operator in which an
   operation would take place
 - stod() is used as a replacement to atof()
    o Recognizes numbers that cannot computed accurately
    o Number of accurate digits determined by __DBL_DIG__ (DBL_DIG)
 - dtos() is used as a replacement to gcvt()
    o Returns dynamically-allocated string
    o Does not require buffer

 ******************************************************************/

void fail(const char *_desc) {
    printf("%s\n", _desc);
    if (CMD_LINE)   // Exit program if using command-line
        exit(EXIT_FAILURE);
}

void printu(const char *_s, size_t _hpos) {
    for (int nchr = 0; nchr < strlen(_s); nchr++) {
        if (nchr == _hpos)
            printf("\e[4m");    // Underline
        putchar(_s[nchr]);
        if (nchr == _hpos)
            printf("\e[24m");   // Reset underline
    }
}

char *dtos(double _x, size_t _sig) {    // Dynamic memory: numstr
    size_t nwhole = nplaces(_x);

    if (nwhole > DBL_DIG)
        return STR_OVER;
    if (nwhole + _sig > DBL_DIG)
        _sig = DBL_DIG - nwhole;
    if (_sig > DBL_DIG) // Decimal place exceeds accurate number allotted by system
        _sig = DBL_DIG;

    bool is_negative = _x < 0, is_decimal = _sig, only_decimal = _x < 1 && _x > -1, only_whole = isequal(_x, (int) _x);
    size_t reqsize = nwhole + _sig + is_negative + is_decimal + only_decimal;
    char *numstr = (char *) calloc(reqsize + 1, sizeof(char));

    if (numstr == NULL) // Allocation fails
        return NULL;
    if (is_negative) {  // Negative and decimal requirements
        numstr[0] = '-';
        if (only_decimal) {
            numstr[1] = '0';
            numstr[2] = '.';
        }   
    } else if (only_decimal) {
        numstr[0] = '0';
        numstr[1] = '.';
    }
    for (int nchr = is_negative + only_decimal * 2, place = nplaces(_x) - !(nwhole == FLT_DIG && only_whole); nchr < reqsize; nchr++, place--) {    // Skip characters reserved for negative sign and decimal point, if present
        numstr[nchr] = getdigit(_x, place) + 48;    // '0' = 48
        if (place == 0) {
            if (only_whole)
                break;
            else if (nchr + 1 != reqsize)
                numstr[++nchr] = '.';
        }
    }
    return numstr;
}

char *popsub(const char *_s, size_t _low, size_t _high) { // Dynamic memory: sub
    char *sub;  // Substring to be 'popped' from string

    if (_low >= strlen(_s) || _high >= strlen(_s) || _low > _high ||    // Low/high indices exceed range of string || Low index is greater than high || Allocation fails
    (sub = (char *) calloc(_high - _low + 2, sizeof(char))) == NULL)
        return NULL;
    for (size_t nchr_old = _low, nchr_new = 0; nchr_old <= _high; nchr_old++, nchr_new++)
        sub[nchr_new] = _s[nchr_old];
    return sub;
}

char *pushsub(char *_s, char *_sub, size_t _low, size_t _high) {    // Dynamic memory: newstr
    char *newstr = (char *) calloc(
        strlen(_s)              // Original length
        - (_high - _low + 1)    // Take away number of characters being removed
        + strlen(_sub)          // Add size of substring
        + 1                     // Add space for null character
    , sizeof(char));
    int nchr_new;
    
    if (_low >= strlen(_s) || _high >= strlen(_s) || _low > _high || newstr == NULL) // Low/high indices exceed range of string || Low index is greater than high || Allocation fails
        return NULL;                                 
    for (nchr_new = 0; nchr_new < _low; nchr_new++)
        newstr[nchr_new] = _s[nchr_new];    // Add contents of old string up to point of integration
    for (int nchr_sub = 0; nchr_sub < strlen(_sub); nchr_sub++, nchr_new++)
        newstr[nchr_new] = _sub[nchr_sub];  // Integrate substring
    for (int nchr_old = _high + 1; nchr_old < strlen(_s); nchr_old++, nchr_new++)
        newstr[nchr_new] = _s[nchr_old];    // Add rest of old string
    free(_s);
    free(_sub);
    return newstr;
}

char *simplify(const char *_expr, size_t _from) {   // Dynamic memory: subA, subB, expr
    bool read_parenth = false;
    char chr, *subA = NULL, *subB = NULL,
        *expr = (char *) calloc(strlen(_expr) + 1, sizeof(char));   // Modifiable expression
    size_t par_low, par_high;
    int invpos;
    double result;

    if (expr == NULL)
        return NULL;
    if (!_from) {   // Check syntax and parenthesese only once (_from is always 0 on first call)
        if ((invpos = chk_syntax(_expr)) != CHK_PASS)   // Check for syntax errors
            goto syntax_err;
        if ((invpos = chk_parenth(_expr)) != CHK_PASS)  // Check for parenthetical errors
            goto syntax_err;
    }
    strcpy(expr, _expr); // Copy constant expression to modifiable one
    for (int nchr = _from; (chr = expr[nchr]); nchr++) {    // Go straight to evaluate() if '-p' is passed
        if (chr == ')') {
            par_high = nchr;
            read_parenth = false;
            if ((subA = popsub(expr, par_low, par_high)) == NULL)
                goto popsub_err;
            expr[par_low] = (toast(expr, par_low)) ? '*' : ' ';
            expr[par_high] = (toast(expr, par_high)) ? '*' : ' ';
            result = evaluate(subA);    // Value passed to result to increase efficiency and improve debugging mode clarity
            if (isequal(result, DBL_FAIL))  // evaluate() does not return heap address, so can be called without assignment
                goto evaluate_err;
            if ((subB = dtos(result, DBL_DIG)) == NULL)
                goto dtos_err;
            free(subA);
            if ((expr = pushsub(expr, subB, par_low + 1, par_high - 1)) == NULL)    // Do not overwite space where parentheses used to be
                goto pushsub_err;
            if (_from)
                return expr;
        }
        else if (chr == '(') {
            if (read_parenth) {
                subA = expr;    // Swap causes 'still reachable' error in valgrind
                if ((expr = simplify(expr, nchr)) == NULL)
                    goto simplify_err;
                free(subA);
            } else {
                read_parenth = true;
                par_low = nchr;
            }
        }
    }
    subA = expr;
    result = evaluate(expr);
    if (isequal(result, DBL_FAIL))
        goto evaluate_err;
    if ((expr = dtos(result, DBL_DIG)) == NULL)
        goto dtos_err;
    free(subA);
    return expr;

    syntax_err:
        printf("Syntax error: ");
        printu(_expr, invpos);
        free(expr);
        return NULL;
    evaluate_err:
        free(subA); // Failure message not required; would have already been handled by evaluate()
        return NULL;
    dtos_err:
        free(subA);
        fail("Internal error (simplify.dtos)"); // Exits program if using command-line
        return NULL;
    popsub_err:
        free(expr);
        fail("Internal error (simplify.dtos)");
        return NULL;
    pushsub_err:
        fail("Internal error (simplify.pushsub)");
        return NULL;
    simplify_err:
        free(subA);
        fail("Internal error (simplify.simplify)");
        return NULL;
}

bool isequal(double _x, double _y) {
    return fabs(_x - _y) < FLT_EPSILON;
}

bool isin(const char _x, const char *_y) {
    for (int nchr = 0; nchr < strlen(_y); nchr++)
        if (_x == _y[nchr])
            return true;
    return false;
}

bool isnumer(char _c) {
    return (isdigit(_c) || _c == '-' || _c == '.');
}

bool toast(const char *_expr, size_t _parpos) { // To asterisk?
    char chr = _expr[_parpos],
         next,  // Character after parenthesis
         last;  // Character before parenthesis
    
    next =  _parpos < strlen(_expr) - 1 ? _expr[_parpos + 1] : 0;
    last = _parpos ? _expr[_parpos - 1] : 0;
    return (isdigit(last) && isnumer(next)  ||
        chr == '(' && last == ')'           ||
        chr == ')' && next == '(') ? true : false;
}

size_t getdigit(double _x, int _place) {
    size_t digit;

    _x = fabs(_x);
    if (abs(_place) > DBL_DIG || _x > LLONG_MAX)    // Place cannot be over/under place limit; Any 'x' over max llong causes overflow on conversion
        return 0;   // Digits that cannot be printed
    for (int nchr = 0; nchr <= abs(_place); _place > 0 ? (_x /= 10) : (_x *= 10), nchr++)
        digit = ((long long int) _x - (long long int) (_x / 10) * 10);
    return digit;
}

size_t nplaces(double _x) {
    _x = fabs(_x);  // log of negative is undefined
    if (_x == 0)    // log of zero is undefined
        return 1;
    return log(_x)/log(10) + 1;
}

int chk_parenth(const char *_expr) {
    char chr;
    int nopen = 0, nclosed = 0, nchr;

    for (nchr = 0; (chr = _expr[nchr]); nchr++) // Get number of closed parentheses
        if (chr == ')')
            nclosed++;
    for (nchr = 0; (chr = _expr[nchr]); nchr++) {
        if (chr == '(')
            nopen++;
        else if (chr == ')')
            nopen--;
        if (nopen > nclosed) {  // Extra open parenthesis?
            while ((chr = _expr[--nchr]) != '(');   // Find last instance of open parenthesis
            return nchr;
        }
        if (nopen < 0)  // Extra closed?
            return nchr;
    }
    return CHK_PASS;
}

int chk_syntax(const char *_expr) {
    char chr,
         lead = 0,  // Last non-space 
         trail = 0, // Next non-space
         last = 0,  // Immediate last
         next = 1;  // Immediate next
    size_t nsingle = 0, // Single operators
           ndouble = 0, // Double operators
           npoint = 0,  // Decimal points
           nchr_err;    // Index position of syntax error

    #ifdef DEBUG
    puts("\e[4mchk_syntax\e[24m");
    #endif
    for (size_t nchr = 0; (chr = _expr[nchr]); nchr++) {
        #ifdef DEBUG
        printf("single: %ld\tdouble: %ld\tchr: %c\n", nsingle, ndouble, chr);
        #endif
        if (nchr)
            last = _expr[nchr - 1];
        if (nchr != strlen(_expr))
            next = _expr[nchr + 1];
        if (next != 0)
            for (int i = nchr + 1; _expr[i]; i++)
                if(!isspace(_expr[i])) {
                    trail = _expr[i];
                    break;
                }
        if (isdigit(chr) || chr == '(' || chr == ')')   // CHECK OPERATORS
            nsingle = 0, ndouble = 0;
        else if (isin(chr, DBLS) && (chr == last && isin(last, DBLS) || chr == next && isin(next, DBLS))) {
            if (chr != '!' && isdigit(lead) || chr == '!' && trail == '!' && !isdigit(lead) && lead != '.') // Operator is obstruction
                return nchr;    
            if (chr == '!' && lead == '!' && !isdigit(trail) && trail != '.') { // Operator isn't obstruction; find obstruction
                for (nchr_err = nchr; (chr = _expr[nchr_err]) != trail; nchr_err++);
                return nchr_err;
            }
            ndouble++;
        }
        else if (isin(chr, OPERS) && !(isin(chr, UNRY) && isin(lead, BNRY)))    // Extra conditionals needed to prevent 'x + !y' from being a syntax error
            nsingle++;  
        if (!isdigit(chr) && chr != '.') // CHECK DECIMAL POINTS
            npoint = 0;
        else if (chr == '.')
            npoint++;
        if (nsingle == 2 || ndouble == 3 || npoint == 2 ||  /* Extra operator or comma ||              */   //  CHECK ERRORS
            !isin(chr, VAL_CHRS) && !isspace(chr) ||        /* Is not a valid character nor a space || */
            isdigit(chr) && isdigit(lead) && lead != last)  /* Two numbers side-by-side w/o operator   */
            return nchr;
        if (!isspace(chr))
            lead = chr;
    }
    return CHK_PASS;
}

int getlim(char *_expr, size_t _operpos, char _dir) {
    bool reading = false, read_digit;
    char chr;
    int lim = -1, nchr;

    if (_dir != 'l' && _dir != 'r') // Left and right directions only
        return INT_FAIL;
    for (nchr = _dir == 'r' ? _operpos + 1 : _operpos - 1; (chr = _expr[nchr]) && nchr >= 0; _dir == 'r' ? nchr++ : nchr--) {
        if (isnumer(chr) && !reading)
            reading = true;
        else if (!isnumer(chr) && reading) {
            lim = _dir == 'r' ? nchr - 1 : nchr + 1;
            break;
        }
        if (isdigit(chr))
            read_digit = true;
    }
    if (!reading || !read_digit)    // No value found
        return INT_FAIL;
    else {
        if (nchr == -1) // Reached beginning of expression
            lim = 0;
        else if (chr == 0)  // Reached end of expression
            lim = strlen(_expr) - 1;
    }
    return lim;
}

size_t fobst(const char *_expr, size_t _operpos, size_t _llim, size_t _rlim) {
    bool l_obstr = false, r_obstr = false;
    char chr, oper = _expr[_operpos];
    int nchr = _operpos,
        off = 0;    // Offset from operator position

    if (_llim == INT_FAIL)  // Left limit of unary operation is operator position
        _llim = 0;
    if (_rlim == INT_FAIL)
        _rlim = strlen(_expr) - 1;
    if (_llim >= strlen(_expr) || _rlim >= strlen(_expr) || _llim > _rlim)
        return INT_FAIL;
    while (off < (int) strlen(_expr)) {
        if (nchr >= _llim && nchr <= _rlim) {
            chr = _expr[nchr];
            if (isin(chr, OPERS) && chr != oper && chr != '-')
                off < 0 ? (l_obstr = true) : (r_obstr = true);
        }
        off <= 0 ? (off = -(off - 1)) : (off = -off);
        nchr = _operpos + off;
    }
    if (l_obstr && r_obstr)
        return LEFT | RIGHT;
    else if (l_obstr && !r_obstr)
        return LEFT;
    else if (!l_obstr && r_obstr)
        return RIGHT;
    else
        return 0;
}

double evaluate(const char *_expr) {    // Dynamic memory: result_str, expr
    char chr,
        *result_str = NULL, // Operation result
        *expr = (char *) calloc(strlen(_expr) + 1, sizeof(char));   // Modifiable expression
    size_t llim, rlim;  // Left- and right-hand limits of operation
    int nchr;
    double result,  // Operation result;    Final return value
           lval, rval;  // Left and right values of operation

    #ifdef DEBUG
    printf("\n\e[4mevaluate\e[24m\n%s\n", _expr);
    #endif
    if (expr == NULL)
        return DBL_FAIL;
    strcpy(expr, _expr);    // Copy constant expression to modifiable one
    for (nchr = 0; (chr = expr[nchr]); nchr++) {
        if (isin(chr, OPERS))
            goto evaluate;
    }
    goto reevaluate;    // Skip main loop if no operators are found

    evaluate:
    for (nchr = 0; (chr = expr[nchr]); nchr++) {    // INCREMENT/DECREMENT
        if (chr == '+' && expr[nchr + 1] == '+' || chr == '-' && expr[nchr + 1] == '-') {
            INIT_VALS();    // Retrieves rval, lval, rlim, and llim
            if (fobst(expr, nchr, llim, rlim) & RIGHT)  // Operation cannot continue if another operator is in the way
                continue;
            CHK_VALS(RIGHT);    // Checks for overflow, getval() failure, and missing operand(s)
            result = chr == '+' ? rval + 1  // '++x' -> '+(x + 1)' 
                               : -rval - 1; // '--x' -> '-(-x + 1)'
            llim = nchr;    // Left limit of unary operation is operator position   
            INIT_EXPR();    // Retrieves new expression
            #ifdef DEBUG
                puts(expr);
            #endif
        }
    }
    for (nchr = 0; (chr = expr[nchr]); nchr++) {    // SQUARE ROOT/OTHER ROOT
        if (chr == '!') {
            INIT_VALS();
            if (expr[nchr + 1] == '!') {
                if (fobst(expr, nchr, llim, rlim) & (LEFT & RIGHT)) // Needs both sides of operator
                    continue;
                CHK_VALS(LEFT|RIGHT);
                if (rval < 0 && (int) lval % 2 == 0)
                    goto evenroot_err;
                if (lval == 0)
                    goto zeroroot_err;
                result = rval < 0 ? -pow(-rval, 1 / lval) : pow(rval, 1 / lval);    // Negative root workaround
            } else {
                if (fobst(expr, nchr, llim, rlim) & RIGHT)  // Needs only the right side of operator
                    continue;
                CHK_VALS(RIGHT);
                if (rval < 0)
                    goto evenroot_err;
                llim = nchr;
                result = sqrt(rval);
            }
            INIT_EXPR();
            #ifdef DEBUG
            puts(expr);
            #endif
        }
    }
    for (nchr = 0; (chr = expr[nchr]); nchr++) {    // EXPONENT
        if (chr == '^') {
            INIT_VALS();
            if (fobst(expr, nchr, llim, rlim) & (LEFT & RIGHT))
                continue;
            CHK_VALS(LEFT|RIGHT);
            result = pow(lval, rval);
            INIT_EXPR();
            #ifdef DEBUG
            puts(expr);
            #endif
        }
    }
    for (nchr = 0; (chr = expr[nchr]); nchr++) {    // MULTIPLICATION/DIVISION/REMAINDER
        if (chr == '*' || chr == '/' || chr == '%') {
            INIT_VALS();
            if (fobst(expr, nchr, llim, rlim) & (LEFT & RIGHT))
                continue;
            CHK_VALS(LEFT|RIGHT);
            if (rval == 0 && chr != '*')
                goto divzero_err;
            if (chr == '*')
                result = lval * rval;
            else if (chr == '/')
                result = lval / rval;
            else if (chr == '%') {
                if (isequal(lval, (int) lval) && isequal(rval, (int) rval))
                    result = (int) lval % (int) rval;
                else
                    goto modulus_err;
            }
            INIT_EXPR();
            #ifdef DEBUG
            puts(expr);
            #endif
        }
    }
    for (nchr = 0; (chr = expr[nchr]); nchr++) {    // ADDITION/SUBTRACTION/UNARY PLUS/UNARY MINUS
        if (chr == '+' || chr == '-') {
            if (chr == '+' && expr[nchr + 1] == '+' || chr == '-' && expr[nchr + 1] == '-') // Increment/Decrement found
                goto evaluate;
            INIT_VALS();
            if (fobst(expr, nchr, llim, rlim) & RIGHT)
                continue;
            CHK_VALS(RIGHT);
            if (llim == INT_FAIL)
                llim = nchr;
            result = chr == '+' ? lval + rval : lval - rval;
            INIT_EXPR();
            #ifdef DEBUG
            puts(expr);
            #endif
        }
    }

    reevaluate:
    while (strcspn(expr, OPERS + 2) != strlen(expr)) {
        if ((result_str = dtos(evaluate(expr), DBL_DIG)) == NULL)
            goto dtos_err;
        if ((result_str = dtos(evaluate(expr), DBL_DIG)) == STR_OVER)
            goto overflow_err;
        free(expr);
        expr = result_str;
    }

    if (isequal(result = stod(expr), DBL_OVER))
        goto overflow_err;
    free(expr);
    return result;

    opermiss_err:
        free(expr);
        fail("Missing operand");    // Exits program if using command-line
        return DBL_FAIL;
    evenroot_err:
        free(expr);
        fail("Even root of negative number");
        return DBL_FAIL;
    zeroroot_err:
        free(expr);
        fail("Root cannot be zero");
        return DBL_FAIL;
    divzero_err:
        free(expr);
        fail("Divide by zero");
        return DBL_FAIL;
    modulus_err:
        free(expr);
        fail("Remainder takes integers only");
        return DBL_FAIL;
    getval_err:
        free(expr);
        fail("Internal error (evaluate.getval)");
        return DBL_FAIL;
    dtos_err:
        free(expr);
        fail("Internal error (evaluate.dtos)");
        return DBL_FAIL;
    overflow_err:
        free(expr);
        fail("Number too large");
        return DBL_FAIL;
}

double getval(char *_expr, size_t _operpos, char _dir) {    // Dynamic memory: val_str
    bool reading = false;
    char chr, *val_str = NULL;
    size_t val_low, val_high;   // Range of indices in which value is located
    int nchr;
    double num;

    if (_dir != 'l' && _dir != 'r') // Left and right directions only
        return DBL_FAIL;
    for (nchr = _dir == 'r' ? _operpos + 1 : _operpos - 1; (chr = _expr[nchr]) && nchr >= 0; _dir == 'r' ? nchr++ : nchr--) {   // Get size of value string
        if (isnumer(chr) && !reading) {
            reading = true;
            _dir == 'r' ? (val_low = nchr) : (val_high = nchr);
        } else if (!isnumer(chr) && reading) {
            _dir == 'r' ? (val_high = nchr) : (val_low = nchr);
            break;
        }
    }
    if (!reading)   // No value found
        return 0;   // Must return 0 in this case to ensure proper evaluate() functionality (used in unary + and -)
    else if (nchr == -1)    // Reached beginning of expression
        val_low = 0;
    else if (chr == 0)  // Reached end of expression
        val_high = strlen(_expr) - 1;
    if ((val_str = popsub(_expr, val_low, val_high)) == NULL) {
        free(_expr);
        return DBL_FAIL;
    }
    if ((num = stod(val_str)) == DBL_FAIL) {
        free(val_str);
        return DBL_OVER;
    }
    free(val_str);
    return num; // Convert value string to value
}

double stod(const char *_s) {   // Equivalent to atof(), except that it does not print inaccurate numbers
    bool read_decim = false, is_negative = false, reading = false;
    char chr;
    int nchr;
    double num = 0, placeval = 0.1;

    for (nchr = 0; (chr = _s[nchr]) && nchr <= DBL_DIG + is_negative + read_decim; nchr++) {    // DBL_DIG is accurate digit limit
        if (chr == '-' && !is_negative && !reading)
            is_negative = true;
        else if (chr == '.' && !read_decim && !reading)
            read_decim = true;
        if (!read_decim && isdigit(chr)) {
            num *= 10;
            num += chr - 48;
        } else if (isdigit(chr)) {
            reading = true;
            num += placeval * (chr - 48);   // '0' = 48
            placeval /= 10;
        }
    }
    if (nchr > DBL_DIG && !read_decim)
        return DBL_OVER;
    if (is_negative)
        num *= -1;
    return num;
}

eval.h

#include <stdbool.h>

#ifndef PARSE_H
#define PARSE_H

#ifdef __GNU_LIBRARY__
#include <err.h>
#define PROG_NAME\
    program_invocation_name[1] == '/' ?\
    program_invocation_name + 2 :\
    program_invocation_name             // Ignore './' if included 

extern char *program_invocation_name;
#else
#define PROG_NAME   "eval"
#endif /* #ifdef __GNU_LIBRARY__ */

/* Initializes left- and right-hand values in evaluate() */
#define INIT_VALS()\
    lval = getval(expr, nchr, 'l');\
    rval = getval(expr, nchr, 'r');\
    llim = getlim(expr, nchr, 'l');\
    rlim = getlim(expr, nchr, 'r')

/* Initializes new expression string in evaluate() */
#define INIT_EXPR()\
    if ((result_str = dtos(result, DBL_DIG)) == NULL) {\
        free(expr);\
        fail("Internal error (evaluate.dtos)");\
    }\
    if ((expr = pushsub(expr, result_str, llim, rlim)) == NULL) {\
        free(result_str);\
        fail("Internal error (evaluate.pushsub)");\
    }

/* Checks for overflow, getval() failure, and missing operand(s) in evaluate()
 * Requires needed values, LEFT or RIGHT */
#define CHK_VALS(reqval)\
    if (isequal(rval, DBL_OVER) || isequal(lval, DBL_OVER))\
        goto overflow_err;\
    if (isequal(rval, DBL_FAIL) || isequal(lval, DBL_FAIL))\
        goto getval_err;\
    if (reqval & RIGHT && rlim == INT_FAIL)\
        goto opermiss_err;\
    if (reqval & LEFT && llim == INT_FAIL)\
        goto opermiss_err

#define INT_FAIL    INT_MAX     // Passed by [type]-returning functions on failure
#define DBL_FAIL    DBL_MAX
#define DBL_OVER    FLT_EPSILON // Passed by [type]-returning functions on overflow
#define STR_OVER    "..."
#define CHK_PASS    -1          // Used by chk_parenth() and chk_syntax(); Indicates valid syntax
#define RIGHT       1
#define LEFT        2
// #define DEBUG                // If defined, prints debug info

extern bool CMD_LINE;
static const char *VAL_CHRS = "+-!^*/%.()\n1234567890'",
                  *OPERS = "+-!^*/%",
                  *DBLS = "+-!",    // Can be double
                  *UNRY = "+-!",    // Can be unary
                  *BNRY = "^*/%";   // Can only be binary

/* Prints error message and exits program */
extern void fail(const char *_desc);

/* Prints string with character at given position underlined */
extern void printu(const char *_s, size_t _hpos);

/* Returns string representation of double
 * Returns NULL on failure
 * Returns OVERFLOW on overflow
 * Resulting string must be freed */
extern char *dtos(double _x, size_t _sig);

/* Returns new, allocated substring spanning the given elements
 * Returns NULL on failure */
extern char *popsub(const char *_s, size_t _low, size_t _high);

/* Returns new, allocated string where substring replaces given elements in old string
 * Frees given string and substring while returning a newly allocated one
 * Returns NULL on failure */
extern char *pushsub(char *_s, char *_sub, size_t _low, size_t _high);

/* Evaluates mathemetical expression starting from given index position
 * Returns string representation of result depending on index
 * Returns NULL on failure */
extern char *simplify(const char *_expr, size_t _from);

/* Returns true if floating-point numbers are equal */
extern bool isequal(double _x, double _y);

/* Returns true if character is in string */
extern bool isin(const char _x, const char *_y);

/* Returns true if character is numerical (digit || '-' || '.') */
extern bool isnumer(char _c);

/* Determines whether a parenthesis indicates multiplication */
extern bool toast(const char *_expr, size_t _parpos);

/* Get digit at given place */
extern size_t getdigit(double _x, int _place);

/* Returns number of whole places */
extern size_t nplaces(double _x);

/* Returns index position of first invalid parenthesis of expression
 * Returns CHK_PASS if no invalid parentheses are found */
extern int chk_parenth(const char *_expr);

/* Returns index position of first invalid character of expression
 * Returns CHK_PASS if no invalid characters are found */
extern int chk_syntax(const char *_expr);

/* Returns left- or right-hand limit of range of operation at given position in expression
 * Returns INT_FAIL if invalid direction or operand is missing */
extern int getlim(char *_expr, size_t _operpos, char _dir);

/* Returns OBS_R or OBS_L depending on type of obstruction(s)
 * If none are found, returns 0
 * Returns INT_FAIL on failure */
extern size_t fobst(const char *_expr, size_t _operpos, size_t _llim, size_t _rlim);

/* Evaluates mathematical expression from index position 0
 * Ignores parentheses and syntax errors
 * Returns DBL_FAIL on failure */
extern double evaluate(const char *_expr);

/* Returns left- or right-hand value of the operand at given position in the expression
 * Returns DBL_FAIL on failure */
extern double getval(char *_expr, size_t _operpos, char _dir);

/* Returns double representation of string
 * Returns DBL_FAIL on overflow */
extern double stod(const char *_s);

#endif /* #ifndef PARSE_H */

GitHub: https://github.com/crypticcu/eval

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for this great question - I hope you get some good reviews, and I hope to see more of your contributions here in future! \$\endgroup\$ Commented Jan 16, 2021 at 9:23

1 Answer 1

1
\$\begingroup\$

Naming

Please don't use ALL_CAPS identifiers for things that are not preprocessor macros:

extern bool CMD_LINE;
static const char *VAL_CHRS = "+-!^*/%.()\n1234567890'",
                  *OPERS = "+-!^*/%",
                  *DBLS = "+-!",    // Can be double
                  *UNRY = "+-!",    // Can be unary
                  *BNRY = "^*/%";   // Can only be binary

The usual naming convention helps alert us to names that don't obey the C rules of scope, and that expand arguments rather than evaluating them. When we name other things this way, it introduces confusion.

extern bool command_line_flag;
static const char *const valid_chars = "+-!^*/%.()\n1234567890'";
static const char *const all_operators = "+-!^*/%";
static const char *const unary_operators = "+-!";    // Can be unary
static const char *const binary_operators = "^*/%";   // Can only be binary

String conversion

ndec = atoi(argv[2]);

Prefer strtol() or sscanf(), because atoi() has no way of reporting failure, and we need to distinguish that from actual entered zero.

Terminal escapes

    puts("\n\e[4mresult\e[24m");

Don't assume that your output is going to a terminal, or that a connected terminal understands the control codes you expect. If you really want to do fancy terminal stuff, then (a) make it optional with a command-line flag and (b) use termcap or curses to abstract away the handling of different terminal types.

Know your tools

        swap = expr;    // Swap causes 'still reachable' error in valgrind

I like this - it shows that you've been using Valgrind to exercise your program.

Even better would be to unit-test as much as possible - occasionally run the tests under Valgrind, too.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you for the review! Every practice you mentioned was helpful and to-the-point. Now I know to improve my program's handling of flags, so that I can implement your escape code suggestion along with some other features I had in mind. \$\endgroup\$
    – Angel E.
    Commented Jan 16, 2021 at 18:48
  • \$\begingroup\$ If you like the review, the Stack Exchange way to show that is to vote the answer (the ▲ at top left of the answer). You can also "accept" an answer when you feel your code is completely reviewed - don't do that just yet, because my review is fairly cursory and you may well get a better one if you wait. \$\endgroup\$ Commented Jan 16, 2021 at 23:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.