Skip to content

Instantly share code, notes, and snippets.

@zserge
Created March 15, 2026 18:32
Show Gist options
  • Select an option

  • Save zserge/9781e3855002f4559ae20c16823aaa6b to your computer and use it in GitHub Desktop.

Select an option

Save zserge/9781e3855002f4559ae20c16823aaa6b to your computer and use it in GitHub Desktop.
#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