;; spread.el -- A very simple spreadsheet mode for GNU emacs ;; by Benjamin C. Pierce ;; ;; LCD Archive Entry: ;; spread|Benjamin C. Pierce|bcp@dcs.ed.ac.uk| ;; A simple spreadsheet for Emacs | ;; Dec-1993|Version 1.5|~/modes/spread.el.Z| ;; ;; Installation instructions: ;; - Place spread.el in a directory on your emacs load path ;; - Add the following lines to your .emacs file: ;; (setq auto-mode-alist (cons '("\\.sp$" . spread-mode) ;; auto-mode-alist)) ;; (autoload 'spread-mode "spread") ;; ;; Complete documentation appears in the header of the spread-mode ;; function, at the top of this file. ;; ;; This package is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 1, or (at your option) ;; any later version. ;; Spread-mode is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to ;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. (defun spread-mode () "Major mode for simple spreadsheets. Quick reference: recalculate \\[spread-recalc] toggle overwrite-mode \\[overwrite-mode] align next => with last \\[spread-format-next-cell] jump to => on this line (or add one) \\[spread-jump-to-formula] OVERVIEW -------- A spreadsheet is an ordinary text buffer with embedded \"cells\" of the form VALUE <= FORMULA or VALUE <= FORMULA => NAME where * VALUE, the current value of the cell, is a single word (typically a number); * FORMULA is an arbitrary lisp expression, used to recalculate VALUE; and * NAME, if present, is a lisp variable to which VALUE is assigned after each recomputation. A single recalculation step consists of scanning the buffer, recalculating each cell by replacing the current VALUE by the result of evaluating FORMULA. A complete recalculation, triggered by typing \\[spread-recalc], iterates this process until the buffer stops changing. When a old value is replaced, the first character of the newly computed value is placed in the same column as the first character of the old. If the values are numeric, the new value is truncated to the same number of decimal places as the old. The spacing of the remainder of the line is preserved. FORMULAS -------- The formula associated with a cell may be just a constant. This form is useful for making names for common constants; e.g.: 10 <= 10 => length More generally, a formula may involve arbitrary arithmetic calculations including variables whose values are set by other cells: 555 <= 555 => breadth 5550 <= (* length breadth) => area $29137 <= (* area 5.25) => total-cost One very useful lisp function is predefined for use in formulas: the expression (total) returns the sum of the column of numbers appearing above the value part of the current cell. (More precisely, it moves the cursor upwards, beginning at the leftmost character of the current value, till it finds a number; then it continues upward until it fails to find a number beginning in this column. The result is the sum of all the numbers in between.) For example: $25 $29137 <= total-cost $55 $888 $-20 ======== total cost: $30085 <= (total) Note that: 1) All the numbers in the list must be left-justified for (total) to work properly. 2) The dollar signs can be contiguous with the numbers, since they are not considered part of the values. 3) The line above the total is composed of equal-signs, not dashes, since these would be misinterpreted as negation-signs. In Emacs version 19 (both FSF and Lucid), floating-point numbers may also be used in formulas. If the value part of a formula is written with a decimal point, new values will be truncated to the same length when it is updated. Another useful function is (date-and-time), which returns the current date and time compressed into a single word: Nov-18-1993-19:18 <= (date-and-time) EDITING ------- Spread-mode provides some simple support for editing spreadsheets: * \\[overwrite-mode] toggles overwrite mode (q.v.) * \\[spread-format-next-cell] formats the next cell so that the => appears exactly under the => of the previous cell * \\[spread-jump-to-formula] moves the cursor to the formula of the cell on the current line, or (if the current line does not contain =>) jumps to the same column as the previous cell's => and adds a =>. Besides the keybindings described above, the bindings of spread-mode are just like those of text-mode. CUSTOMIZATION ------------- Invoking spread-mode calls the value of text-mode-hook and then of spread-mode-hook, if they are non-nil." (interactive) (kill-all-local-variables) (setq truncate-lines t) (use-local-map spread-mode-map) (setq mode-name "Spread") (setq major-mode 'spread-mode) (define-abbrev-table 'text-mode-abbrev-table ()) (setq local-abbrev-table text-mode-abbrev-table) (set-syntax-table text-mode-syntax-table) (modify-syntax-entry ?- "w") (modify-syntax-entry ?. "w") (modify-syntax-entry ?: "w") (modify-syntax-entry ?$ "_") (run-hooks 'text-mode-hook 'spread-mode-hook) (auto-fill-mode nil) (spread-initialize-variables) ) ;; -------------------------------------------------------------------------- ;; Set-up and keybindings (defvar spread-running-18 (string-match "^18" emacs-version)) (defvar spread-running-FSF19 (and (string-match "^19" emacs-version) (not (string-match "Lucid" (emacs-version))))) (defvar spread-mode-map nil "") (if spread-mode-map nil (setq spread-mode-map (copy-keymap text-mode-map)) (define-key spread-mode-map "\C-c\C-c" 'spread-recalc) (define-key spread-mode-map "\C-c\C-o" 'overwrite-mode) (define-key spread-mode-map "\C-c1" 'spread-recalc-once) (define-key spread-mode-map "\C-c\C-v" 'spread-initialize-variables) (define-key spread-mode-map "\C-c\C-i" 'spread-format-next-cell) (define-key spread-mode-map "\C-c\C-x" 'spread-jump-to-formula) ) (defun spread-initialize-variables () (interactive) (let (val name pos) (spread-debug "Initializing variables") (save-excursion (goto-char (point-min)) (while (search-forward "=>" (point-max) t) (setq pos (point)) (search-backward "<=") (re-search-backward "[-0-9.]") (setq val (spread-number-under-cursor)) (goto-char pos) (re-search-forward "\\w+") (setq name (intern (buffer-substring (match-beginning 0) (match-end 0)))) (spread-debug "%s := %s" name val) (make-variable-buffer-local name) (set name val))))) ;; -------------------------------------------------------------------------- ;; Recalculation (defvar spread-recalc-limit 40 "*Maximum iterations of spreadsheet recalculation") (defun spread-recalc () "Recalculate all computed cells in buffer, iterating until all cells' values have stabilized or for SPREAD-RECALC-LIMIT iterations, whichever comes first." (interactive) (message "Recalculating... ") (let ((limit 0)) (while (save-excursion (spread-recalc-once)) (message "Recalculating... (%s)" limit) (sit-for 0) (setq limit (+ limit 1)) (if (= limit spread-recalc-limit) (spread-error "Recalculation looping!")))) (message "Recalculating... done")) (defun spread-get-next-cell (cont) (if (search-forward "<=" (point-max) t) (let (after eol res start end contents var formula formula-start) (setq formula-start (point)) (forward-word -1) (setq start (point)) (forward-word 1) (setq end (point)) (setq contents (buffer-substring start end)) (end-of-line) (setq eol (point)) (goto-char formula-start) (setq res (read-from-string (buffer-substring formula-start eol))) (setq formula (car res)) (forward-char (cdr res)) (setq after (point)) (if (looking-at "[ \t]*=>[ \t]*\\(\\w+\\)") (let ((b (match-beginning 1)) (e (match-end 1))) (setq after e) (setq var (intern (buffer-substring b e))))) (if spread-running-FSF19 (spread-fontify-cell start end after)) (funcall cont start end contents var formula after) t) nil)) (defun spread-recalc-once () "Recalculate all computed cells in buffer. Return T if any of them change their values, NIL otherwise." (interactive) (spread-debug "Recalculating once") (let ((any-changes nil) cell) (goto-char (point-min)) (while (spread-get-next-cell '(lambda (cell-start cell-end contents var formula formula-end) (goto-char cell-start) (setq new (spread-eval formula)) (setq new-string (spread-format-like contents new)) (setq new-length (length new-string)) (spread-debug "%s <--- %s from %s" contents new-string formula) (if (not (string= new-string contents)) (progn (setq any-changes t) (goto-char cell-start) (if (>= new-length (length contents)) (progn (delete-region cell-start (+ cell-start new-length)) (insert new-string)) (progn (delete-region cell-start cell-end) (insert new-string (make-string (- (length contents) new-length) 32))) ))) (if var (progn (spread-debug "%s := %s" var new) (set var new))) (goto-char formula-end) ))) any-changes)) (defun spread-eval (exp) (condition-case err (eval exp) (void-variable (let ((r 88888)) (spread-warning (format "Variable \"%s\" is unbound; using value %d" (car (cdr err)) r)) r)) (error (let ((r 99999)) (spread-warning (format "Evaluation failed with \"%s\"; using value %d" err r)) r)))) ;; ---------------------------------------------------------------------- ;; Formatting and editing (defun spread-format-next-cell () (interactive) (search-forward "<=") (let ((after (point)) (prev-indent (spread-last-formula-column))) (forward-char -2) (re-search-backward "[^ \t]") (forward-char 1) (delete-region (point) (- after 2)) (indent-to prev-indent) (forward-char 2) )) (defun spread-last-formula-column () (let ((here (point)) there) (beginning-of-line) (if (not (search-backward "<=" (point-min) t)) (error "No previous cell to imitate!")) (setq there (current-column)) (goto-char here) there)) (defun spread-jump-to-formula () (interactive) (let ((eol (progn (end-of-line) (point)))) (beginning-of-line) (if (search-forward "<=" eol t) (re-search-forward "\\b") (progn (goto-char eol) (insert " <= ") (forward-char -4) (spread-format-next-cell) (end-of-line))))) ;; ---------------------------------------------------------------------- ;; Some useful recalculation functions (defun total () "Return the total of the list of numbers above this position, not including the number under the cursor." (let ((sum 0)) (while (spread-find-number-above) (setq sum (+ sum (spread-number-under-cursor)))) sum)) (defun date-and-time () "Returns the current date and time as a string, stripping the seconds and substituting dashes for blanks" (interactive) (concat (substring (current-time-string) 4 7) "-" (if (string= (substring (current-time-string) 8 9) " ") "" (substring (current-time-string) 8 9)) (substring (current-time-string) 9 10) "-" (substring (current-time-string) 20) "-" (substring (current-time-string) 11 16) )) ;; ---------------------------------------------------------------------- ;; Utility functions (defun spread-find-number-above () "Moves directly up until either a number or a blank line is found. Returns T for a number, NIL for a blank line." (interactive) (let ((done nil) (result t)) (while (not done) (let ((here (point))) (previous-line 1) (cond ((= (point) here) (setq result nil done t)) ((looking-at "[ \t\n]") (setq result nil done t)) ((looking-at "[-0-9.]") (setq done t))))) result)) (defun spread-number-under-cursor () (interactive) (save-excursion (goto-char (+ (point) 1)) (re-search-backward "\\(^\\|[^-0-9.]\\)\\([-0-9.]\\)") (let ((begin (match-beginning 2))) (goto-char begin) (re-search-forward "[^-0-9.]") (string-to-int ;; (in V19, this actually returns a float if necessary!) (buffer-substring begin (match-beginning 0)))))) (defun spread-format-like (old new) (if spread-running-18 ;; If we're running emacs 18, then floating point numbers ;; do not make sense anyway, so just format it as an integer (int-to-string new) (progn (let ((old-decimal (string-match "\\." old))) (setq new (cond ((stringp new) new) ((numberp new) (if old-decimal (let ((oldprecision (- (length old) 1 old-decimal))) (spread-float-to-string-with-precision new oldprecision)) (int-to-string (truncate new)))) (t (prin1-to-string new)))) (if (spread-contains-char new 32) (setq new (concat "\"" new "\""))) new )))) (defun spread-float-to-string-with-precision (n p) (let ((float-output-format (concat "%." (int-to-string p) "f"))) (format "%s" (float n)))) (defun spread-contains-char (s c) (let ((len (length s)) (i 0) (found nil)) (while (and (not found) (< i len)) (if (char-equal (elt s i) c) (setq found t) (setq i (+ i 1)))) found)) ;; ---------------------------------------------------------------------- ;; Font support for FSF19 ; (if (string-match "^18" emacs-version) ; (error "Sorry: spread.el requires Emacs version 19 (FSF or Lucid)")) (defun spread-fontify-cell (val-start val-end cell-end) (add-text-properties val-start val-end '(face bold)) (add-text-properties (+ 1 val-end) cell-end '(face italic)) ) ;; ---------------------------------------------------------------------- ;; Debugging and error reporting (defvar spread-debugging nil "*Debugging for spreadsheet recalculations") (defun spread-debug (&rest args) (if spread-debugging (progn (save-window-excursion (switch-to-buffer "*Spread*") (goto-char (point-max)) (insert (apply 'format args)) (insert "\n") )))) (defun spread-error (m) (error "Spreadsheet error: %s" m)) (defun spread-warning (m) (message "Warning: %s" m) (beep) (sit-for 2))