Created
August 6, 2025 10:11
-
-
Save tung/3040d3f3dc67304ca55695bbea66062e to your computer and use it in GitHub Desktop.
Math expression calculator in C, using precedence climbing for operator precedence.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Math expression calculator in C, using precedence climbing for operator precedence. | |
| #include <ctype.h> | |
| #include <math.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <string.h> | |
| // Build tests that can be run with "--test". | |
| // Needs "utest.h" in the same dir: https://github.com/sheredom/utest.h | |
| //#define TEST | |
| #ifdef TEST | |
| #include <float.h> | |
| #include "utest.h" | |
| UTEST_STATE(); | |
| #endif | |
| typedef struct { | |
| int bytes_read; | |
| const char* rest; | |
| } Parser; | |
| #define DEFINE_OPTION(ty) \ | |
| typedef struct { \ | |
| bool is_some; \ | |
| ty inner; \ | |
| } Option##ty; | |
| DEFINE_OPTION(double); | |
| typedef struct { | |
| int count; | |
| int value; | |
| } Digits; | |
| DEFINE_OPTION(Digits); | |
| typedef enum { | |
| OpAdd, | |
| OpSub, | |
| OpMul, | |
| OpDiv, | |
| OpExp, | |
| } Operator; | |
| DEFINE_OPTION(Operator); | |
| typedef struct { | |
| int precedence; | |
| bool left_associative; | |
| } OperatorData; | |
| typedef enum { | |
| ExpectedOpenParen, | |
| ExpectedCloseParen, | |
| ExpectedExpression, | |
| } Err; | |
| typedef struct { | |
| bool is_ok; | |
| union { | |
| double inner; | |
| Err err; | |
| }; | |
| } Result; | |
| OperatorData OPERATORS[] = { | |
| [OpAdd] = { 1, true }, | |
| [OpSub] = { 1, true }, | |
| [OpMul] = { 2, true }, | |
| [OpDiv] = { 2, true }, | |
| [OpExp] = { 3, false }, | |
| }; | |
| #define OP_MAX_PRECEDENCE 4 | |
| void whitespace(Parser* p) { | |
| while (p->rest[0] == ' ') { | |
| p->bytes_read++; | |
| p->rest++; | |
| } | |
| } | |
| bool literal(Parser* p, const char* s) { | |
| whitespace(p); | |
| size_t s_len = strlen(s); | |
| if (strncmp(p->rest, s, s_len)) return false; | |
| if (isalpha(s[0]) && isalpha(p->rest[s_len])) return false; | |
| p->bytes_read += s_len; | |
| p->rest += s_len; | |
| return true; | |
| } | |
| OptionDigits digits(Parser* p) { | |
| whitespace(p); | |
| OptionDigits opt_digits = (OptionDigits){ false, {0} }; | |
| opt_digits.is_some = isdigit(p->rest[0]); | |
| while (isdigit(p->rest[0])) { | |
| opt_digits.inner.count++; | |
| opt_digits.inner.value *= 10; | |
| opt_digits.inner.value += p->rest[0] - '0'; | |
| p->bytes_read++; | |
| p->rest++; | |
| } | |
| return opt_digits; | |
| } | |
| Optiondouble number(Parser* p) { | |
| Optiondouble opt_double = (Optiondouble){ false, 0.0 }; | |
| OptionDigits opt_whole_digits = digits(p); | |
| if (!opt_whole_digits.is_some) return opt_double; | |
| Parser frac_parser = *p; | |
| if (!literal(&frac_parser, ".")) { | |
| opt_double.is_some = true; | |
| opt_double.inner = (double)opt_whole_digits.inner.value; | |
| return opt_double; | |
| } | |
| OptionDigits opt_frac_digits = digits(&frac_parser); | |
| if (!opt_frac_digits.is_some) { | |
| opt_double.is_some = true; | |
| opt_double.inner = (double)opt_whole_digits.inner.value; | |
| return opt_double; | |
| } | |
| opt_double.is_some = true; | |
| opt_double.inner = (double)opt_frac_digits.inner.value; | |
| for (int i = 0; i < opt_frac_digits.inner.count; i++) opt_double.inner *= 0.1; | |
| opt_double.inner += (double)opt_whole_digits.inner.value; | |
| p->bytes_read = frac_parser.bytes_read; | |
| p->rest = frac_parser.rest; | |
| return opt_double; | |
| } | |
| OptionOperator operator(Parser* p, int min_precedence) { | |
| Parser op_parser = *p; | |
| Operator op; | |
| if (literal(&op_parser, "+")) op = OpAdd; | |
| else if (literal(&op_parser, "-")) op = OpSub; | |
| else if (literal(&op_parser, "*")) op = OpMul; | |
| else if (literal(&op_parser, "/")) op = OpDiv; | |
| else if (literal(&op_parser, "^")) op = OpExp; | |
| else return (OptionOperator){ false, 0 }; | |
| if (OPERATORS[op].precedence < min_precedence) { | |
| return (OptionOperator){ false, 0 }; | |
| } | |
| p->bytes_read = op_parser.bytes_read; | |
| p->rest = op_parser.rest; | |
| return (OptionOperator){ true, op }; | |
| } | |
| double compute(Operator op, double lhs, double rhs) { | |
| switch (op) { | |
| case OpAdd: return lhs + rhs; | |
| case OpSub: return lhs - rhs; | |
| case OpMul: return lhs * rhs; | |
| case OpDiv: return lhs / rhs; | |
| case OpExp: return pow(lhs, rhs); | |
| } | |
| return 0.0; | |
| } | |
| Result expr(Parser* p, int min_precedence); | |
| Result atom(Parser* p) { | |
| if (literal(p, "(")) { | |
| Result x = expr(p, 0); | |
| if (!x.is_ok) return x; | |
| if (!literal(p, ")")) return (Result){ false, { .err = ExpectedExpression } }; | |
| return (Result){ true, { x.inner } }; | |
| } | |
| if (literal(p, "-")) { | |
| Result x = expr(p, OP_MAX_PRECEDENCE + 1); | |
| if (x.is_ok) x.inner *= -1.0; | |
| return x; | |
| } | |
| if (literal(p, "sqrt")) { | |
| if (!literal(p, "(")) return (Result){ false, { .err = ExpectedOpenParen } }; | |
| Result x = expr(p, 0); | |
| if (!x.is_ok) return x; | |
| if (!literal(p, ")")) return (Result){ false, { .err = ExpectedCloseParen } }; | |
| x.inner = sqrt(x.inner); | |
| return x; | |
| } | |
| Optiondouble opt_double = number(p); | |
| if (opt_double.is_some) return (Result){ true, { .inner = opt_double.inner } }; | |
| return (Result){ false, { .err = ExpectedExpression } }; | |
| } | |
| Result expr(Parser* p, int min_precedence) { | |
| Result res_atom = atom(p); | |
| if (!res_atom.is_ok) return res_atom; | |
| double num = res_atom.inner; | |
| for (;;) { | |
| OptionOperator opt_op = operator(p, min_precedence); | |
| if (!opt_op.is_some) break; | |
| int next_min_precedence = OPERATORS[opt_op.inner].precedence; | |
| if (OPERATORS[opt_op.inner].left_associative) next_min_precedence++; | |
| Result res_rhs = expr(p, next_min_precedence); | |
| if (!res_rhs.is_ok) return res_rhs; | |
| num = compute(opt_op.inner, num, res_rhs.inner); | |
| } | |
| return (Result){ true, { .inner = num } }; | |
| } | |
| int main(int argc, const char* argv[]) { | |
| if (argc < 2) { | |
| fprintf(stderr, "Usage: %s [expression]\n", argc > 0 ? argv[0] : "calc"); | |
| return 1; | |
| } | |
| #ifdef TEST | |
| if (argc >= 2 && !strcmp(argv[1], "--test")) { | |
| return utest_main(argc - 1, argv + 1); | |
| } | |
| #endif | |
| if (argc > 2) { | |
| fprintf(stderr, "Usage: %s [expression]\n", argc > 0 ? argv[0] : "calc"); | |
| return 1; | |
| } | |
| Parser p = (Parser){ 0, argv[1] }; | |
| Result res = expr(&p, 0); | |
| if (res.is_ok) whitespace(&p); | |
| if (!p.rest[0]) { | |
| printf("%g\n", res.inner); | |
| } else { | |
| fprintf(stderr, "%s\n", argv[1]); | |
| for (int i = 0; i < (&p.rest[0] - argv[1]); i++) fputc(' ', stderr); | |
| fputs("^ Error: ", stderr); | |
| if (res.is_ok) { | |
| fputs("trailing input\n", stderr); | |
| } else { | |
| switch (res.err) { | |
| case ExpectedOpenParen: | |
| fputs("expected open parenthesis\n", stderr); | |
| break; | |
| case ExpectedCloseParen: | |
| fputs("expected close parenthesis\n", stderr); | |
| break; | |
| case ExpectedExpression: | |
| fputs("expected expression\n", stderr); | |
| break; | |
| } | |
| } | |
| return 2; | |
| } | |
| return 0; | |
| } | |
| #ifdef TEST | |
| #define TEST_WHITESPACE(br, rst, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| whitespace(&p); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| UTEST(parser, whitespace) { | |
| Parser p; | |
| TEST_WHITESPACE(0, "", ""); | |
| TEST_WHITESPACE(0, "1", "1"); | |
| TEST_WHITESPACE(1, "", " "); | |
| TEST_WHITESPACE(1, "1", " 1"); | |
| TEST_WHITESPACE(2, "", " "); | |
| } | |
| #define TEST_LITERAL(br, rst, res, lit, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| EXPECT_EQ(res, literal(&p, lit)); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| UTEST(parser, literal) { | |
| Parser p; | |
| TEST_LITERAL(0, "", true, "", ""); | |
| TEST_LITERAL(1, "", true, "(", "("); | |
| TEST_LITERAL(1, "(", true, "(", "(("); | |
| TEST_LITERAL(3, "", true, "aaa", "aaa"); | |
| TEST_LITERAL(0, "aaaa", false, "aaa", "aaaa"); | |
| TEST_LITERAL(4, "", true, "aaa", " aaa"); | |
| TEST_LITERAL(0, "aaa", false, "bbb", "aaa"); | |
| TEST_LITERAL(3, " bbb", true, "aaa", "aaa bbb"); | |
| TEST_LITERAL(0, "aaabbb", false, "aaa", "aaabbb"); | |
| } | |
| #define TEST_NO_DIGITS(br, rst, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = digits(&p); \ | |
| EXPECT_FALSE(res.is_some); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| #define TEST_DIGITS(br, rst, cnt, val, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = digits(&p); \ | |
| ASSERT_TRUE(res.is_some); \ | |
| EXPECT_EQ(cnt, res.inner.count); \ | |
| EXPECT_EQ(val, res.inner.value); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| UTEST(parser, digits) { | |
| Parser p; | |
| OptionDigits res; | |
| TEST_NO_DIGITS(0, "", ""); | |
| TEST_NO_DIGITS(0, "aaa", "aaa"); | |
| TEST_DIGITS(3, "", 3, 123, "123"); | |
| TEST_DIGITS(4, "", 3, 123, " 123"); | |
| } | |
| #define TEST_NO_NUMBER(br, rst, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = number(&p); \ | |
| EXPECT_FALSE(res.is_some); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| #define TEST_NUMBER(br, rst, val, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = number(&p); \ | |
| ASSERT_TRUE(res.is_some); \ | |
| EXPECT_NEAR(val, res.inner, DBL_EPSILON); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while(0) | |
| UTEST(parser, number) { | |
| Parser p; | |
| Optiondouble res; | |
| TEST_NO_NUMBER(0, "", ""); | |
| TEST_NUMBER(3, "", 123.0, "123"); | |
| TEST_NUMBER(3, ".", 123.0, "123."); | |
| TEST_NUMBER(7, "", 123.456, "123.456"); | |
| TEST_NUMBER(3, ".abc", 123.0, "123.abc"); | |
| } | |
| #define TEST_ATOM_ERR(br, rst, e, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = atom(&p); \ | |
| ASSERT_FALSE(res.is_ok); \ | |
| EXPECT_EQ(e, (int)res.err); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| #define TEST_ATOM_OK(br, rst, val, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = atom(&p); \ | |
| ASSERT_TRUE(res.is_ok); \ | |
| EXPECT_NEAR(val, res.inner, DBL_EPSILON); \ | |
| EXPECT_EQ(br, p.bytes_read); \ | |
| EXPECT_STREQ(rst, p.rest); \ | |
| } while (0) | |
| UTEST(parser, atom) { | |
| Parser p; | |
| Result res; | |
| TEST_ATOM_ERR(0, "", ExpectedExpression, ""); | |
| TEST_ATOM_OK(7, "", 123.456, "123.456"); | |
| TEST_ATOM_OK(9, "", 123.456, "(123.456)"); | |
| } | |
| UTEST(expr, empty) { | |
| Parser p = (Parser){ 0, "" }; | |
| Result res = expr(&p, 0); | |
| ASSERT_FALSE(res.is_ok); | |
| EXPECT_EQ(ExpectedExpression, (int)res.err); | |
| } | |
| #define TEST_EXPR(val, input) \ | |
| do { \ | |
| p = (Parser){ 0, input }; \ | |
| res = expr(&p, 0); \ | |
| ASSERT_TRUE(res.is_ok); \ | |
| EXPECT_NEAR(val, res.inner, DBL_EPSILON); \ | |
| } while (0) | |
| UTEST(expr, solo) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(1.0, "1"); | |
| TEST_EXPR(0.2, "0.2"); | |
| } | |
| UTEST(expr, ops) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(15.0, "12+3"); | |
| TEST_EXPR(9.0, "12-3"); | |
| TEST_EXPR(36.0, "12*3"); | |
| TEST_EXPR(4.0, "12/3"); | |
| TEST_EXPR(1728.0, "12^3"); | |
| } | |
| UTEST(expr, precedence) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(10.0, "5+4*3/2-1"); | |
| TEST_EXPR(5.0, "4+3*6/9-1"); | |
| TEST_EXPR(1.0, "5+2*6/4-7"); | |
| TEST_EXPR(81.0, "-9^2"); | |
| } | |
| UTEST(expr, paren) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(1.0, "(1)"); | |
| TEST_EXPR(1.0, "(((1)))"); | |
| TEST_EXPR(60.0, "(2+3)*(5+7)"); | |
| } | |
| UTEST(expr, prefix) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(-1.0, "-1"); | |
| TEST_EXPR(1.0, "--1"); | |
| TEST_EXPR(-1.0, "---1"); | |
| TEST_EXPR(-1.0, "- 1"); | |
| TEST_EXPR(-1.0, "(-1)"); | |
| TEST_EXPR(-1.0, "-(1)"); | |
| TEST_EXPR(1.0, "-(-1)"); | |
| TEST_EXPR(2.0, "1--1"); | |
| TEST_EXPR(0.0, "1---1"); | |
| } | |
| UTEST(expr, pow) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(1.0, "1 ^ 1"); | |
| TEST_EXPR(4.0, "2^2"); | |
| TEST_EXPR(8.0, "2^3"); | |
| TEST_EXPR(9.0, "3^2"); | |
| TEST_EXPR(27.0, "3^3"); | |
| TEST_EXPR(1.0, "4^0"); | |
| } | |
| UTEST(expr, sqrt) { | |
| Parser p; | |
| Result res; | |
| TEST_EXPR(1.0, "sqrt(1)"); | |
| TEST_EXPR(2.0, "sqrt(4)"); | |
| TEST_EXPR(3.0, "sqrt(9)"); | |
| TEST_EXPR(4.0, "sqrt(16)"); | |
| TEST_EXPR(5.0, "sqrt(25)"); | |
| TEST_EXPR(1.0, "sqrt (1)"); | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment