/****************************************************************************** * Copyright 1994-2002,2007 by Thomas E. Dickey * * All Rights Reserved. * * * * Permission to use, copy, modify, and distribute this software and its * * documentation for any purpose and without fee is hereby granted, provided * * that the above copyright notice appear in all copies and that both that * * copyright notice and this permission notice appear in supporting * * documentation, and that the name of the above listed copyright holder(s) * * not be used in advertising or publicity pertaining to distribution of the * * software without specific, written prior permission. * * * * THE ABOVE LISTED COPYRIGHT HOLDER(S) DISCLAIM ALL WARRANTIES WITH REGARD * * TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND * * FITNESS, IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE * * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR * * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * ******************************************************************************/ static const char Id[] = "$Id: add.c,v 1.40 2007/02/15 00:47:32 tom Exp $"; static const char copyrite[] = "Copyright 1994-2002,2007 by Thomas E. Dickey"; /* * Title: add.c * Author: T.E.Dickey * Created: 05 May 1986 * Modified: (see CHANGES) * * Function: This is a simple adding machine that uses curses to display * a column of values, operators and results. The user can * move up and down in the column, modifying the values and * operators. */ #include #include #define SALLOC(s) (s *)malloc(sizeof(s)) #define SSTACK struct SStack SSTACK { SSTACK *next; FILE *sfp; char **sscripts; }; static void Recompute(DATA *); static void ShowRange(DATA *, DATA *); static void ShowFrom(DATA *); /* * Common data */ static char *top_output; static DATA *all_data; /* => beginning of data */ static DATA *top_data; /* => beginning of current screen */ static Value val_frac; /* # of units in 'len_frac' (e.g., 100.0) */ static long big_long; /* largest signed 'long' value */ static int interval; /* compounding interval-divisor */ static int val_width; /* maximum width of formatted number */ static int len_frac; /* nominal number of digits after period */ static Bool show_error; /* suppress normal reporting until GetC() */ static Bool show_scripts; /* force script to be visible, for testing */ /* * Input-script control: */ static SSTACK *sstack; /* script-stack */ static FILE *scriptFP; /* current script file-pointer */ static char **scriptv; /* pointer to list of input-scripts */ static Bool scriptCHG; /* set true if there's a change after scripts */ static Bool scriptNUM; /* set true to 0..n for script number */ /* * Help-screen */ static DATA *all_help; static char *helpfile; #define CMD(op,l,u,f,s) {op, l, u, f, s} /* * Check to see if the given character is a legal 'add' operator (excluding * editing/scrolling): */ /* *INDENT-OFF* */ static struct { char command; char repeats; char toggles; Bool isunary; char *explain; } Commands[] = { CMD(OP_ADD, 'a', 'A', TRUE, "add"), CMD(OP_SUB, 's', 'S', TRUE, "subtract"), CMD(OP_NEG, 'n', 'N', FALSE, "negate"), CMD(OP_MUL, 'm', 'M', FALSE, "multiply"), CMD(OP_DIV, 'd', 'D', FALSE, "divide"), CMD(OP_INT, 'i', 'I', FALSE, "interest"), CMD(OP_TAX, 't', 'T', FALSE, "tax"), CMD(L_PAREN, EOS, EOS, TRUE, "begin group"), CMD(R_PAREN, EOS, EOS, FALSE, "end group") }; /* *INDENT-ON* */ /* * Normally we don't show the results of replaying a script. This makes * loading scripts much faster. */ static int isVisible(void) { return show_scripts || (scriptFP == 0); } /* * Lookup a character to see if it is a legal operator, returning nonnull * in that case. */ #define LOOKUP(func,lookup,result) \ static int func(int c) { \ unsigned j; \ for (j = 0; j < SIZEOF(Commands); j++) \ if (Commands[j].lookup == c) \ return result; \ return 0; \ } LOOKUP(isCommand, command, c) LOOKUP(isRepeats, repeats, Commands[j].command) LOOKUP(isToggles, toggles, Commands[j].command) LOOKUP(isUnary, command, Commands[j].isunary) /* * Find the end of the DATA list */ static DATA *EndOfData(void) { register DATA *np; if ((np = all_data) != 0) { while (!LastData(np)) np = np->next; } return np; } /* * Returns true if the DATA entry is either the 0th or 1st entry in the * list. */ static int FirstData(DATA * np) { return (np->prev == 0 || np->prev == all_data); } /* * Tests for special case of operators that cannot appear in a unary context. */ static int UnaryConflict(DATA * np, int chr) { if (isCommand(chr)) return (!isUnary(chr) && (FirstData(np) || np->prev->psh)); return FALSE; } /* * Trim whitespace from the end of a string */ static void TrimString(char *src) { register char *end = src + strlen(src); while (end-- != src) { if (isspace(UCH(*end))) *end = EOS; else break; } } /* * Allocate a string, trimming whitespace from the end for consistency. */ static char * AllocString(char *src) { return strcpy(malloc((unsigned) (strlen(src) + 1)), src); } /* * Allocate and initialize a DATA entry */ static DATA * AllocData(DATA * after) { register DATA *np = SALLOC(DATA); np->txt = 0; np->val = np->sum = np->aux = 0.0; np->cmd = EOS; np->psh = FALSE; np->dot = len_frac; np->next = np->prev = 0; if (after != 0) { np->prev = after; np->next = after->next; after->next = np; if (!LastData(np)) np->next->prev = np; } else { /* append to the end of the list */ register DATA *op = EndOfData(); if (op != 0) { op->next = np; np->prev = op; } else { all_data = np; } } return np; } /* * Free and delink the data from the list. If it was a permanent entry, * recompute the display from that point. Otherwise, simply repaint. */ static DATA * FreeData(DATA * np, int permanent) { register DATA *prev = np->prev; register DATA *next = np->next; if (prev == 0) { /* we're at the beginning of the list */ all_data = next; if (next != 0) next->prev = 0; } else { prev->next = next; if (next != 0) { next->prev = prev; } else { /* deleted the end-of-data */ next = prev; } } if (np->txt != 0) free(np->txt); free((char *) np); if (top_data == np) top_data = next; if (screen_active) { if (permanent) { Recompute(next); } else { ShowFrom(next); } } return next; } /* * Count the data from the beginning to the specified entry. */ static int CountData(DATA * np) { register int seq = 0; while (np->prev != 0) { seq++; np = np->prev; } return seq; } static int CountFromTop(DATA * np) { return CountData(np) - CountData(top_data); } static int CountAllData(void) { return CountData(EndOfData()); } static DATA * FindData(int seq) { register DATA *np = all_data; while (seq-- > 0) { if (np == 0) break; np = np->next; } return np; } /* * Remove any fractional portion of the given value 'val', return the result. */ static Value Floor(Value val) { if (val > big_long) { val = big_long; } else if (val < -big_long) { val = -big_long; } else { long tmp = val; val = tmp; } return (val); } static Value Ceiling(Value val) { Value tmp = Floor(val); if (val > 0.0 && val > tmp) { tmp += 1.0; } else if (val < 0.0 && val < tmp) { tmp -= 1.0; } return (tmp); } /* * Return the last operand for the given operator, excluding the given data. */ static Value LastVAL(DATA * np, int cmd) { np = np->prev; while (np != 0) { if (cmd == np->cmd) return (np->val); np = np->prev; } if (cmd == OP_MUL || cmd == OP_DIV) return (1.0 * val_frac); else if (cmd == OP_INT || cmd == OP_TAX) return (4.0 * val_frac); return (0.0); } /* * Convert the given value 'val' to printing format (comma between groups of * three digits, decimal point before fractional part). */ static char * Format(char *dst, Value val) { int len, j, neg = val < 0.0; size_t grp; char bfr[MAXBFR], *s = dst; if (neg) { val = -val; *s++ = OP_SUB; } if (val >= big_long) { (void) strcpy(s, " ** overflow"); } else { (void) sprintf(bfr, "%0*.0f", len_frac, val); len = strlen(bfr) - len_frac; grp = len % 3; j = 0; while (j < len) { if (grp) { (void) strncpy(s, &bfr[j], grp); j += grp; s += grp; if (j < len) *s++ = COMMA; } grp = 3; } (void) sprintf(s, ".%s", &bfr[len]); } return (dst); } /* * Convert a value to printing form, writing it on the screen: */ static void putval(Value val) { char bfr[MAXBFR]; screen_printf("%*.*s", val_width, val_width, Format(bfr, val)); } /* */ static void setval(DATA * np, int cmd, Value val, int psh) { np->cmd = isCommand(cmd) ? cmd : OP_ADD; np->val = val; np->psh = psh; } /* * Compute the parenthesis level of the given data entry. This is a positive * number. */ static int LevelOf(DATA * target) { register DATA *np; register int level = 0; for (np = all_data; np != target; np = np->next) { if (np->cmd == R_PAREN) level--; if (np->psh) level++; } return level; } /* * Return a pointer to the last data entry in the screen. */ static DATA * ScreenBottom(void) { register DATA *np = top_data; register int count = screen_full; while (--count > 0) { if (!LastData(np)) np = np->next; else break; } return np; } /* * For an entry that isn't the currently-edited one, show the operator, * operand and result(s). Return the index of this entry in the screen * to use in testing for completion of repainting activity. */ static int ShowValue(DATA * np, int *editing, Bool comment) { int row = CountFromTop(np); int col = 0; int level; if (!isVisible()) { if (editing != 0) { editing[0] = editing[1] = 0; } } else if (row >= 0 && row < screen_full) { char cmd = isprint(UCH(np->cmd)) ? np->cmd : '?'; screen_set_position(row + 1, col); screen_clear_endline(); if (np->cmd != EOS) { for (level = LevelOf(np); level > 0; level--) screen_puts(". "); if (editing != 0) { screen_set_reverse(TRUE); screen_printf(" %c>>", cmd); screen_set_reverse(FALSE); } else { screen_printf(" %c: ", cmd); } if (editing != 0) { *editing = screen_col(); screen_set_reverse(TRUE); } if ((cmd == R_PAREN) || ((editing != 0) && !comment)) screen_printf("%*.*s", val_width, val_width, " "); else if (np->psh) screen_putc(L_PAREN); else putval(np->val); if (editing != 0) screen_set_reverse(FALSE); if (!np->psh) { screen_puts(" "); if (editing != 0) screen_set_bold(TRUE); putval(np->sum); if (editing != 0) screen_set_bold(FALSE); } if (cmd == OP_INT || cmd == OP_TAX) { screen_puts(" "); putval(np->aux); } col = screen_col(); if (editing != 0) editing[1] = col; col += 3; if ((np->txt != 0) && screen_cols_left(col) > 3) screen_puts(" # "); } if ((np->txt != 0) && screen_cols_left(col) > 0) { screen_printf("%.*s", screen_cols_left(col), np->txt); } if (LastData(np) && screen_rows_left(row) > 1) { screen_set_position(row + 2, 0); screen_clear_bottom(); } } return row; } static void ShowRange(DATA * first, DATA * last) { register DATA *np = first; while (np != last) { if (ShowValue(np, (int *) 0, FALSE) >= screen_full) break; np = np->next; } } static void ShowFrom(DATA * first) { ShowRange(first, (DATA *) 0); } /* * (Re)display the status line at the top of the screen. */ static void ShowStatus(DATA * np, int opened) { int seq = CountData(np); int top = CountData(top_data); DATA *last = EndOfData(); unsigned j; int c; char buffer[BUFSIZ]; if (!show_error && isVisible()) { screen_set_bold(TRUE); screen_set_position(0, 0); screen_clear_endline(); (void) sprintf(buffer, "%d of %d", seq, CountData(last)); screen_set_position(0, screen_cols_left((int) strlen(buffer))); screen_puts(buffer); screen_set_position(0, 0); if (opened < 0) { screen_puts("Edit comment (press return to exit)"); } else if (opened > 0) { screen_puts("Open-line expecting operator "); for (j = 0; j < SIZEOF(Commands); j++) { c = Commands[j].command; if (!isUnary(c) && FirstData(np)) continue; if ((c = Commands[j].command) == L_PAREN) continue; if ((c == R_PAREN) && (opened < 2)) continue; screen_putc(c); } screen_puts(" or oO to cancel"); } else if (np->cmd != EOS) { /* editing a value */ for (j = 0; j < SIZEOF(Commands); j++) { if (Commands[j].command == np->cmd) { screen_printf(" %s", Commands[j].explain); break; } } screen_set_position(0, 5 + val_width); putval(last->sum); screen_puts(" -- total"); } screen_set_bold(FALSE); } if (isVisible()) screen_set_position(seq - top + 1, 0); } /* * Show text in the status line */ static void ShowInfo(char *msg) { if (screen_active) { /* we've started curses */ screen_message("%s", msg); } else { (void) printf("%s\n", msg); } } /* * Show an error-message in the status line */ static void ShowError(char *msg, char *arg) { static const char format[] = "?? %s \"%s\""; if (screen_active) { /* we've started curses */ screen_message(format, msg, arg); show_error = TRUE; } else { (void) fprintf(stderr, format, msg, arg); (void) fprintf(stderr, "\n"); perror("add"); exit(errno); } } /* * Returns true if a file exists, -true if it isn't a file, false if neither. */ static int Fexists(char *path) { struct stat sb; if (*path == EOS) ShowError("No filename specified", path); if (stat(path, &sb) < 0) return FALSE; if ((sb.st_mode & S_IFMT) != S_IFREG) { ShowError("Not a file", path); return -TRUE; } return TRUE; } /* * Check file-access for writing a script */ static int Ok2Write(char *path) { if (Fexists(path) != -TRUE && access(path, 02) != 0 && errno != ENOENT) { ShowError("No write access", path); } else { return TRUE; } return FALSE; } /* * Write the current list of data as an ADD-script */ static void PutScript(char *path) { DATA *np; FILE *fp = (path && *path) ? fopen(path, "w") : 0; char buffer[MAXBFR]; int count = 0; if (fp == 0) { ShowError("Cannot open output", path); return; } (void) sprintf(buffer, "Writing results to \"%s\"", path); ShowInfo(buffer); for (np = all_data->next; np != 0; np = np->next) { if (np->cmd == EOS && np->next == 0) break; (void) fprintf(fp, "%c", np->cmd); if (np->psh) (void) fprintf(fp, "%c", L_PAREN); else if (np->cmd != R_PAREN) (void) fprintf(fp, "%s", Format(buffer, np->val)); if (!np->psh) (void) fprintf(fp, "\t%s", Format(buffer, np->sum)); if (np->cmd == OP_INT || np->cmd == OP_TAX) (void) fprintf(fp, "\t%s", Format(buffer, np->aux)); if (np->txt != 0) (void) fprintf(fp, "\t#%s", np->txt); (void) fprintf(fp, "\n"); count++; } (void) fclose(fp); /* If we've written the specified output, reset the changed-flag */ if (!strcmp(path, top_output)) scriptCHG = FALSE; (void) sprintf(buffer, "Wrote %d line%s to \"%s\"", count, count != 1 ? "s" : "", path); ShowInfo(buffer); show_error++; /* force the message to stay until next char */ } /* * Check file-access for reading a script. */ static int Ok2Read(char *path) { if (Fexists(path) != TRUE || access(path, 04) != 0) ShowError("No read access", path); else return TRUE; return FALSE; } /* * Save the current script-state and nest a new one. */ static void PushScripts(char *script) { SSTACK *p = SALLOC(SSTACK); p->next = sstack; p->sfp = scriptFP; p->sscripts = scriptv; sstack = p; scriptFP = 0; scriptv = (char **) calloc(2, sizeof(char *)); scriptv[0] = AllocString(script); } /* * Restore a previous script-state, if any. */ static int PopScripts(void) { SSTACK *p; scriptFP = 0; scriptv = 0; if ((p = sstack) != 0) { scriptFP = p->sfp; scriptv = p->sscripts; sstack = p->next; } return (scriptFP != 0); } /* * On end-of-file, go to the next script (or resume the parent script) */ static void NextScript(void) { (void) fclose(scriptFP); scriptFP = 0; if (!*(++scriptv)) PopScripts(); } /* * Read from a script, checking for end-of-file, and performing control-char * conversion. */ static int ReadScript(void) { int c = fgetc(scriptFP); if (feof(scriptFP) || ferror(scriptFP)) { NextScript(); if (!scriptNUM++) scriptCHG = FALSE; c = EOF; } else if (c == '^') { c = ReadScript(); if (c == EOF) c = '^'; /* we'll get an EOF on the next call */ else if (c == '?') c = '\177'; /* delete */ else c &= 037; } return c; } /* * As long as there is another input-script to process, read it. Scripts are * formatted * * to permit line-oriented entries. */ static int GetScript(void) { static int first; static int ignored; static int comment; register int c; while (scriptv != 0 && *scriptv != 0) { int was_invisible = !isVisible(); if (scriptFP == 0) { scriptFP = fopen(*scriptv, "r"); if (scriptFP == 0) { ShowError("Cannot read", *scriptv); scriptv++; } else { ShowInfo("Reading script"); first = TRUE; } continue; } while (scriptFP != 0) { if ((c = ReadScript()) == EOF) continue; if (c == '#' || c == COLON) { comment = TRUE; ignored = FALSE; } else if (!comment && (c == '\t')) { ignored = TRUE; } if (isReturn(c)) first = TRUE; if (ignored && isReturn(c)) { ignored = FALSE; } else if (!ignored) { if (isReturn(c)) { comment = FALSE; } else if (first) { if (isdigit(UCH(c))) { ungetc(c, scriptFP); c = OP_ADD; } first = FALSE; } return (c); } } /* Finally, paint the screen if I wasn't doing so before */ if (was_invisible) { int editcols[3]; DATA *last = EndOfData(); ShowStatus(last, FALSE); ShowRange(top_data, last); ShowValue(last, editcols, FALSE); first = TRUE; return EQUALS; /* flush out the last line */ } } if (first) { if (scriptNUM == 1) scriptCHG = FALSE; first = FALSE; } return EOS; } /* * Read a character from an input-script, if it is available. Otherwise, read * directly from the terminal. */ static int GetC(void) { register int c; if ((c = GetScript()) == EOS) { show_error = FALSE; c = screen_getc(); } return (c); } /* * Given a string, offset into it and insert-position, delete the character at * that offset, both from the string and screen. Return the resulting offset. */ static int DeleteChar(char *buffer, int offset, int pos, int limit) { int y, x, col; register char *t; /* delete from the actual buffer */ for (t = buffer + offset; (t[0] = t[1]) != EOS; t++) ; if (isVisible()) { /* update the display */ y = screen_row(); x = screen_col(); /* get current insert-position */ col = x - pos + offset; /* assume pos < len, offset < len */ screen_set_position(y, col); screen_delete_char(); if (limit > 0 && (int) strlen(buffer) < limit) { screen_set_position(y, col - offset); screen_insert_char(' '); x++; } if (pos > offset) x--; screen_set_position(y, x); } if (pos > offset) pos--; return pos; } /* * Insert a character into the given string, returning the updated insert * position. If the "rmargin" parameter is nonzero, we keep the buffer * right-justified to that limit. */ static int InsertChar(char *buffer, int chr, int pos, int lmargin, int rmargin, int *offset) { int y, x; int len = strlen(buffer); register char *t; /* perform the actual insertion into the buffer */ for (t = buffer + len;; t--) { t[1] = t[0]; if (t == buffer + pos) break; } t[0] = chr; if (isVisible()) { /* update the display on the screen */ y = screen_row(); x = screen_col(); if (screen_cols_left(x) > 0) { if (rmargin > 0) { x--; screen_set_position(y, x - pos); screen_delete_char(); screen_set_position(y, x); } screen_insert_char(chr); screen_set_position(y, x + 1); } else if (offset != 0) { screen_set_position(y, lmargin); screen_delete_char(); screen_set_position(y, x - 1); screen_insert_char(chr); screen_set_position(y, x); *offset += 1; } else { screen_alarm(); } } return pos + 1; } /* * Delete from the buffer the character to the left of the given col-position. */ static int doDeleteChar(char *buffer, int col, int limit) { if (col > 0) { col = DeleteChar(buffer, col - 1, col, limit); } else { screen_alarm(); } return col; } /* * Returns the index of the decimal-point in the given buffer (or -1 if not * found). */ static int DecimalPoint(char *buffer) { register char *dot = strchr(buffer, PERIOD); if (dot != 0) return (int) (dot - buffer); return -1; } /* * Return the sequence-pointer of the left-parenthesis enclosing the given * operand-set at 'np'. */ static DATA * Balance(DATA * np, int level) { int target = level; while (np->prev != 0) { if (np->cmd == R_PAREN) level++; else if (np->psh) level--; if (level <= target) break; /* unbalanced */ np = np->prev; } return (level == 0) ? np : 0; } static void ShowScriptName(void) { while (screen_move_left(screen_col() + 1, 0) > 0) { ; } if (*top_output) { screen_printf("script: %s", top_output); } else { screen_puts("no script"); } screen_clear_endline(); (void) screen_getc(); } /* * Edit an arbitrary buffer, starting at the current screen position. */ static void EditBuffer(char *buffer, int length) { int end, chr; int col = strlen(buffer); int done = FALSE; int offset = 0; int lmargin = screen_col(); int shown = FALSE; end = screen_cols_left(lmargin); end = min(end, length); if (end < (int) strlen(buffer)) offset = strlen(buffer) - end; while (!done) { while (col - offset < 0) { offset--; shown = FALSE; } while (col - offset > end) { offset++; shown = FALSE; } if (isVisible() && !shown) { int x; screen_set_position(screen_row(), lmargin); screen_printf("%.*s", end, buffer + offset); if (screen_cols_left(x = screen_col()) > 0) { screen_set_position(screen_row(), x); screen_clear_endline(); } screen_set_position(screen_row(), lmargin + col - offset); shown = TRUE; } chr = GetC(); if (isReturn(chr)) { done = TRUE; } else if (isAscii(chr) && isprint(UCH(chr))) { if ((int) strlen(buffer) < length - 1) col = InsertChar(buffer, chr, col, lmargin, 0, &offset); else screen_alarm(); } else if (is_delete_left(chr)) { col = doDeleteChar(buffer, col, 0); } else if (is_left_char(chr)) { col = screen_move_left(col, 0); } else if (is_right_char(chr)) { col = screen_move_right(col, (int) strlen(buffer)); } else if (is_home_char(chr)) { while (col > 0) col = screen_move_left(col, 0); } else if (is_end_char(chr)) { while (col < (int) strlen(buffer)) col = screen_move_right(col, (int) strlen(buffer)); } else { screen_alarm(); } } } /* * Edit the comment-field */ static void EditComment(DATA * np) { char buffer[BUFSIZ]; int row; int editcols[3]; (void) strcpy(buffer, np->txt != 0 ? np->txt : ""); ShowStatus(np, -1); row = ShowValue(np, editcols, TRUE) + 1; if (isVisible()) { screen_set_position(row, editcols[1]); screen_puts(" # "); } EditBuffer(buffer, sizeof(buffer)); TrimString(buffer); if (*buffer != EOS || (np->txt != 0)) { if (np->txt != 0) { if (!strcmp(buffer, np->txt)) return; /* no change needed */ free(np->txt); } np->txt = (*buffer != EOS) ? AllocString(buffer) : 0; scriptCHG = TRUE; } } /* * Returns true if the given entry has editable data */ static int HasData(DATA * np) { return !LastData(np) || (np->val != 0.0); } /* * Read a new number, until the operator for the next number is encountered. * Inputs: * np = line entry at which to prompt/read data * edit = true iff we re-edit prior contents of this line * Outputs: * *len_ = -2 if right parenthesis found, * = -1 if left parenthesis found, * = 0 if no number found (usually to switch operators) * = +n if value found, i.e., its length. * *val_ = the decoded number. * Returns: * The terminating character (e.g., an operator or command). */ static int EditValue(DATA * np, int *len_, Value * val_, int edit) { int c; int row; int col; /* current insert-position */ int editcols[3]; int done = FALSE; int was_visible = isVisible(); int lmargin = screen_col(); int nesting; /* if we find left parenthesis rather than number */ char buffer[MAXBFR]; /* current input value */ static char old_digit = EOS; /* nonzero iff we have pending digit */ if (np->cmd == R_PAREN) edit = FALSE; ShowStatus(np, FALSE); row = ShowValue(np, editcols, FALSE) + 1; if (isVisible()) { screen_set_position(row, editcols[0]); screen_set_reverse(TRUE); } if (edit) { if (np->psh) { buffer[0] = L_PAREN; buffer[1] = EOS; } else { register int len, dot; register char *s; (void) sprintf(buffer, "%0*.0f", len_frac, np->val); len = strlen(buffer); s = buffer + len; s[1] = EOS; for (c = 0; c < len_frac; c++, s--) s[0] = s[-1]; dot = len - len_frac; len++; buffer[dot] = PERIOD; } if (isVisible()) { screen_set_position(row, (int) (editcols[0] + val_width - strlen(buffer))); screen_puts(buffer); } } else { buffer[0] = EOS; } if (isVisible()) screen_set_position(row, editcols[0] + val_width); col = strlen(buffer); nesting = (*buffer == L_PAREN); c = EOS; while (!done) { if (old_digit) { c = old_digit; old_digit = EOS; } else { c = GetC(); } /* * If the current operator is a right parenthesis, we must have * an operator following, with no data intervening: */ if (np->cmd == R_PAREN) { if (isDigit(c) || (c == PERIOD) || (c == L_PAREN)) { screen_alarm(); } else { *len_ = -2; done = TRUE; } } /* * Move left/right within the buffer to adjust the insertion * position. In curses mode, CTL/F and CTL/B are conflicting. */ else if (is_left_char(c) && !is_up_page(c)) { col = screen_move_left(col, 0); } else if (is_right_char(c) && !is_down_page(c)) { col = screen_move_right(col, (int) strlen(buffer)); } else if (is_home_char(c)) { while (col > 0) col = screen_move_left(col, 0); } else if (is_end_char(c)) { while (col < (int) strlen(buffer)) col = screen_move_right(col, (int) strlen(buffer)); } /* * Backspace deletes the last character entered: */ else if (is_delete_left(c)) { col = doDeleteChar(buffer, col, val_width); if (*buffer == EOS) nesting = FALSE; } /* * A left parenthesis may be used only as the first (and only) * character of the operand. */ else if (c == L_PAREN) { if (*buffer != EOS) { screen_alarm(); } else { col = InsertChar(buffer, c, col, lmargin, val_width, (int *) 0); nesting = TRUE; } } /* * If we have received a left parenthesis, and do not delete * it, the next character begins a new operand-line: */ else if (nesting) { if (UnaryConflict(np, c)) screen_alarm(); else { if (isDigit(c) || c == PERIOD) { old_digit = c; c = OP_ADD; } *len_ = -1; done = TRUE; } } /* * Otherwise, we assume we have a normal value which we are * decoding: */ else if (isDigit(c)) { int limit = val_width; if (strchr(buffer, '.') == 0) limit -= (1 + len_frac); if ((int) strlen(buffer) > limit) screen_alarm(); else col = InsertChar(buffer, c, col, lmargin, val_width, (int *) 0); } /* * Decimal point can be entered once for each number. If we * get another, simply move it to the new position. */ else if (c == PERIOD) { register int dot; if ((dot = DecimalPoint(buffer)) >= 0) col = DeleteChar(buffer, dot, col, val_width); col = InsertChar(buffer, c, col, lmargin, val_width, (int *) 0); } /* * Otherwise, we assume a new operator-character for the * next command, flushing out the current command. Decode * the number (if any) which we have read: */ else if (c != COMMA) { if (*buffer != EOS) { register int len = strlen(buffer); register int dot; Value cents; if ((dot = DecimalPoint(buffer)) < 0) buffer[dot = len++] = PERIOD; while ((len - dot) <= len_frac) buffer[len++] = '0'; len = dot + 1 + len_frac; /* truncate */ buffer[len] = EOS; (void) sscanf(&buffer[dot + 1], "%lf", ¢s); if (dot) { buffer[dot] = EOS; (void) sscanf(buffer, "%lf", val_); buffer[dot] = PERIOD; *val_ *= val_frac; } else *val_ = 0.0; *val_ += cents; } else { *val_ = np->val; } *len_ = (*buffer == L_PAREN) ? 0 : strlen(buffer); done = TRUE; } } if (was_visible) screen_set_reverse(FALSE); return (c); } /* * Return true if (given the length returned by 'EditValue()', and the * state of the data-list) we don't display a value. */ static int NoValue(int len) { return ((len == 0 || len == -2) && (all_data->next->next != 0)); } /* * Compute one stage of the running total. To support parentheses, we use two * arguments: 'old' is the operand which contains the left parenthesis. */ static int Calculate(DATA * np, DATA * old) { Bool same; Value before = np->sum; np->sum = (old->prev) ? old->prev->sum : 0.0; switch (old->cmd) { default: case OP_ADD: np->sum += np->val; break; case OP_SUB: np->sum -= np->val; break; case OP_NEG: np->sum = -np->sum; break; case OP_MUL: np->sum *= (np->val / val_frac); np->sum = Floor(np->sum); break; case OP_DIV: if (np->val == 0.0) { if (np->sum > 0.0) np->sum = big_long; else if (np->sum < 0.0) np->sum = -big_long; } else { np->sum /= (np->val / val_frac); np->sum = Floor(np->sum); } break; case OP_INT: np->aux = Ceiling(np->sum * np->val / (interval * 100. * val_frac)); np->sum += np->aux; break; case OP_TAX: np->aux = Ceiling(np->sum * np->val / (100. * val_frac)); np->sum += np->aux; break; } same = (before == np->sum) && (before < big_long) && (before > -big_long); if (isVisible() && !same) scriptCHG = TRUE; return (same); } /* * Given a pointer 'np' into the operand list, and (possibly) new 'cmd' and * 'val' components, propagate the computation to the end of the vector, * showing the result on the screen. */ static void Recompute(DATA * base) { register DATA *np = base; register DATA *op; int level = LevelOf(np); Bool same; while (np != 0) { if (np->psh) { np->sum = 0.0; level++; } else { if (np->cmd == R_PAREN) { level--; np->val = np->prev->sum; op = Balance(np, level); if (op == 0) { op = all_data; level = 0; } } else { op = np; } same = Calculate(np, op); if ((level == 0) && same) break; } np = np->next; } ShowRange(base, np ? np->next : np); } /* * "Open" a new entry for editing. If 'after' is set, we open the entry * after the current 'base'. This is the normal mode of operation, and is * consistent with 'x'-command. */ static DATA * OpenLine(DATA * base, int after, int *repeated, int *edit) { DATA *save_top = top_data; DATA *op = after ? base : base->prev; DATA *np; int chr; int this_row; int done = FALSE; int nested; np = AllocData(op); nested = LevelOf(np); this_row = CountFromTop(np); /* Adjust 'top_data' if we have to scroll a little */ if (this_row <= 1) { while (this_row++ <= 1) { if (top_data->prev == all_data) break; top_data = top_data->prev; } } else { this_row -= (screen_full - 2); while (this_row-- > 0) top_data = top_data->next; } /* (Re)display the screen with the opened line */ if (top_data == save_top) ShowFrom(np->next); else ShowFrom(top_data); ShowStatus(np, TRUE + nested); screen_clear_endline(); *edit = FALSE; /* assume we'll get some new data */ while (!done) { chr = GetC(); if (UnaryConflict(np, chr)) { screen_alarm(); continue; } switch (chr) { case OP_INT: /* sC: open to interest */ case OP_TAX: /* sC: open to sales tax */ /* patch: provide defaults */ case OP_ADD: /* sC: open to add */ case OP_SUB: /* sC: open to subtract */ case OP_NEG: /* sC: open to negate */ case R_PAREN: /* sC: open closing brace */ setval(np, chr, 0.0, FALSE); done++; break; case L_PAREN: setval(np, OP_ADD, 0.0, TRUE); *edit = TRUE; /* force this to display */ done++; break; case OP_MUL: /* sC: open to multiply */ case OP_DIV: /* sC: open to divide */ setval(np, chr, val_frac, FALSE); done++; break; case 'a': case 's': case 'n': case 'm': case 'd': case 'i': case 't': chr = isRepeats(chr); setval(np, chr, LastVAL(np, chr), FALSE); done++; *repeated = TRUE; break; case 'q': case 'Q': case 'o': case 'O': case 'x': case 'X': case 'u': case 'U': (void) FreeData(np, FALSE); if (top_data != save_top) { top_data = save_top; ShowFrom(top_data); } np = base; done++; *edit = TRUE; /* go back to the original */ break; default: screen_alarm(); } } return (np); } /* * Perform half/full-screen scrolling: */ static DATA * ScrollBy(DATA * np, int amount) { int last_seq = CountAllData(); int this_seq = CountData(np); int next_seq = this_seq; int top = CountData(top_data); if (amount > 0) { if ((top + amount) < last_seq) { top += amount; next_seq = top; } else next_seq = last_seq; } else { if (this_seq > top) next_seq = top; else { next_seq = top + amount; next_seq = max(next_seq, 1); top = next_seq; } } ShowFrom(top_data = FindData(top)); return FindData(next_seq); } /* * Compute a one-line movement of the cursor. The 'amount' argument * compensates for other adjustments to the current data pointer in the calling * functions. */ static DATA * JumpBy(DATA * np, int amount) { int this_seq = CountData(np); int next_seq = this_seq + amount; int last_seq = CountAllData(); Bool end_flag = TRUE; Bool un_moved = FALSE; if (next_seq < 1) { next_seq = 1; } else if (next_seq > last_seq) { next_seq = last_seq; } else { end_flag = FALSE; } if (next_seq != this_seq) { np = FindData(next_seq); } else if (end_flag) { un_moved = TRUE; } /* Figure out if we have to adjust the top_data variable. * If so, we've got to redisplay the screen. */ if (!un_moved) { int top = CountData(top_data); Bool adjust = TRUE; if (next_seq < top) top_data = np; else if (next_seq >= top + screen_full) top_data = FindData(next_seq - screen_full + 1); else adjust = FALSE; if (adjust) ShowFrom(top_data); } return np; } /* * Jump to a specified entry, by number */ static DATA * JumpTo(DATA * np, int seq) { return JumpBy(np, seq - CountData(np)); } /* * Prompt/process a :-command */ static DATA * ColonCommand(DATA * np) { DATA *save_top = top_data; DATA *prior_np = np; char buffer[BUFSIZ]; char *reply; static char *last_write = ""; if (CountFromTop(np) >= screen_full - 1) { top_data = top_data->next; ShowFrom(top_data); } ShowStatus(np, FALSE); /* in case we have multiple prompts */ screen_set_position(screen_full, 0); screen_putc(COLON); screen_putc(' '); *buffer = EOS; EditBuffer(buffer, sizeof(buffer)); TrimString(buffer); reply = buffer; while (isspace(UCH(*reply))) reply++; if (*reply != EOS) { if (isdigit(UCH(*reply))) { char *dst; int seq = (int) strtol(reply, &dst, 0); np = JumpTo(np, seq); } else { char *param = reply + 1; while (isspace(UCH(*param))) param++; switch (*reply) { case '$': /* FALLTHRU */ case '%': np = JumpTo(np, CountAllData()); break; case 'e': np = all_data->next; while (np->next != 0) np = FreeData(np, TRUE); save_top = top_data; setval(np, OP_ADD, 0.0, FALSE); Recompute(np); if (Ok2Read(param)) PushScripts(param); break; case 'f': ShowScriptName(); break; case 'r': if (Ok2Read(param)) PushScripts(param); break; case 'w': if (*param == EOS) param = last_write; if (*param == EOS) param = top_output; if (Ok2Write(param)) { last_write = AllocString(param); PutScript(param); } break; case 'x': show_scripts = TRUE; break; default: screen_alarm(); } } } if (top_data != save_top && prior_np == np) { top_data = save_top; ShowFrom(top_data); } return np; } /* * Do simple screen movement. Note that some movement-commands may be * printing characters, so (if in edit-mode) we may have already intercepted * these as text. */ static int ScreenMovement(DATA ** pp, int chr) { DATA *np = *pp; int ok = TRUE; if (chr == COLON) { np = ColonCommand(np); } else if (chr == 'z') { chr = GetC(); if (isReturn(chr)) { top_data = np; } else { int top = CountData(top_data); int seq = CountData(np); if (chr == '-') { /* use current entry as end */ top = seq - screen_full + 1; } else { top = seq - screen_half + 1; } top = max(top, 1); top_data = FindData(top); } ShowFrom(top_data); #ifdef KEY_HOME } else if (chr == KEY_HOME) { /* C: move to first entry in list */ np = all_data->next; ShowFrom(top_data = np); #endif #ifdef KEY_END } else if (chr == KEY_END) { /* C: move to last entry in list */ int top, seq; np = EndOfData(); seq = CountData(np); top = seq - screen_full + 1; top = max(top, 1); top_data = FindData(top); ShowFrom(top_data); #endif } else if (chr == 'H') { /* C: move to first entry on screen */ np = top_data; } else if (chr == 'L') { /* C: move to last entry on screen */ np = ScreenBottom(); } else if (is_down_char(chr)) { /* C: move down 1 line */ np = JumpBy(np, 1); } else if (is_up_char(chr)) { /* C: move up 1 line */ np = JumpBy(np, -1); } else if (chr == CTL('D')) { /* C: scroll forward 1/2 screen */ np = ScrollBy(np, screen_half); } else if (chr == CTL('U')) { /* C: scroll backward 1/2 screen */ np = ScrollBy(np, -screen_half); } else if (is_down_page(chr)) { /* C: scroll forward one screen */ np = ScrollBy(np, screen_full); } else if (is_up_page(chr)) { /* C: scroll backward one screen */ np = ScrollBy(np, -screen_full); } else { ok = FALSE; } *pp = np; return ok; } /* * Display the help file. * We store the help-text as a special case of the data list to permit use * of the scrolling code. */ static void ShowHelp(void) { FILE *fp; DATA *save_data = all_data; DATA *save_top = top_data; DATA *np; int chr; int end; int done = FALSE; char buffer[BUFSIZ]; if ((all_data = all_help) == 0) { np = AllocData((DATA *) 0); /* header line not shown */ if ((fp = fopen(helpfile, "r")) != 0) { while (fgets(buffer, sizeof(buffer), fp) != 0) { np = AllocData(np); np->txt = AllocString(buffer); } (void) fclose(fp); } else { np = AllocData(np); np->txt = "Could not find help-file. Press 'q' to exit."; } } np = top_data = all_data->next; end = CountAllData(); ShowFrom(all_data); while (!done) { screen_set_position(0, 0); screen_set_bold(TRUE); screen_clear_endline(); (void) sprintf(buffer, "line %d of %d", CountData(np), end); screen_set_position(0, screen_cols_left((int) strlen(buffer))); screen_puts(buffer); screen_set_position(0, 0); screen_printf("ADD %s -- %s -- ", RELEASE, copyrite); screen_set_bold(FALSE); screen_set_position(CountFromTop(np) + 1, 0); chr = GetC(); if (chr == 'q' || chr == 'Q') { done = TRUE; } else if (!ScreenMovement(&np, chr)) { screen_alarm(); } } all_help = all_data; all_data = save_data; top_data = save_top; ShowFrom(top_data); } #ifndef VMS # ifdef unix # define isSlash(c) ((c) == '/') # else # define isSlash(c) ((c) == '/' || (c) == '\\') # endif static int AbsolutePath(char *path) { #if !defined(unix) && !defined(vms) /* assume MSDOS */ if (isalpha(UCH(*path)) && path[1] == ':') path += 2; #endif return isSlash(*path) || ((*path++ == '.') && (isSlash(*path) || (*path++ == '.' && isSlash(*path)))); } static char * PathLeaf(char *path) { register int n; for (n = strlen(path); n > 0; n--) if (isSlash(path[n - 1])) break; return path + n; } #endif /* * Find the help-file. On UNIX and MSDOS, we look for the file in the same * directory as that in which this program is stored, located by searching the * PATH environment variable. */ static void FindHelp(char *program) { #ifdef ADD_HELPFILE helpfile = ADD_HELPFILE; #else char temp[BUFSIZ]; register char *s = strcpy(temp, program); # if SYS_VMS for (s += strlen(temp); s != temp; s--) if (s[-1] == ']' || s[-1] == ':') break; # else /* assume UNIX or MSDOS */ if (AbsolutePath(temp)) { s = PathLeaf(temp); } else { char *path = getenv("PATH"); int j = 0, k, l; if (path == 0) path = ""; while (path[j] != EOS) { for (k = j; path[k] != EOS && path[k] != PATHSEP; k++) temp[k - j] = path[k]; if ((l = k - j) != 0) temp[l++] = '/'; s = strcpy(temp + l, program); if (access(temp, 5) == 0) { temp[l] = EOS; break; } j = (path[k] != EOS) ? k + 1 : k; } if (path[j] == EOS) { s = temp; *s++ = '.'; *s++ = '/'; s = PathLeaf(strcpy(s, program)); } } # endif /* VMS/UNIX/MSDOS */ (void) strcpy(s, "add.hlp"); helpfile = AllocString(temp); #endif /* ADD_HELPFILE */ } /* * Main program loop: given 'old' operator (applies to current entry), read the * value 'val', delimited by the next operator 'chr'. */ static int Loop(void) { DATA *np = EndOfData(); Value val; int test_c; int chr; int len; int opened; int repeated; /* if true, 'Loop' assumes editable value */ int edit = FALSE; for (;;) { chr = EditValue(np, &len, &val, edit); switch (chr) { case '\t': chr = EQUALS; break; case CTL('P'): chr = 'k'; break; case CTL('N'): case '\r': case '\n': chr = 'j'; break; case ' ': chr = DefaultOp(np); break; } if (chr == CTL('G')) { ShowScriptName(); } else if (chr == 'X') { /* C: delete current entry, move up */ if (NoValue(len)) { np = FreeData(np, TRUE); np = JumpBy(np, -1); edit = HasData(np); } else { edit = FALSE; } } else if (chr == 'x') { /* C: delete current entry, move down */ if (NoValue(len)) { np = FreeData(np, TRUE); edit = HasData(np); } else { edit = FALSE; } } else if (chr == 'u') { /* C: undo last 'x' command */ if (HasData(np)) edit = TRUE; else screen_alarm(); } else if ((test_c = isToggles(chr)) != EOS) { /* C: toggle current operator */ chr = test_c; if (UnaryConflict(np, chr)) { screen_alarm(); } else { if (len == 0) val = np->val; else edit = TRUE; setval(np, chr, val, np->psh); } } else { if (len != 0) { setval(np, np->cmd, val, (len == -1)); if (LastData(np) && isCommand(chr)) { (void) AllocData(np); np->next->cmd = chr; } else if (LastData(np) && isRepeats(chr)) { (void) AllocData(np); np->next->cmd = isRepeats(chr); } Recompute(np); } else { (void) ShowValue(np, (int *) 0, FALSE); } repeated = FALSE; opened = FALSE; if (chr == '?') { /* C: display help file */ ShowHelp(); } else if (chr == '#') { /* C: edit comment */ EditComment(np); } else if (chr == 'Q') { /* C: quit w/o changes */ return (FALSE); } else if (chr == 'q') { /* C: quit */ return (TRUE); } else if (ScreenMovement(&np, chr)) { ; } else if (chr == 'O' /* C: open before */ || chr == 'o') { /* C: open after */ opened = TRUE; np = OpenLine(np, (chr == 'o'), &repeated, &edit); if (repeated) chr = np->cmd; } else { if ((test_c = isRepeats(chr)) != EOS) { chr = test_c; repeated = TRUE; } if (isCommand(chr)) { np = JumpBy(np, 1); np->cmd = chr; } else if (chr == EQUALS) { Recompute(np); } else { screen_alarm(); } } if (repeated) { edit = TRUE; setval(np, chr, LastVAL(np, chr), FALSE); } else if (!opened) { edit = HasData(np); } } } } static void usage(void) { static const char *tbl[] = { "Usage: add [options] [scripts]" ,"" ,"Options:" ," -h print this message" ," -i interval specify compounding-interval (default=12)" ," -o script specify output-script name (default is the first" ," input-script name)" ," -p num specify precision (default=2)" ," -V print the version" ,"" ,"Description:" ," Script-based adding machine that allows you to edit the operations" ," and data." }; unsigned j; for (j = 0; j < SIZEOF(tbl); j++) fprintf(stderr, "%s\n", tbl[j]); exit(EXIT_FAILURE); } #if !HAVE_GETOPT int optind; int optchr; char *optarg; int getopt(int argc, char **argv, char *opts) { if (++optind < argc && (optarg = argv[optind]) != 0 && *optarg == '-') { if (*(++optarg) != ':' && (optchr = *optarg) != EOS && strchr(opts, *(optarg++)) != 0) return optchr; } return EOF; } #endif int main(int argc, char **argv) { long k; int j; int max_digits; /* maximum length of a number */ int changed; char tmp; Bool o_option = FALSE; (void) signal(SIGFPE, SIG_IGN); len_frac = 2; interval = 12; /* * Compute the maximum number of digits to display: * * big_long - the maximum positive value that we can stuff into * a 'long'. Assume symmetry, i.e., that we can use * the same negative magnitude. * val_width - the maximum length of a formatted number, counting * sign, decimal point and commas between groups * of digits. */ big_long = k = 1; while ((k = (k << 1) + 1) > big_long) big_long = k; for (k = big_long, max_digits = 0; k >= 10; k /= 10) max_digits++; FindHelp(argv[0]); while ((j = getopt(argc, argv, "hi:o:p:V")) != EOF) switch (j) { case 'p': if ((sscanf(optarg, "%d%c", &len_frac, &tmp) != 1) || (len_frac <= 0 || len_frac > max_digits - 2)) { fprintf(stderr, "Option p limited to 0..%d\n", max_digits - 2); usage(); } break; case 'i': if ((sscanf(optarg, "%d%c", &interval, &tmp) != 1) || (interval <= 0 || interval > 100)) { fprintf(stderr, "Option i limited to 0..100\n"); usage(); } break; case 'o': o_option = TRUE; top_output = optarg; break; case 'h': default: usage(); case 'V': puts(RELEASE); return EXIT_SUCCESS; } for (j = 0, val_frac = 1.0; j < len_frac; j++) val_frac *= 10.0; val_width = 1 + ((max_digits - len_frac) + 2) / 3 + max_digits + 1; /* * Allocate some dummy data so we can propagate results from it. */ all_data = 0; for (j = 0; j < 2; j++) { setval(AllocData((DATA *) 0), OP_ADD, 0.0, FALSE); } top_data = all_data->next; /* * If we have input scripts, save a pointer to the list: */ if (optind < argc) { if (top_output == 0 && !Fexists(argv[optind])) top_output = argv[optind++]; scriptv = argv + optind; for (j = 0; scriptv[j] != 0; j++) { (void) Ok2Read(scriptv[j]); } } else { scriptv = argv + argc; /* points to a null-pointer */ } /* * Get the default output-filename */ if (optind < argc && !top_output) top_output = argv[argc - 1]; if (top_output == 0) top_output = ""; /* * Setup and run the interactive portion of the program. */ screen_start(); changed = Loop(); screen_finish(); /* * If one or more scripts were given as input, and a '-o' argument * was given, overwrite the last one with the results. */ if (*top_output && changed && scriptCHG) PutScript(top_output); #if NO_LEAKS free(helpfile); while (FreeData(all_data, FALSE) != 0) /*EMPTY */ ; #if HAVE_DBMALLOC_H free(-1); /* FIXME: force linux+dbmalloc to report */ #endif #endif return (EXIT_SUCCESS); }