aboutsummaryrefslogtreecommitdiff
path: root/ch4
diff options
context:
space:
mode:
Diffstat (limited to 'ch4')
-rw-r--r--ch4/4-06_var-support.c272
1 files changed, 272 insertions, 0 deletions
diff --git a/ch4/4-06_var-support.c b/ch4/4-06_var-support.c
new file mode 100644
index 0000000..f57aa92
--- /dev/null
+++ b/ch4/4-06_var-support.c
@@ -0,0 +1,272 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <math.h>
+
+/* The C Programming Language: 2nd Edition
+ *
+ * Exercise 4-6: Add commands for handling variables. (It's easy to provide
+ * twenty-six variables with single-letter names.) Add a variable for the most
+ * recently printed value.
+ *
+ * Answer: I'm not sure if I found the "easy" way to do a bunch of variables.
+ * The knowledge of ASCII I gained earlier in the book let me know that 'a'
+ * minus 'a' == 0, and thus I used an array of doubles to do my bidding, using
+ * the appropriate math. Add a few extra things to the '\n' command case, and
+ * it pretty much wrote itself.
+ *
+ * One glaring limitation is the fact that you *must* put the variable letter
+ * directly after the '=' command or it won't assign it to anything. I could
+ * add another loop to account for this but I really see no need, since you
+ * should know which variable you're assigning to. It differentiates it from
+ * inline variables, too.
+ */
+
+#define MAXOP 100
+#define NUMBER '0'
+#define MAXVAL 100
+#define BUFSIZE 100
+
+int getop(char []);
+void push(double);
+double pop(void);
+int getch(void);
+void ungetch(int);
+void stack_top(void);
+double dupe_top(void);
+void swap_top_two(void);
+void clear_stack(void);
+double fetch_var(char);
+void store_var(char, double);
+void store_last(double);
+double fetch_last(void);
+
+int sp = 0; // Next free stack position
+double val[MAXVAL]; // Value stack
+char buf[BUFSIZE]; // buffer for ungetch
+int bufp = 0; // next free position in buf
+double vars[27];
+
+/* Reverse Polish calculator:
+ *
+ * Binary operations (+-*\%)
+ * operand operand operator
+ *
+ * Example: 6 minus 2 in Reverse Polish Notation is "6 2 -"
+ */
+int main() {
+ int type;
+ double op2;
+ char s[MAXOP];
+ char ltr;
+
+ while ((type = getop(s)) != EOF) {
+ if (isalpha(type) && islower(type)) {
+ push(fetch_var(type));
+ continue;
+ }
+ switch (type) {
+ case NUMBER:
+ push(atof(s));
+ break;
+ case '+':
+ push(pop() + pop());
+ break;
+ case '*':
+ push(pop() * pop());
+ break;
+ case '-':
+ op2 = pop();
+ push(pop() - op2);
+ break;
+ case '/':
+ op2 = pop();
+ if (op2 != 0.0) {
+ push(pop() / op2);
+ } else {
+ printf("Error: Cannot divide by zero.\n");
+ }
+ break;
+ /* Yay for modulus! */
+ case '%':
+ op2 = pop();
+ if (op2 != 0.0) {
+ push((int)pop() % (int)op2);
+ } else {
+ printf("Error: Cannot modulo by zero.\n");
+ }
+ break;
+ /* Top of stack */
+ case '?':
+ stack_top();
+ break;
+ /* Dupe the top of the stack */
+ case '#':
+ dupe_top();
+ break;
+ /* Swap the top two */
+ case '~':
+ swap_top_two();
+ break;
+ /* Clear the stack */
+ case '!':
+ clear_stack();
+ break;
+ /* sin() support */
+ case '(':
+ op2 = sin(pop());
+ push(op2);
+ break;
+ /* exp() support */
+ case '{':
+ op2 = exp(pop());
+ push(op2);
+ break;
+ /* pow() support */
+ case '^':
+ op2 = pop();
+ push(pow(pop(), op2));
+ break;
+ /* 'lastprint' support */
+ case '@':
+ push(fetch_last());
+ break;
+ /* setting variables */
+ case '=':
+ ltr = getchar();
+ if (isalpha(ltr) && islower(ltr)) {
+ op2 = pop();
+ store_var(ltr, op2);
+ push(op2);
+ }
+ break;
+ /* Final output */
+ case '\n':
+ op2 = pop();
+ printf("\t%.8g\n", op2);
+ /* Extra Credit: Lets output every non-zero variable! */
+ for (ltr = 'a'; ltr <= 'z'; ltr++) {
+ if (fetch_var(ltr) != 0) {
+ printf("\t%c: %.8g\n", ltr, fetch_var(ltr));
+ }
+ }
+ store_last(op2);
+ break;
+ default:
+ printf("Error: Unknown command %s\n", s);
+ break;
+ }
+ }
+ return 0;
+}
+
+void push(double f) {
+ if (sp < MAXVAL) {
+ val[sp++] = f;
+ } else {
+ printf("Error: Stack full. Cannot push %g\n", f);
+ }
+}
+
+double pop(void) {
+ if (sp > 0) {
+ return val[--sp];
+ } else {
+ printf("Error: Stack empty.\n");
+ return 0.0;
+ }
+}
+
+int getop(char s[]) {
+ int i = 0;
+ int c, next;
+
+ while ((s[0] = c = getch()) == ' ' || c == '\t') {
+ }
+ s[1] = '\0';
+ if (s[i] >= 'a' && s[i] <= 'z') {
+ return s[i];
+ }
+ /* The final check is for negative numbers. */
+ if (!isdigit(c) && c != '.' && c != '-') {
+ return c;
+ }
+ /* The second half of this if-statement accounts for negatives */
+ if (c == '-') {
+ next = getch();
+ if (!isdigit(next) && next != '.') {
+ return c;
+ } else {
+ c = next;
+ }
+ } else {
+ c = getch();
+ }
+
+ while (isdigit(s[++i] = c)) {
+ c = getch();
+ }
+ if (c == '.') {
+ while (isdigit(s[++i] = c = getch())) {
+ }
+ }
+ s[i] = '\0';
+ if (c != EOF) {
+ ungetch(c);
+ }
+ return NUMBER;
+}
+
+int getch(void) {
+ return (bufp > 0) ? buf[--bufp] : getchar();
+}
+
+void ungetch(int c) {
+ if (bufp >= BUFSIZE) {
+ printf("ungetch: Too many characters.\n");
+ } else {
+ buf[bufp++] = c;
+ }
+}
+
+void stack_top(void) {
+ if (sp > 0) {
+ printf("Top of stack is %8g\n", val[sp - 1]);
+ } else {
+ printf("Error: Stack empty.\n");
+ }
+}
+
+double dupe_top(void) {
+ double temp = pop();
+ push(temp);
+ push(temp);
+}
+
+void swap_top_two(void) {
+ double tmp1, tmp2;
+ tmp1 = pop();
+ tmp2 = pop();
+ push(tmp1);
+ push(tmp2);
+}
+
+void clear_stack(void) {
+ sp = 0;
+}
+
+double fetch_var(char c) {
+ return vars[c - 'a'];
+}
+
+void store_var(char c, double f) {
+ vars[c - 'a'] = f;
+}
+
+void store_last(double f) {
+ vars[26] = f;
+}
+
+double fetch_last(void) {
+ return vars[26];
+}
highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
import vgstash
import sqlite3
import click
import os
import subprocess
import sys
import tempfile
import yaml
import json

# Click also has this, but it doesn't support a fallback value.
from shutil import get_terminal_size

def get_db():
    """Fetch a vgstash DB object from the default location.

    Change DEFAULT_CONFIG['db_location'] before calling this function
    to alter behavior."""
    return vgstash.DB(vgstash.DEFAULT_CONFIG['db_location'])


@click.group('vgstash')
def cli():
    pass


@cli.command()
def init():
    db = get_db()
    click.echo("Initializing the database...")
    if db.create_schema():
        click.echo("Schema created.")
    else:
        raise sqlite3.OperationalError("Cannot create schema.")


def row_format(row, width, header):
    """
    Prints a row from the result set into a nice table.
    """
    # The magic number comes from:
    #    3 chars per separator (9 chars total)
    #    8 chars for "System" (so there's an additional space on each side)
    #    3 chars for "Own"
    #    9 chars for "Progress" and an additional space
    #    Total is 29 characters
    twidth = int(width) - 29
    if header == True:
        click.echo("{:<{w}s} | {:^8s} | {:^3s} | {}".format(
            "Title",
            "System",
            "Own",
            "Progress",
            w=twidth)
        )
        click.echo("-" * int(width))

    titlestr = "{: <{w}s}".format(row['title'][:twidth], w=twidth)
    systemstr = "{: ^8s}".format(row['system'][:8])
    # unowned, physical, digital, both
    ownltr = [' ', 'P', '  D', 'P D']
    ownstr = "{: <3s}".format(ownltr[row['ownership']])
    progltr = {
        0: '',
        1: 'N',
        2: 'P',
        3: 'B',
        4: 'C'
    }
    progstr = "{}".format((" " * (row['progress'] - 1) * 2) + progltr[row['progress']])
    print(" | ".join((titlestr, systemstr, ownstr, progstr)))


@cli.command('add')
@click.argument('title', type=str)
@click.argument('system', type=str)
@click.argument('ownership', type=str, required=False, default=vgstash.DEFAULT_CONFIG['ownership'])
@click.argument('progress', type=str, required=False, default=vgstash.DEFAULT_CONFIG['progress'])
@click.argument('notes', type=str, required=False, default="")
def add(title, system, ownership, progress, notes):
    db = get_db()
    game = vgstash.Game(title, system, ownership, progress, notes)
    try:
        db.add_game(game, update=False)
        own_clause = (
            "do not own",
            "physically own",
            "digitally own",
            "digitally and physically own",
        )
        progress_clause = (
            "cannot beat",
            "haven't started",
            "are playing",
            "have beaten",
            "have completed",
        )
        note_clause = "" if len(game.notes) == 0 else " It also has notes."
        click.echo("Added {} for {}. You {} it and {} it.{}".format(
            game.title,
            game.system,
            own_clause[game.ownership],
            progress_clause[game.progress],
            note_clause,
        ))
    except sqlite3.IntegrityError as e:
        print(e)
        click.echo("Couldn't add game.")


@cli.command('list')
@click.argument('filter', type=click.Choice(vgstash.FILTERS.keys()), required=False, default="allgames")
@click.option('--raw', '-r', is_flag=True, show_default=True, default=False, help="Output raw, pipe-delimited lines")
@click.option('--width', '-w', type=str, required=False, default=get_terminal_size(fallback=(80,24)).columns, help="The width of the table to output, in characters.")
def list_games(filter, raw, width):
    db = get_db()
    res = db.list_games(filter)
    first_pass = True
    # res can be False if the filter doesn't exist, but Click should catch it
    # and spit out an error before this function even starts.
    for r in res:
        if 'notes' in r.keys() and len(r['notes']) > 0:
            notes = r['notes'].replace('\n', '\\n')
            notes = notes.replace('\r', '\\r')
        else:
            notes = ''
        if raw:
            click.echo("|".join((
                r['title'],
                r['system'],
                str(r['ownership']),
                str(r['progress']),
                notes
            ))
            )
        else:
            row_format(r, width, first_pass)
            first_pass = False


@cli.command('delete')
@click.argument('title', required=True)
@click.argument('system', required=True)
def delete_game(title, system):
    db = get_db()
    target_game = vgstash.Game(title, system)
    if db.delete_game(target_game):
        click.echo("Removed {} for {} from your collection.".format(title, system))
    else:
        click.echo("That game does not exist in your collection. Please try again.")


@cli.command('update')
@click.argument('title', required=True)
@click.argument('system', required=True)
@click.argument('attr', type=click.Choice(['title', 'system', 'ownership', 'progress']), required=True)
@click.argument('val', required=True)
def update_game(title, system, attr, val):
    # TODO: Consider namedtuple as a solution
    db = get_db()
    try:
        target_game = db.get_game(title, system)
    except:
        click.echo("Game not found. Please try again.")
        return
    if attr == 'ownership':
        val = vgstash.vtok(val, vgstash.OWNERSHIP)
    if attr == 'progress':
        val = vgstash.vtok(val, vgstash.PROGRESS)
    updated_game = vgstash.Game(
        val if attr == 'title' else target_game.title,
        val if attr == 'system' else target_game.system,
        val if attr == 'ownership' else target_game.ownership,
        val if attr == 'progress' else target_game.progress,
        target_game.notes
    )
    if db.update_game(target_game, updated_game):
        click.echo("Updated {} for {}. Its {} is now {}.".format(title, system, attr, val))


@cli.command('notes')
@click.argument('title', required=True)
@click.argument('system', required=True)
@click.option('--edit', '-e', is_flag=True, default=False)
def notes(title, system, edit):
    db = get_db()
    try:
        target_game = db.get_game(title, system)
    except:
        click.echo("Game not found. Please try again.")
        return

    if edit:
        with tempfile.NamedTemporaryFile() as tmpfile:
            tmpfile.write(target_game.notes.encode("UTF-8"))
            tmpfile.flush()
            process = subprocess.run([os.getenv("EDITOR", "vim"), tmpfile.name])
            tmpfile.flush()
            tmpfile.seek(0)
            note_arr = []
            for line in tmpfile:
                note_arr.append(line.decode("UTF-8").rstrip("\r\n"))
            target_game.notes = "\n".join(note_arr)
            db.update_game(target_game, target_game)
        if process.returncode == 0:
            click.echo("Notes for {} on {} have been updated!".format(target_game.title, target_game.system))
        else:
            click.echo("Couldn't find an editor for notes. Check the EDITOR environment variable and try again.")
    else:
        if len(target_game.notes) > 0:
            click.echo("Notes for {} on {}:".format(target_game.title, target_game.system))
            click.echo()
            click.echo(target_game.notes)
        else:
            click.echo("No notes for {} on {}.".format(target_game.title, target_game.system))


@cli.command("import")
@click.option("--format", "-f", type=click.Choice(["yaml", "json"]), required=False, default="yaml")
@click.option("--update", "-u", is_flag=True, default=False, help="Overwrite existing games with the file's data")
@click.argument("filepath",
                type=click.Path(
                    readable=True,
                    resolve_path=True,
                    dir_okay=False,
                    file_okay=True),
                default=sys.stdin,
                required=False,
                )
def import_file(format, filepath, update):
    """
    Import game data from an external file matching the chosen format.

    The default format is YAML.

    Available formats:

    * JSON
    * YAML
    """
    with open(filepath) as fp:
        if format == "yaml":
            data = yaml.safe_load(fp)
        if format == "json":
            data = json.load(fp)
    db = get_db()
    count = len(data)
    for game in data:
        try:
            db.add_game(
                vgstash.Game(
                    game["title"],
                    game["system"],
                    game["ownership"],
                    game["progress"],
                    game["notes"]
                ),
                update=update
            )
        except sqlite3.IntegrityError as e:
            # skip games that already exist
            count -= 1
    if count > 0:
        click.echo("Successfully imported {} games from {}.".format(count, filepath))
    else:
        click.echo("Couldn't import any games. Is the file formatted correctly?")


@cli.command("export")
@click.option("--format", "-f", type=click.Choice(["yaml", "json"]), required=False, default="yaml")
@click.argument("filepath",
                type=click.Path(
                    exists=False,
                    readable=True,
                    writable=True,
                    resolve_path=True,
                    dir_okay=False,
                    file_okay=True),
                default=sys.stdout,
                required=False,
                )
def export_file(format, filepath):
    """
    Export the game database to a file written in the chosen format.

    The default format is YAML.

    Available formats:

    * JSON
    * YAML
    """
    db = get_db()
    data = db.list_games()
    game_set = []
    # Time to re-read the master branch's code
    for game in data:
        g = {}
        for field in game.keys():
            g.update({field: game[field]})
        game_set.append(g)
    with open(filepath, "w") as fp:
        if format == "yaml":
            yaml.dump(game_set, fp, default_flow_style=False,
                             indent=4, allow_unicode=True)
        if format == "json":
            json.dump(game_set, fp, allow_nan=False, indent=1, skipkeys=True, sort_keys=True)
    if len(game_set) > 0:
        click.echo("Successfully exported {} games to {}.".format(len(game_set), filepath))
    else:
        click.echo("Could not export any games; have you made sure your collection has games in it?")