-
-
Save wangii/a99e6b903e2a81791d0345c554c5f1f0 to your computer and use it in GitHub Desktop.
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
| #include <assert.h> | |
| #include <ctype.h> | |
| #include <math.h> | |
| #include <ncurses.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #define NCOL 26 // max number of columns (A..Z) | |
| #define NROW 50 // max number of rows | |
| #define CW 9 // column display width | |
| #define GW 4 // row-number gutter | |
| #define MAXIN 128 // max cell input length | |
| enum { EMPTY, NUM, LABEL, FORMULA }; // cell types | |
| struct cell { | |
| int type; | |
| float val; | |
| char text[MAXIN]; // raw user input | |
| int fmt; // 0=general 'I'=integer '$'=dollar 'L'=left 'R'=right | |
| }; | |
| struct grid { | |
| struct cell cells[NCOL][NROW]; | |
| int cc, cr, vc, vr; | |
| }; | |
| struct parser { | |
| const char *s, *p; | |
| struct grid* g; | |
| }; | |
| float expr(struct parser* p); | |
| struct cell* cell(struct grid* g, int c, int r) { | |
| return (c >= 0 && c < NCOL && r >= 0 && r < NROW) ? &g->cells[c][r] : NULL; | |
| } | |
| void recalc(struct grid* g) { | |
| for (int pass = 0; pass < 100; pass++) { | |
| int changed = 0; | |
| for (int r = 0; r < NROW; r++) | |
| for (int c = 0; c < NCOL; c++) { | |
| struct cell* cl = &g->cells[c][r]; | |
| if (cl->type != FORMULA) continue; | |
| struct parser p = {cl->text, cl->text, g}; | |
| float v = expr(&p); | |
| if (v != cl->val) changed = 1; | |
| cl->val = v; | |
| } | |
| if (!changed) break; | |
| } | |
| } | |
| void setcell(struct grid* g, int c, int r, const char* input) { | |
| struct cell* cl = cell(g, c, r); | |
| if (!cl) return; | |
| if (!*input) { | |
| *cl = (struct cell){0}; | |
| recalc(g); | |
| return; | |
| } | |
| strncpy(cl->text, input, MAXIN - 1); | |
| if (input[0] == '+' || input[0] == '-' || input[0] == '(' || input[0] == '@') { | |
| cl->type = FORMULA; | |
| } else if (isdigit(input[0]) || input[0] == '.') { | |
| char* end; | |
| double v = strtod(input, &end); | |
| cl->type = (*end == '\0') ? NUM : FORMULA; | |
| if (cl->type == NUM) cl->val = v; | |
| } else { | |
| cl->type = LABEL; | |
| cl->val = 0; | |
| } | |
| recalc(g); | |
| } | |
| void skipws(struct parser* p) { for (; isspace(*p->p); p->p++); } | |
| float number(struct parser* p) { | |
| char* end; | |
| float v = strtof(p->p, &end); | |
| if (end == p->p) return NAN; | |
| p->p = end; | |
| return v; | |
| } | |
| int ref(const char* s, int* col, int* row) { | |
| char* end; | |
| const char* p = s; | |
| if (!isalpha(*p)) return 0; | |
| *col = toupper(*p++) - 'A'; | |
| if (isalpha(*p)) *col = *col * 26 + (toupper(*p++) - 'A'); | |
| int n = strtol(p, &end, 10); | |
| if (n <= 0 || end == p) return 0; | |
| *row = n - 1; | |
| return (int)(end - s); | |
| } | |
| static float cellval(struct parser* p) { | |
| int c, r, n = ref(p->p, &c, &r); | |
| if (!n) return NAN; | |
| p->p += n; | |
| struct cell* cl = cell(p->g, c, r); | |
| if (!cl) return NAN; | |
| return cl->val; | |
| } | |
| float func(struct parser* p) { | |
| char fn[16] = {0}; | |
| for (int i = 0; isalpha(*p->p) && i < sizeof(fn) - 1;) fn[i++] = toupper(*p->p++); | |
| if (*p->p != '(') return NAN; | |
| p->p++; | |
| skipws(p); | |
| // Try to parse range: A1...B5 | |
| int c1, r1, c2, r2, is_range = 0; | |
| const char* q = p->p; | |
| int n = ref(q, &c1, &r1); | |
| if (n) { | |
| q += n; | |
| if (*q == '.' && *(q + 1) == '.' && *(q + 2) == '.') { | |
| q += 3; | |
| n = ref(q, &c2, &r2); | |
| if (n) { | |
| p->p = q + n; | |
| is_range = 1; | |
| } | |
| } | |
| } | |
| float result = 0; | |
| if (is_range && strcmp(fn, "SUM") == 0) { | |
| if (c1 > c2) n = c1, c1 = c2, c2 = n; | |
| if (r1 > r2) n = r1, r1 = r2, r2 = n; | |
| for (int c = c1; c <= c2; c++) | |
| for (int r = r1; r <= r2; r++) { | |
| struct cell* cl = cell(p->g, c, r); | |
| if (cl && cl->type != EMPTY && cl->type != LABEL) result += cl->val; | |
| } | |
| } else if (!is_range) { | |
| float arg = expr(p); | |
| if (strcmp(fn, "SUM") == 0) | |
| result = arg; | |
| else if (strcmp(fn, "ABS") == 0) | |
| result = fabs(arg); | |
| else if (strcmp(fn, "INT") == 0) | |
| result = (int)arg; | |
| else if (strcmp(fn, "SQRT") == 0) | |
| result = arg >= 0 ? sqrt(arg) : NAN; | |
| else | |
| return NAN; | |
| } else { | |
| return NAN; | |
| } | |
| skipws(p); | |
| if (*p->p != ')') return NAN; | |
| p->p++; | |
| return result; | |
| } | |
| float primary(struct parser* p) { | |
| skipws(p); | |
| if (!*p->p) return NAN; | |
| if (*p->p == '+') p->p++; | |
| if (*p->p == '-') { | |
| p->p++; | |
| return -primary(p); | |
| } | |
| if (*p->p == '@') { | |
| p->p++; | |
| return func(p); | |
| } | |
| if (*p->p == '(') { | |
| p->p++; | |
| float v = expr(p); | |
| skipws(p); | |
| if (*p->p != ')') return NAN; | |
| p->p++; | |
| return v; | |
| } | |
| if (isdigit(*p->p) || *p->p == '.') return number(p); | |
| return cellval(p); | |
| } | |
| float term(struct parser* p) { | |
| float l = primary(p); | |
| for (;;) { | |
| skipws(p); | |
| char op = *p->p; | |
| if (op != '*' && op != '/') break; | |
| p->p++; | |
| float r = primary(p); | |
| if (op == '*') | |
| l *= r; | |
| else if (r == 0) | |
| return NAN; | |
| else | |
| l /= r; | |
| } | |
| return l; | |
| } | |
| float expr(struct parser* p) { | |
| float l = term(p); | |
| for (;;) { | |
| skipws(p); | |
| char op = *p->p; | |
| if (op != '+' && op != '-') break; | |
| p->p++; | |
| float r = term(p); | |
| l = (op == '+') ? l + r : l - r; | |
| } | |
| return l; | |
| } | |
| static int vcols(void) { return (COLS - GW) / CW > 0 ? (COLS - GW) / CW : 1; } | |
| static int vrows(void) { return (LINES - 4) > 0 ? (LINES - 4) : 1; } | |
| enum { READY, ENTRY, GOTO }; | |
| static void draw(struct grid* g, int mode, const char* buf) { | |
| erase(); | |
| // Status bar | |
| attron(A_BOLD | A_REVERSE); | |
| move(0, 0); | |
| clrtoeol(); | |
| struct cell* cur = cell(g, g->cc, g->cr); | |
| mvprintw(0, 0, " %c%c%d", g->cc > 26 ? g->cc / 26 : ' ', 'A' + g->cc % 26, g->cr + 1); | |
| if (cur && cur->type == NUM) | |
| printw(" %.10g", cur->val); | |
| else if (cur && cur->type == FORMULA) | |
| printw(" %s = %s%.10g", cur->text, isnan(cur->val) ? "ERR " : "", | |
| isnan(cur->val) ? 0.0 : cur->val); | |
| else if (cur && cur->type == LABEL) | |
| printw(" %s", cur->text); | |
| mvprintw(0, COLS - 6, mode == ENTRY ? "ENTRY" : mode == GOTO ? " GOTO" : "READY"); | |
| attroff(A_BOLD | A_REVERSE); | |
| // Edit line | |
| move(1, 0); | |
| clrtoeol(); | |
| if (mode) | |
| mvprintw(1, 0, "> %s_", buf); | |
| else if (cur && cur->type != EMPTY) | |
| mvprintw(1, 0, " %s", cur->text); | |
| /* Row 2: column headers */ | |
| attron(A_BOLD | A_REVERSE); | |
| move(2, 0); | |
| clrtoeol(); | |
| for (int c = 0; c < vcols() && g->vc + c < NCOL; c++) | |
| mvprintw(2, GW + c * CW, "%*c", CW, 'A' + g->vc + c); | |
| attroff(A_BOLD | A_REVERSE); | |
| // Grid cells | |
| for (int r = 0; r < vrows() && g->vr + r < NROW; r++) { | |
| int row = g->vr + r, y = 3 + r; | |
| move(y, 0); | |
| clrtoeol(); | |
| attron(A_REVERSE); | |
| mvprintw(y, 0, "%*d ", GW - 1, row + 1); | |
| attroff(A_REVERSE); | |
| for (int c = 0; c < vcols() && g->vc + c < NCOL; c++) { | |
| int col = g->vc + c; | |
| struct cell* cl = cell(g, col, row); | |
| char fb[64]; | |
| if (!cl || cl->type == EMPTY) { | |
| memset(fb, ' ', CW); | |
| fb[CW] = '\0'; | |
| } else if (cl->type == LABEL) | |
| snprintf(fb, CW + 1, "%-*.*s", CW, CW, cl->text[0] == '"' ? cl->text + 1 : cl->text); | |
| else if (isnan(cl->val)) { | |
| snprintf(fb, CW + 1, "%*s", CW, "ERROR"); | |
| } else { | |
| char t[32]; | |
| if (cl->fmt == '$') | |
| snprintf(t, sizeof(t), "%.2f", cl->val); | |
| else if (cl->fmt == 'I') | |
| snprintf(t, sizeof(t), "%ld", (long)cl->val); | |
| else if (cl->val == (long)cl->val && fabs(cl->val) < 1e9) | |
| snprintf(t, sizeof(t), "%ld", (long)cl->val); | |
| else | |
| snprintf(t, sizeof(t), "%.2f", cl->val); | |
| snprintf(fb, CW + 1, cl->fmt == 'L' ? "%-*s" : "%*s", CW, t); | |
| } | |
| int is_cur = (col == g->cc && row == g->cr); | |
| if (is_cur) attron(A_REVERSE); | |
| mvprintw(y, GW + c * CW, "%s", fb); | |
| if (is_cur) attroff(A_REVERSE); | |
| } | |
| } | |
| // Help bar | |
| attron(A_REVERSE); | |
| move(LINES - 1, 0); | |
| clrtoeol(); | |
| mvprintw(LINES - 1, 0, " /:Cmd >=Goto !=Recalc /B:Blank /Q:Quit /F(L/R/I/D/G):Fmt"); | |
| attroff(A_REVERSE); | |
| refresh(); | |
| } | |
| #if 1 | |
| int main(void) { | |
| struct grid g = {0}; | |
| initscr(); | |
| raw(); | |
| keypad(stdscr, TRUE); | |
| noecho(); | |
| curs_set(0); | |
| int mode = READY, len = 0; | |
| char buf[MAXIN] = {0}; | |
| for (;;) { | |
| if (g.cc < g.vc) g.vc = g.cc; | |
| if (g.cc >= g.vc + vcols()) g.vc = g.cc - vcols() + 1; | |
| if (g.cr < g.vr) g.vr = g.cr; | |
| if (g.cr >= g.vr + vrows()) g.vr = g.cr - vrows() + 1; | |
| draw(&g, mode, buf); | |
| int ch = getch(); | |
| if (mode == READY) { | |
| if (ch == (0x1f & 'c')) | |
| break; | |
| else if (ch == KEY_UP && g.cr > 0) | |
| g.cr--; | |
| else if (ch == KEY_DOWN && g.cr < NROW - 1) | |
| g.cr++; | |
| else if (ch == KEY_LEFT && g.cc > 0) | |
| g.cc--; | |
| else if (ch == KEY_RIGHT && g.cc < NCOL - 1) | |
| g.cc++; | |
| else if (ch == KEY_HOME) | |
| g.cc = g.cr = 0; | |
| else if (ch == 9 && g.cc < NCOL - 1) | |
| g.cc++; | |
| else if (ch == 10 || ch == 13 || ch == KEY_ENTER) { | |
| if (g.cr < NROW - 1) g.cr++; | |
| } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { | |
| struct cell* cl = cell(&g, g.cc, g.cr); | |
| if (cl) *cl = (struct cell){0}; | |
| recalc(&g); | |
| } else if (ch == '!') { | |
| recalc(&g); | |
| } else if (ch == '/') { | |
| mvprintw(1, 0, "Command: /"); | |
| clrtoeol(); | |
| refresh(); | |
| ch = getch(); | |
| if (toupper(ch) == 'Q') | |
| break; | |
| else if (toupper(ch) == 'B') { | |
| struct cell* cl = cell(&g, g.cc, g.cr); | |
| if (cl) *cl = (struct cell){0}; | |
| recalc(&g); | |
| } else if (toupper(ch) == 'F') { | |
| mvprintw(1, 10, "F"); | |
| refresh(); | |
| ch = toupper(getch()); | |
| struct cell* cl = cell(&g, g.cc, g.cr); | |
| if (cl && (ch == 'L' || ch == 'R' || ch == 'I' || ch == 'G')) | |
| cl->fmt = (ch == 'G') ? 0 : ch; | |
| else if (cl && (ch == 'D' || ch == '$')) | |
| cl->fmt = '$'; | |
| } | |
| } else if (ch == '>') { | |
| mode = GOTO; | |
| buf[0] = '\0'; | |
| len = 0; | |
| } else if (isalpha(ch)) { | |
| mode = ENTRY; | |
| buf[0] = '"'; | |
| buf[1] = ch; | |
| buf[2] = '\0'; | |
| len = 2; | |
| } else if (ch == '"' || ch == '+' || ch == '-' || ch == '(' || ch == '@' || ch == '.' || | |
| isdigit(ch)) { | |
| mode = ENTRY; | |
| buf[0] = ch; | |
| buf[1] = '\0'; | |
| len = 1; | |
| } | |
| } else { /* ENTRY or GOTO — shared line editing */ | |
| if (ch == 27) { | |
| mode = READY; | |
| buf[0] = '\0'; | |
| len = 0; | |
| } else if (ch == 10 || ch == 13 || ch == KEY_ENTER) { | |
| if (mode == ENTRY) { | |
| setcell(&g, g.cc, g.cr, buf); | |
| if (g.cr < NROW - 1) g.cr++; | |
| } else { | |
| int c, r; | |
| if (ref(buf, &c, &r)) g.cc = c, g.cr = r; | |
| } | |
| mode = READY; | |
| buf[0] = '\0'; | |
| len = 0; | |
| } else if (ch == 9 && mode == ENTRY) { | |
| setcell(&g, g.cc, g.cr, buf); | |
| if (g.cc < NCOL - 1) g.cc++; | |
| mode = READY; | |
| buf[0] = '\0'; | |
| len = 0; | |
| } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { | |
| if (len > 0) buf[--len] = '\0'; | |
| if (len == 0) mode = READY; | |
| } else if (len < MAXIN - 1 && ch >= 32 && ch < 127) { | |
| buf[len++] = ch; | |
| buf[len] = '\0'; | |
| } | |
| } | |
| } | |
| endwin(); | |
| return 0; | |
| } | |
| #else | |
| int main(void) { | |
| struct grid g = {0}; | |
| struct parser p = {0}; | |
| g.cells[0][0].type = NUM; | |
| g.cells[0][0].val = 3.0f; | |
| g.cells[0][1].type = NUM; | |
| g.cells[0][1].val = 5.0f; | |
| g.cells[0][2].type = NUM; | |
| g.cells[0][2].val = 11.0f; | |
| g.cells[0][3].type = NUM; | |
| g.cells[0][3].val = -13.5f; | |
| #define EXPR(e) (p.s = p.p = e, p.g = &g, expr(&p)) | |
| // numbers | |
| assert(isnan(EXPR(""))); | |
| assert(EXPR("42") == 42.0f); | |
| assert(EXPR("1.5") == 1.5f); | |
| assert(EXPR(".5") == 0.5f); | |
| assert(EXPR("-123") == -123.0f); | |
| assert(EXPR("+123") == 123.0f); | |
| assert(EXPR("(123)") == 123.0f); | |
| // references | |
| assert(EXPR("A1") == 3.0f); | |
| assert(EXPR("A2") == 5.0f); | |
| assert(EXPR("A3") == 11.0f); | |
| assert(EXPR("B1") == 0.0f); | |
| assert(EXPR("A12") == 0.0f); | |
| // expressions | |
| assert(EXPR("A1*A2") == 15.0f); | |
| assert(EXPR("A1*10/A2") == 6.0f); | |
| assert(isnan(EXPR("A1/0"))); | |
| assert(EXPR("A1+A2") == 8.0f); | |
| assert(EXPR("A1+A2-A3") == -3.0f); | |
| assert(EXPR("A1+A2*A3") == 58.0f); | |
| assert(EXPR("(A1+A2)*A3") == 88.0f); | |
| // functions | |
| assert(EXPR("@ABS(A1)") == 3.0f); | |
| assert(EXPR("@ABS(A4)") == 13.5f); | |
| assert(EXPR("@INT(A4)") == -13.0f); | |
| assert(EXPR("@INT(@ABS(A4))") == 13.0f); | |
| assert(EXPR("@SQRT(A3+A2))") == 4.0f); | |
| assert(EXPR("@SUM(A3))") == 11.0f); | |
| assert(EXPR("@SUM(A1...A3))") == 19.0f); | |
| assert(EXPR("@SUM(A3...A1))") == 19.0f); | |
| assert(EXPR("@SUM(A1...A1))") == 3.0f); | |
| assert(EXPR("@SUM(A3...Z1))") == 19.0f); | |
| #undef EXPR | |
| // testing re-evaluation | |
| setcell(&g, 0, 0, "5"); | |
| setcell(&g, 0, 1, "7"); | |
| setcell(&g, 0, 2, "11"); | |
| setcell(&g, 0, 3, "+@SUM(A1...A3)"); | |
| assert(g.cells[0][3].val == 23.0f); | |
| setcell(&g, 0, 0, "5"); | |
| setcell(&g, 0, 1, "+A1+5"); | |
| setcell(&g, 0, 2, "+A2+A1"); | |
| assert(g.cells[0][3].val == 5.0f + 10.0f + 15.0f); | |
| setcell(&g, 0, 0, "7"); | |
| assert(g.cells[0][3].val == 7.0f + 12.0f + 19.0f); | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment