
2026/03/17 19:39
**VisiCalc 再構築**
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
記事は、1979年のオリジナルが「キラーアプリ」と呼ばれた主要なスプレッドシート機能に焦点を当てつつ、C言語で最小限のVisiCalcクローンを再構築する方法について説明しています。まず、Dan Bricklin と Bob Frankston が16 KB Apple‑II マシン向けに約3,000行の6502アセンブリで作成した VisuCalc の簡潔な歴史と、その売上が100万部を突破したことを紹介します。
クローンの データモデル
セル内容は
enum { EMPTY, NUM, LABEL, FORMULA } を使って保存し、デフォルトのグリッドは 26 列(A–Z) × 50 行です。再帰下降パーサーが数値、セル参照、関数 (@SUM, @ABS, @INT, @SQRT) を処理し、括弧と基本算術演算 (+ − * /) も扱います。依存関係は、グリッド全体を最大で100回まで再計算し、値に変化がなくなるまで繰り返すことで解決します。これは VisiCalc の手動「recalc」コマンドと同様です。
UI
ncurses で構築され、4つの垂直領域(ステータスバー、編集行、列ヘッダー、グリッド)にレイアウトされています。ユーザーは矢印キーで移動し、スクロール時にはカーソルを中心に表示が保たれます。プログラムはモーダルモード (READY, ENTRY, GOTO) で動作します。READY モードでは /B(空白)、/Q(終了)、/F(フォーマット)、>(移動)などのコマンドが利用可能です。任意の印刷可能文字は編集を開始します。
実装全体は 500 行未満の C コードで、ファイル I/O、大きなグリッドサイズ、高度な範囲関数、および列/行ロック機能は省略されています。そのため、スプレッドシート内部構造を学習したい開発者やホビイストにとって、軽量で扱いやすい学習ツールとなっています。
参考リンク
- GitHub の最小限の VisiCalc 実装(単一の gist)
- より完全な再実装 (
) へのリンクkalk
本文
VisiCalc:数行のCで作るオリジナルスプレッドシート
概要
- VisiCalc は約半世紀にわたり世界を支配した。
- その UX は極めてシンプルで学習コストが低く、データ操作・ロジック記述・結果可視化・アート作成・GameBoy のゲーム実行までこなせる数少ないツールの一つ。
歴史
- 1979年:Dan Bricklin と Bob Frankston が VisiCalc(初代スプレッドシートソフト)を発表。
- 約3 000 行の6502アセンブリで書かれ、16 KB RAM のマシン上で動作。
- Apple II の「キラ―アプリ」として1 百万本以上販売され、初期 PC をビジネスツールへと変貌させた。
目的
ゼロから最小限の VisiCalc クローンを再実装する:
- データモデル
- 式評価器
- セル表示用簡易 UI
結果は次のようになる:
CellsLike
1. データモデル
#define MAXIN 128 // 入力文字列最大長 enum { EMPTY, NUM, LABEL, FORMULA }; // セルタイプ struct cell { int type; float val; // 評価済み値 char text[MAXIN]; // 生入力 }; #define NCOL 26 // 列 A..Z #define NROW 50 // 行 struct grid { struct cell cells[NCOL][NROW]; };
2. 式評価器(再帰下降パーサ)
struct parser { const char *s; int pos; struct grid *g; }; void skipws(struct parser *p) { for (; isspace(*p->s); p->s++); } /* A1, AA12 などセル参照を解析 */ int ref(const char *s, int *col, int *row) { ... } /* 数値を解析 */ float number(struct parser *p) { ... } /* 参照先セルの値を返す */ static float cellval(struct parser *p) { ... } /* 関数呼び出し:@SUM(A1...B5)、@ABS(-A1)… */ float func(struct parser *p) { ... } /* プライマリ式(数値・参照・関数・括弧) */ float primary(struct parser *p) { skipws(p); if (!*p->s) return NAN; if (*p->s == '+') { p->s++; return primary(p); } if (*p->s == '-') { p->s++; return -primary(p); } if (*p->s == '@') { p->s++; return func(p); } if (*p->s == '(') { p->s++; float v = expr(p); skipws(p); if (*p->s != ')') return NAN; p->s++; return v; } if (isdigit(*p->s) || *p->s=='.') return number(p); return cellval(p); } /* 乗除:factor [*|/ factor]* */ float term(struct parser *p) { float l = primary(p); for (;;) { skipws(p); char op=*p->s; if (op!='*' && op!='/') break; p->s++; float r=primary(p); if (op=='*') l*=r; else if (r==0) return NAN; else l/=r; } return l; } /* 加減:term [+|- term]* */ float expr(struct parser *p) { float l = term(p); for (;;) { skipws(p); char op=*p->s; if (op!='+' && op!='-') break; p->s++; float r=term(p); l = (op=='+') ? l+r : l-r; } return l; }
NAN は浮動小数点演算で自然にエラーを伝搬させる。
3. 再計算
VisiCalc は「シート全体を再評価」する単純戦略を採用していた:
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; } }
4. セル設定
struct cell *cell(struct grid *g, int c, int r) { return &g->cells[c][r]; } 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=='+'||*input=='-'||*input=='('||*input=='@') cl->type=FORMULA; else if (isdigit(*input)||*input=='.') { char *end; double v=strtod(input,&end); cl->type = (*end=='\0')?NUM:LABEL; if (cl->type==NUM) cl->val=v; } else cl->type=LABEL; recalc(g); }
5. サンプル利用
struct grid g={0}; 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);
6. テキストユーザーインタフェース(curses)
#define CW 9 // 列表示幅 #define GW 4 // 行余白幅 int vcols(void) { return (COLS-GW)/CW; } int vrows(void) { return LINES-4; } struct grid { struct cell cells[NCOL][NROW]; int cc,cr; // カーソル int vc,vr; // ビューポート左上 };
ビューポートスクロール
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;
描画
static void draw(struct grid *g, int mode, const char *buf) { erase(); /* ステータスバー */ attron(A_BOLD|A_REVERSE); mvprintw(0,0," %c%d", 'A'+g->cc, g->cr+1); if (cur->type==FORMULA) printw(" %s = %.10g", cur->text, cur->val); mvprintw(0,COLS-6, mode==ENTRY?"ENTRY":"READY"); attroff(A_BOLD|A_REVERSE); /* 編集行 */ if (mode) mvprintw(1,0,"> %s_",buf); else if (cur->type!=EMPTY) mvprintw(1,0," %s", cur->text); /* 列ヘッダー */ attron(A_BOLD|A_REVERSE); for (int c=0; c<vcols(); ++c) mvprintw(2,GW+c*CW,"%*c",CW,'A'+g->vc+c); attroff(A_BOLD|A_REVERSE); /* グリッドセル */ for (int r=0; r<vrows() && g->vr+r<NROW; ++r) { int row=g->vr+r, y=3+r; 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); /* cl->val を文字列 fb にフォーマット … */ if (col==g->cc && row==g->cr) attron(A_REVERSE); mvprintw(y,GW+c*CW,"%s",fb); if (col==g->cc && row==g->cr) attroff(A_REVERSE); } } }
7. メインループ & モード
VisiCalc はモーダルインタフェースを採用していた:
| モード | トリガー |
|---|---|
| 通常の移動 |
| コマンドモード(例:, , ) |
| 移動モード(アドレス入力) |
| それ以外 | セル編集 |
for (;;) { int ch = getch(); if (mode==ENTRY && ch==10) { /* Enter */ } else if (mode==READY && ch=='/') { /* コマンド */ } else if (mode==READY && ch=='>') { /* goto */ } else { mode=ENTRY; buf[0]=ch; len=1; } }
setcell() が入力を式・数値・ラベルのどれに分類するか判断。
8. 未実装項目
- ファイル I/O
コマンド(コピー)/R- より大きなグリッド / 列幅制御
- 追加関数/演算子
- 複雑な範囲操作(移動、コピー)
結論
500 行未満の C で以下を実装できる:
- セル:タイプ・値・生テキスト
- 式:再帰下降パーサでセル参照と基本関数を扱う
- 再計算:全シート再評価ループ
- TUI:curses で移動・編集・表示
VisiCalc の核となるアイデア(セル、式、再計算、グリッド)は今日も変わらず有効です。自分だけの実装を作るか、GitHub のより完成度の高い再実装を探してみてください: https://github.com/zserge/kalk