Due Date: | 4/20/17 |
---|
The monster database library we built in the previous labs and homeworks has a couple of limitations:
One possible solution is to just make all of these #defines ridiculously large, but that would be an inefficient use of memory and an unnecessary complication to the specifications.
There's a more elegant solution: Make all of the arrays take up exactly the amount of memory they require with no unused space left over. We can do this with dynamic storage allocation.
monsterdb2 is the name we'll give to an enhanced version of the monsterdb library we built in Lab 11 that will use dynamic storage.
First, we'll slightly modify the monsterdb.h header file.
Download the template monsterdb2_tplt.h from the homework directory:
Rename it to monsterdb2.h:
$ mv monsterdb2_tplt.h monsterdb2.h
Here are its contents:
#ifndef _INCLUDED_MONSTERDB_H /* effectively prevents multiple #includes */ /* * This header file describes the "Monster" struct and all accessible * functions in the "monsterdb" library. */ /* * This `struct` declares all information we want to maintain in the * monster database. This is identical to the original Monster * `struct`, except that we've changed the strings into `char` * pointers. */ struct Monster { char *name; char *film; int year; char *weakness; char *defeatedBy; double rating; }; /* * ASSIGNMENT * * Add an "extern" declaration of `monsters_p` as a pointer to an * array of Monster structs instead of an array `monsters`. * (Syntactically, this would also be a pointer to a single Monster: * In C, they're the same thing.) */ extern int monsterdb_getCount(void); extern int monsterdb_load(char *fileName); extern void monsterdb_print(void); extern void monsterdb_printCsv(void); extern void monsterdb_printHtml(void); extern void monsterdb_sortUsing(int (*compare_p)( const void *monster0_vp, const void *monster1_vp)); #define _INCLUDED_MONSTERDB_H #endif
Next, we'll create a modified monsterdb.c source file. Here are the steps:
Download the template monsterdb2_tplt.c from the homework directory.
Rename it to monsterdb2.c:
$ mv monsterdb2_tplt.c monsterdb2.c
Here are its contents:
#include <stdio.h> #include <string.h> /* for strcmp() prototype */ #include <stdlib.h> /* for qsort(), atoi(), and atof() prototypes */ /* * ASSIGNMENT * * "#include" the new header file here. */ /* * Declaring storage for the database. */ /* * ASSIGNMENT * * Declare the `monsters_p` pointer here. This should be identical to * the one in the header file, except that it won't begin with * "extern". * * Also declare an `int monsterCount` here and initialize it to * 0. It's not in the header file, so be sure to make it global only * to this file (hint: `static`). We'll use this to maintain the size * of the database rather than an entry with a zero-length monster * name. */ /* * ASSIGNMENT * * Put other "static forward" prototypes here as needed. */ static int foundColumnName(FILE *f_p, char columnName[], char followedBy) /* helper function used to read an expected column name */ { /* * ASSIGNMENT * * Modify the body of this function to use fscanToken() instead * of fscanf() as follows by implementing this pseudocode: * * let `token` be the result of an fscanToken() call on `f_p`, * expecting the delimiting character to be `followedBy` * if `token` is NULL (there were errors) * return 0 * call strcmp() to compare `token` with `columnName` * if they are not equal * call free() on `token` (you don't need it any more) * return a 0 (the expected token not found) * (at this point the expected token was found) * call free() on `token` * return a 1 * * Note that since `token` was dynamically allocated, it's the * programmer's responsibility to free() it when it is no longer * in use. */ } int monsterdb_getCount(void) { /* * ASSIGNMENT * * Now that there's a global variable containing the number of * monsters in the database, just return it. */ } int monsterdb_load(char *fileName) /* * reads a CSV monster database from `fileName`, returns the number of * members (or 0 if there are errors) */ { /* * ASSIGNMENT * * Rewrite the body of this function to implement the following: * * let `f_p` be the result of fopen()'ing `fileName` for reading * if the fopen() fails, * return 0 * if scanCsvHeader() can't read the header from `f_p`, * close `f_p` with fclose() * return 0 * loop: * use scanCsvRow() to read a row into a Monster struct `monster` * if it fails, * break out of the loop * call malloc() to allocate a (pointer to a) `newMonsters_p` * array big enough to hold all the old monsters and the new * one (its size will be (monsterCount + 1) * sizeof(Monster) * bytes) * for each existing monster `i`, * set `newMonsters_p[i]` to `monsters_p[i]` * set `newMonsters_p[monsterCount]` to `monster` * increment `monsterCount` * free the existing `monsters_p` (pointer) * set `monsters_p` to `newMonsters_p`, so that it points to * the (new) block of memory * close `f_p` with fclose() * return `monsterCount` */ } void monsterdb_print(void) /* print a human-readable version of the database on standard output */ { int i, count; count = monsterdb_getCount(); for (i = 0; i < count; i++) { printMonster(monsters_p[i]); putchar('\n'); } } void monsterdb_printCsv(void) /* prints the monster database on standard output as CSV */ { int i, count; printCsvHeader(); count = monsterdb_getCount(); for (i = 0; i < count; i++) { printCsvRow(monsters_p[i]); } } /* * This is from the main() function in Part 2 of Laboratory #9. */ void monsterdb_printHtml(void) /* prints the monster database on standard output as HTML */ { int i, count; printf("<table>\n"); /* HTML tables begin with this tag */ printHtmlTableHeader(); count = monsterdb_getCount(); for (i = 0; i < count; i++) { printHtmlTableRow(monsters_p[i]); } printf("</table>\n"); /* HTML tables end with this tag */ } /* * This is from Homework #6. */ void monsterdb_sortUsing(int (*compare_p)( const void *monster0_vp, const void *monster1_vp)) /* sort the monster database given a comparison function (*compare_p)() */ { int monsterCount = monsterdb_getCount(); qsort(monsters_p, monsterCount, sizeof(struct Monster), compare_p); } /* * This is from Homework #6, but made "static". */ static void printCsvHeader(void) /* print a CSV-compatible "monster" header to standard output */ { printf("Name,"); printf("Film,"); printf("Year,"); printf("Weakness,"); printf("Defeated by,"); printf("Rating\n"); } /* * This is from Homework #6, but made "static". */ static void printCsvRow(struct Monster monster) /* print a CSV-compatible "monster" row to standard output */ { printf("%s,", monster.name); printf("%s,", monster.film); printf("%d,", monster.year); printf("%s,", monster.weakness); printf("%s,", monster.defeatedBy); printf("%.1f\n", monster.rating); } /* * This is from Laboratory #9. */ static void printHtmlTableHeader(void) /* print the header for an HTML table of monsters */ { printf("<tr>\n"); /* HTML table rows begin with this tag */ printf("<th>Name</th>\n"); printf("<th>Film</th>\n"); printf("<th>Year</th>\n"); printf("<th>Weakness</th>\n"); printf("<th>Defeated by</th>\n"); printf("<th>Rating</th>\n"); printf("</tr>\n"); /* HTML table rows end with this tag */ } /* * This is from Laboratory #9. */ static void printHtmlTableRow(struct Monster monster) /* print the data in "monster" as an HTML row */ { printf("<tr>\n"); /* HTML table rows begin with this tag */ printf("<td>%s</td>\n", monster.name); printf("<td>%s</td>\n", monster.film); printf("<td>%d</td>\n", monster.year); printf("<td>%s</td>\n", monster.weakness); printf("<td>%s</td>\n", monster.defeatedBy); printf("<td>%.1f</td>\n", monster.rating); printf("</tr>\n"); /* HTML table rows end with this tag */ } /* * This is "monster_print()" from Laboratory #9, but renamed and made * "static". */ static void printMonster(struct Monster monster) /* print a "monster" in a convenient, human-readable format */ { int colonIndent = 15; /* set this empirically so the ':'s line up */ /* * The following shows a good way to print out attributes: one per * line with the attribute name appearing as "ragged left" (as * often done for movie and TV credits) followed by a ':'. */ printf("%*s: %s\n", colonIndent, "Name", monster.name); printf("%*s: %s\n", colonIndent, "Film", monster.film); printf("%*s: %d\n", colonIndent, "Year", monster.year); printf("%*s: %s\n", colonIndent, "Weakness", monster.weakness); printf("%*s: %s\n", colonIndent, "Defeated by", monster.defeatedBy); printf("%*s: %.1f\n", colonIndent, "Rating", monster.rating); } /* * This is from Homework #6, modified as you should have for Homework * #7. We have also made it "static", as it is purely internal to the * library. */ static int scanCsvHeader(FILE *f_p) /* scans a CSV-compatible "monster" header for compatibility */ { /* * Read a CSV header line and verify its correctness. Return 1 if * it is correct and 0 if it is not. * * You need to confirm that each header field is occurring in * order. The function foundColumnName() can do the dirty work * here. All you need to do is call it properly. */ if (!foundColumnName(f_p, "Name", ',')) return 0; if (!foundColumnName(f_p, "Film", ',')) return 0; if (!foundColumnName(f_p, "Year", ',')) return 0; if (!foundColumnName(f_p, "Weakness", ',')) return 0; if (!foundColumnName(f_p, "Defeated by", ',')) return 0; if (!foundColumnName(f_p, "Rating", '\n')) return 0; return 1; } static int scanCsvRow(FILE *f_p, struct Monster *monster_p) /* reads a Monster row from a CSV file */ { /* * ASSIGNMENT * * Replace the body of this function to use fscanToken() instead * of fscanf() as follows by implementing this pseudocode: * * let `token` be the result of an fscanToken() call on f_p, expecting * the delimiting character to be `,` * if `token` is NULL (there were errors) * return a 0 (i.e. failure) to the caller * set `monster_p->name` to `token` * do the same for `monster_p->film` as for `monster_p->name` * do the same for `monster_p->year` as for `monster_p->name`, except * that you'll (a) use atoi() (see below) to convert `token` to * an int before assigning it and (b) call free() on `token` * afterwards: We're keeping the int, not the token. * do the same for `monster_p->weakness` as for `monster_p->name` * do the same for `monster_p->defeatedBy` as for `monster_p->name` * do the same for `monster_p->rating` as for `monster_p->year`, * except that (a) the delimiter will be '\n' instead of ',' and * (b) you'll use atof() (see below) to convert `token` to a * double before assigning it * return a 1 (i.e. success) to the caller */ } /* * In programming, we often look at input data in terms of "tokens": * strings of characters which have a particular meaning. In the case * of C, tokens are things like numbers, strings, variable names, and * keywords. In the case of a CSV file, a "token" is any sequence of * characters which isn't a comma or a newline. Commas and newlines * are then called "delimiters", as they "delimit" tokens. */ static char *fscanToken(FILE *f_p, char delimiter) /* reads a token (delimited by `delimiter`, which is ',' or '\n') from `f_p` */ { /* * ASSIGNMENT * * This is a short and very versatile (but somewhat tricky) * function to write in C. Implement the following pseudocode: * * call malloc() to allocate a char pointer `str` to a string of * size 1 * call strcpy() to copy a zero-length string (i.e. "") to `str` * set an int `lenStr` (the length of `str`) to 0 * loop: * call getc() to read a character (declared "int") `ch` from `f_p` * if `ch` is EOF, ',', '\n' * break out of the loop * increment `lenStr` * call malloc() to allocate a char pointer `newStr` to a new * string of size `lenStr` + 1 * call strcpy() to copy `str` to `newStr` * set the next-to-last element of `newStr[]` to `ch` * set last element of `newStr[]` to `\0` (so `newStr[]` remains * null-terminated) * call `free()` on `str` * set `str` to `newStr` * if `ch` is `delimiter` * return str * else * call `free()` on `str` * return NULL * * Note this function returns either NULL or a pointer to a * malloc()'d string. In the former case, this function must * free() that string. In the latter case, it is the * responsibility of the caller to free() it if and when it is no * longer needed. */ }
Compile your code to produce monsterdb2.o:
$ gcc -Wall -c monsterdb2.c
You'll use this "object" file in the next part.
Download monsters2.csv from the homework directory. This version contains new monster data, including some that would not have worked with the earlier version.
This assignment makes use of two functions that convert (or "parse") strings into numeric values. atoi() parses a string argument into an int, which it returns. Its (simplified) prototype is:
int atoi(char *token_p);
so that if i is an int variable:
i = atoi("42");
would accomplish the same thing as:
i = 42;
Similarly, atof()'s prototype is:
double atof(char *token_p);
It works just like atoi(), except that it parses its argument into a double, so that if d is a double variable:
d = atof("3.1515");
would accomplish the same thing as:
d = 3.1515;
You'll use these functions to convert string tokens to numerical values in scanCsvRow().
With a couple of minor changes, all of the test programs you used for the monsterdb library will work with the monsterdb2 library. These changes are:
Otherwise, the previous code should continue to work. In the software biz, this is referred to as "backwards compatibility".
The homework directory contains the same three source files that came with the first version monsterdb. Do the following:
Download the file monsterdb_print.c.
Rename it:
$ mv monsterdb_print.c monsterdb2_print.c
Modify it as above.
Compile it with:
$ cc -Wall -c monsterdb2_print.c
Link it it together with monsterdb2.o to build the monsterdb2_print.exe program (aka "binary"):
$ cc monsterdb2_print.o monsterdb2.o -o monsterdb2_print.exe
Run it and make sure it's giving the expected output.
Perform steps 1-6 for monsterdb_html.c, compiling it into monsterdb2_html.exe.
Perform steps 1-6 for monsterdb_sort.c, compiling it into monsterdb2_sort.exe.
This project requires you to modify multiple files, so you need to turn them all in. Instead of making multiple Angel entries, you will turn in a single file created by a command line utility named tar, which stands for "Tape Archive Utility". Don't worry about the "Tape" part. There's no tape drive involved here: That's how the format was first created, but these day's it's used everywhere to copy multiple files into a single file called a "tarball" for transmission.
tar is also used to extract those files at the receiving end.
Once you're satisfied that all of your source code is working, create the tarball with this command:
$ tar -cvzf hw08.tgz monsterdb2.c monsterdb2.h monsterdb2_print.c \ monsterdb2_html.c monsterdb2_sort.c
(The ``\`` at the end of the line lets the line wrap in this document. It also works in the shell, but is not necessary if you just type one long line.)
This will create a file hw08.tgz that contains all five files in gzip-compressed form. gzip-compression is a traditional way to package multiple source files in tarballs under Linux, MacOS, and other UNIX-like systems. Windows systems use the zip format, which is different from gzip.
tar (the "GNU" version) is available under MinGW (e.g. West 145), under all Linux systems (e.g. West 151) and under MacOS, but if you don't have it on your system, you may submit a zip archive, but you're on your own to figure out how to create it.
Submit hw08.tgz as you've submitted source files via email in the past.