/* Code provided by S. Cotton, U.Penn
*
* N O T E !!!! (Rada 06/11/01)
* For this code to work, it is VERY important that all input files
* (i.e. key, answer and sensemap) are SORTED
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define TRUE 1
#define FALSE 0
#define NOT_FOUND -1
#define COMMENT_DELIMITER "!!"
#define WEIGHT_DELIMITER '/'
#define MINIMAL_SCORING_FLAG "-m"
#define VERBOSITY_FLAG "-v"
#define GRANULARITY_FLAG "-g"
#define COARSE_GRAIN_FLAG "coarse"
#define MIXED_GRAIN_FLAG "mixed"
#define UNASSIGNABLE_SENSE_TAG "U"
#define TYPO_SENSE_TAG "TYPO"
#define PROPER_SENSE_TAG "P"
typedef char boolean;
typedef struct
{FILE * pointer;
char * name;
char * line;
long int length_of_line;}
i_o_block;
typedef struct
{char ** index;
char * buffer;
long int last_entry;
long int initial_bound [('z' - 'a') + 1] [('z' - 'a') + 1] [2];}
string_table;
void newline (void)
{printf ("\n");
return;}
void fnewline (FILE * out)
{if (out != NULL) fprintf (out, "\n");
return;}
boolean strequal (char * first_string, char * second_string)
{int i;
if (first_string == NULL || second_string == NULL) return FALSE;
for (i = 0; first_string [i] != '\0' && second_string [i] != '\0'; i++)
if (first_string [i] != second_string [i]) break;
return (first_string [i] == second_string [i]);}
char * make_buffer_of_size (long int length)
{if (length <= 0) length = 1;
return (char *) calloc (length, sizeof (char));}
void reclaim (void * space_to_reclaim)
{if (space_to_reclaim != NULL) free (space_to_reclaim);
return;}
void copystr (char ** target_string, char * source_string)
{int i;
if (target_string == NULL || source_string == NULL) return;
reclaim (*target_string);
i = 0;
while (source_string [i] != '\0') i++;
*target_string = (char *) calloc (i + 1, sizeof (char));
while (i >= 0)
{*(*target_string + i) = source_string [i];
i--;}
return;}
void addstr (char ** target_string, char * source_string)
{char * new_string;
int i;
int j;
if (target_string == NULL || source_string == NULL) return;
i = 0;
if (*target_string != NULL)
while (*(*target_string + i) != '\0')
i++;
j = 0;
while (source_string [j] != '\0') j++;
new_string = (char *) calloc (i + j + 1, sizeof (char));
i = 0;
if (*target_string != NULL)
while (*(*target_string + i) != '\0')
{new_string [i] = *(*target_string + i);
i++;}
for (j = 0; source_string [j] != '\0'; j++)
{new_string [i] = source_string [j];
i++;}
new_string [i] = '\0';
reclaim (*target_string);
*target_string = new_string;
return;}
boolean has_prefix (char * line, char * prefix)
{int i;
if (line == NULL || prefix == NULL) return FALSE;
for (i = 0; prefix [i] != '\0'; i++)
if (line [i] != prefix [i]) break;
return (prefix [i] == '\0');}
void strip_carriage_return (char * string_to_be_stripped)
{int length = 0;
if (string_to_be_stripped == NULL) return;
while (string_to_be_stripped [length] != '\0') length++;
length--;
if (string_to_be_stripped [length] == '\n') string_to_be_stripped [length] = '\0';
return;}
boolean unreadable_file (char * file_name)
{FILE * file_to_be_tested;
if (file_name == NULL) return TRUE;
if ((file_to_be_tested = fopen (file_name, "r")) != NULL)
{fclose (file_to_be_tested);
return FALSE;}
else
{fclose (file_to_be_tested);
return TRUE;}}
long int length_of_file (char * file_name)
{FILE * file_to_be_measured;
long int counter = 0;
if (file_name != NULL)
{if ((file_to_be_measured = fopen (file_name, "r")) != NULL)
{while (fgetc (file_to_be_measured) != EOF) counter++;
counter++;}
fclose (file_to_be_measured);}
return counter + 1;}
long int length_of_longest_line_in (char * file_name)
{FILE * file_to_be_measured;
char file_character;
long int max = 0;
long int i;
if (file_name != NULL)
{if ((file_to_be_measured = fopen (file_name, "r")) != NULL)
{i = 1;
while ((file_character = fgetc (file_to_be_measured)) != EOF)
{if (file_character == '\n')
{if (i > max) max = i;
i = 1;}
else
i++;}
if (i > max) max = i;}
fclose (file_to_be_measured);}
return max + 1;}
boolean open_file (i_o_block * file_to_open, char * mode)
{if (file_to_open != NULL)
{file_to_open -> length_of_line = length_of_longest_line_in (file_to_open -> name);
file_to_open -> line = make_buffer_of_size (file_to_open -> length_of_line);
if (mode != NULL)
file_to_open -> pointer = fopen (file_to_open -> name, mode);
else
file_to_open -> pointer = NULL;
return (file_to_open -> pointer != NULL);}
return FALSE;}
void close_file (i_o_block * file_to_close)
{if (file_to_close != NULL)
{fclose (file_to_close -> pointer);
file_to_close -> pointer = NULL;
file_to_close -> length_of_line = 0;
reclaim (file_to_close -> line);
file_to_close -> line = NULL;
reclaim (file_to_close -> name);
file_to_close -> name = NULL;}
return;}
boolean read_line (i_o_block * file_to_read)
{if (file_to_read == NULL || file_to_read -> line == NULL || file_to_read -> pointer == NULL ||
fgets (file_to_read -> line, file_to_read -> length_of_line, file_to_read -> pointer) == NULL)
return FALSE;
strip_carriage_return (file_to_read -> line);
return TRUE;}
string_table * make_string_table (void)
{string_table * new;
int i;
int j;
new = (string_table *) malloc (sizeof (string_table));
new -> index = NULL;
new -> buffer = NULL;
new -> last_entry = 0;
for (i = 0; i < ('z' - 'a') + 1; i++)
for (j = 0; j < ('z' - 'a') + 1; j++)
{new -> initial_bound [i] [j] [0] = 0;
new -> initial_bound [i] [j] [1] = 0;}
return new;}
void reclaim_string_table (string_table * table_to_reclaim)
{if (table_to_reclaim != NULL)
{reclaim (table_to_reclaim -> index);
reclaim (table_to_reclaim -> buffer);
reclaim (table_to_reclaim);}
return;}
void build_initial_bounds_of_table (string_table * table_to_build)
{char * table_entry;
int current_first_letter = NOT_FOUND;
int current_second_letter = NOT_FOUND;
int i;
if (table_to_build != NULL)
{for (i = 0; i < table_to_build -> last_entry; i++)
{table_entry = table_to_build -> index [i];
if (table_entry [0] < 'a' || table_entry [0] > 'z' ||
table_entry [1] < 'a' || table_entry [1] > 'z')
{if (current_first_letter != NOT_FOUND)
{table_to_build -> initial_bound [current_first_letter] [current_second_letter] [1] = i;
current_first_letter = NOT_FOUND;}}
else
{if (table_entry [0] - 'a' != current_first_letter ||
table_entry [1] - 'a' != current_second_letter)
{if (current_first_letter != NOT_FOUND)
table_to_build -> initial_bound [current_first_letter] [current_second_letter] [1] = i;
current_first_letter = table_entry [0] - 'a';
current_second_letter = table_entry [1] - 'a';
table_to_build -> initial_bound [current_first_letter] [current_second_letter] [0] = i;}}}
if (current_first_letter != NOT_FOUND)
table_to_build -> initial_bound [current_first_letter] [current_second_letter] [1] = i;}
return;}
long int search_string_table (char * word, string_table * table_to_search)
{long int high_bound;
long int low_bound;
long int new_middle;
long int i;
int j;
if (word == NULL || table_to_search == NULL || table_to_search -> index == NULL)
return NOT_FOUND;
if (word [0] >= 'a' && word [0] <= 'z' &&
word [1] >= 'a' && word [1] <= 'z')
{high_bound = table_to_search -> initial_bound [word [0] - 'a'] [word [1] - 'a'] [1];
if (high_bound == 0) return NOT_FOUND;
low_bound = table_to_search -> initial_bound [word [0] - 'a'] [word [1] - 'a'] [0];}
else
{high_bound = table_to_search -> last_entry;
low_bound = 0;}
new_middle = (high_bound + low_bound) / 2;
do
{i = new_middle;
for (j = 0; word [j] != '\0'; j++)
{
if (word [j] < table_to_search -> index [i] [j])
{high_bound = i;
break;}
if (word [j] > table_to_search -> index [i] [j])
{low_bound = i;
break;}}
if (word [j] == '\0')
{if (table_to_search -> index [i] [j] == '\0' ||
table_to_search -> index [i] [j] == ' ')
return i;
else
high_bound = i;}
new_middle = (high_bound + low_bound) / 2;}
while (i != new_middle);
return NOT_FOUND;}
string_table * unsorted_string_table_for (char * file_name)
{string_table * new;
FILE * words;
long int beginning_of_word;
long int characters_seen;
char file_character;
new = make_string_table ();
characters_seen = 0;
if (file_name != NULL)
{if ((words = fopen (file_name, "r")) != NULL)
while ((file_character = fgetc (words)) != EOF)
{if (file_character == '\n')
(new -> last_entry)++;
characters_seen++;}
fclose (words);}
new -> buffer = make_buffer_of_size (characters_seen + 1);
new -> buffer [0] = '\0';
new -> index = (char **) calloc ((new -> last_entry) + 1, sizeof (char *));
new -> index [0] = new -> buffer;
new -> last_entry = 0;
characters_seen = 0;
beginning_of_word = 0;
if (file_name != NULL)
{if ((words = fopen (file_name, "r")) != NULL)
while ((new -> buffer [characters_seen] = fgetc (words)) != EOF)
{if (new -> buffer [characters_seen] == '\n')
{new -> buffer [characters_seen] = '\0';
new -> index [new -> last_entry] = &(new -> buffer [beginning_of_word]);
beginning_of_word = characters_seen + 1;
(new -> last_entry)++;}
characters_seen++;}
fclose (words);}
return new;}
string_table * string_table_for (char * file_name)
{string_table * new;
new = unsorted_string_table_for (file_name);
build_initial_bounds_of_table (new);
return new;}
char ** tokenize (char * line)
{char ** tokens;
int last_token = 0;
int i;
if (line != NULL)
tokens = (char **) calloc (strlen (line) + 1, sizeof (char *));
else
tokens = (char **) calloc (1, sizeof (char *));
tokens [last_token] = NULL;
if (line != NULL)
for (i = 0; line [i] != '\0'; i++)
{if (line [i] == ' ' || line [i] == '\n' || line [i] == '\t')
{line [i] = '\0';
if (tokens [last_token] != NULL)
{last_token++;
tokens [last_token] = NULL;}}
else
if (tokens [last_token] == NULL)
tokens [last_token] = &(line [i]);}
if (tokens [last_token] != NULL)
{last_token++;
tokens [last_token] = NULL;}
return tokens;}
char * coarse_sense_tag_for (char * tag, string_table * sense_table)
{char * coarse_tag;
long int sense_index;
int i;
coarse_tag = tag;
sense_index = search_string_table (tag, sense_table);
if (sense_index != NOT_FOUND)
{coarse_tag = sense_table -> index [sense_index];
for (i = 0; sense_table -> index [sense_index] [i] != '\0'; i++)
if (sense_table -> index [sense_index] [i] == ' ')
coarse_tag = &(sense_table -> index [sense_index] [i + 1]);}
return coarse_tag;}
float subsumption_probability_of_tags (char * subsumer, char * tag, string_table * sense_table)
{float probability;
long int sense_index;
int i;
int j;
sense_index = search_string_table (tag, sense_table);
if (sense_index != NOT_FOUND && subsumer != NULL)
{probability = 1.0;
i = 0;
for (j = 0; sense_table -> index [sense_index] [j] != '\0'; j++)
if (j == 0 || sense_table -> index [sense_index] [j - 1] == ' ')
{i++;
if (i % 2 == 0)
probability *= atof (&(sense_table -> index [sense_index] [j]));
else
if (has_prefix (&(sense_table -> index [sense_index] [j]), subsumer) &&
(sense_table -> index [sense_index] [j + strlen (subsumer)] == ' ' ||
sense_table -> index [sense_index] [j + strlen (subsumer)] == '\0'))
return 1.0 / probability;}}
return 0.0;}
int main (int argc, char * argv [])
{
boolean invalid_argument_seen = FALSE;
string_table * answer;
string_table * key;
string_table * sense_table;
int sense_table_arg = 0;
char *** key_tag;
char ** answer_tag;
float * answer_weight;
long int answer_index;
long int key_index;
long int sense_index;
char * sense_granularity = NULL;
char * coarse_sense_tag;
boolean minimal_scoring_desired = FALSE;
boolean line_by_line_trace_desired = FALSE;
float total_weight;
float instances_attempted;
float instances;
float score;
float subscore;
float global_score;
int i;
int j;
int k;
if (argc < 3)
{fnewline (stderr);
fprintf (stderr, "This scorer requires two command-line arguments in the");
fnewline (stderr);
fprintf (stderr, "following order:");
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, " ANSWER FILE NAME (name of a file containing formatted answers)");
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, " KEY FILE NAME (name of an answer-key file)");
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, "Optionally, the following may be appended:");
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, " SENSE FILE NAME (name of a file containing sense-map information)");
fnewline (stderr);
fprintf (stderr, " - without this file, only fine-grained scoring is available,\n");
fprintf (stderr, " and illformed sense tags will lower precision (rather than recall)");
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, " %s", GRANULARITY_FLAG);
fprintf (stderr, " (specifies granularity: ");
fprintf (stderr, "\"%s\" or \"%s\"; \"fine\" is default)", COARSE_GRAIN_FLAG, MIXED_GRAIN_FLAG);
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, " %s", MINIMAL_SCORING_FLAG);
fprintf (stderr, " (causes exclusion of instances tagged with multiple tags in key)");
fnewline (stderr);
fnewline (stderr);
fprintf (stderr, " %s", VERBOSITY_FLAG);
fprintf (stderr, " (causes line-by-line scoring calculations to be printed)");
fnewline (stderr);
fnewline (stderr);
invalid_argument_seen = TRUE;}
for (i = 1; i <= 2 && i < argc; i++)
if (unreadable_file (argv [i])) {
fnewline (stderr);
fprintf (stderr, "Unable to read \"%s\".", argv [i]);
fnewline (stderr);
fnewline (stderr);
invalid_argument_seen = TRUE;
}
for (i = 3; i < argc; i++) {
if (argv[i][0] != '-') {
if (unreadable_file (argv [i])) {
fnewline (stderr);
fprintf (stderr, "Unable to read \"%s\".", argv [i]);
fnewline (stderr);
fnewline (stderr);
invalid_argument_seen = TRUE;
} else {
sense_table_arg = i;
}
}
if (strequal (argv [i], MINIMAL_SCORING_FLAG)) minimal_scoring_desired = TRUE;
if (strequal (argv [i], VERBOSITY_FLAG)) line_by_line_trace_desired = TRUE;
if (strequal (argv [i], GRANULARITY_FLAG) && i + 1 < argc &&
(strequal (argv [i + 1], COARSE_GRAIN_FLAG) || strequal (argv [i + 1], MIXED_GRAIN_FLAG)))
sense_granularity = argv [++i];
}
if (sense_table_arg == 0) {
/* force fine-grained scoring */
if (sense_granularity) {
fprintf(stderr,"Fine-grained scoring will be used since no sense map was given\n");
sense_granularity = NULL;
}
}
if (!(invalid_argument_seen)) {
global_score = 0.0;
instances = 0.0;
instances_attempted = 0.0;
if (sense_table_arg > 0) {
sense_table = string_table_for (argv [sense_table_arg]);
} else {
sense_table = NULL;
}
key = string_table_for (argv [2]);
key_tag = (char ***) calloc (key -> last_entry + 1, sizeof (char ***));
for (key_index = 0; key_index < key -> last_entry; key_index++) {
/* Re-format the key's first two fields, which specify the test instance to be answered */
for (i = 0; key -> index [key_index] [i] != '\0'; i++)
if (key -> index [key_index] [i] == ' ') {
key -> index [key_index] [i] = '_';
while (key -> index [key_index] [i] != '\0' &&
key -> index [key_index] [i] != ' ')
{i++;}
break;
}
if (key -> index [key_index] [i] == ' ') {
key_tag [key_index] = tokenize (&(key -> index [key_index] [i]));
/* Convert correct answers to appropriate granularity, collapsing duplicates if they arise */
if (strequal (sense_granularity, COARSE_GRAIN_FLAG))
for (i = 0; key_tag [key_index] [i] != NULL; i++) {
coarse_sense_tag = coarse_sense_tag_for (key_tag [key_index] [i], sense_table);
for (j = 0; j < i; j++)
if (strequal (coarse_sense_tag, key_tag [key_index] [j])) break;
if (j < i) {
for (j = i; key_tag [key_index] [j] != NULL; j++)
key_tag [key_index] [j] = key_tag [key_index] [j + 1];
i--;
}
else
if (!(strequal (coarse_sense_tag, key_tag [key_index] [i])))
key_tag [key_index] [i] = coarse_sense_tag;
}
/* If filter criteria for this instance aren't matched, mark the key to exclude it from scoring */
if (minimal_scoring_desired && key_tag [key_index] [0] != NULL && key_tag [key_index] [1] != NULL)
key_tag [key_index] [0] = NULL;
if (key_tag [key_index] [0] != NULL) instances += 1.0;
/*break;*/
}
}
answer = string_table_for (argv [1]);
for (answer_index = 0; answer_index < answer -> last_entry; answer_index++)
/* Strip off any comments in answers */
{for (i = 0; answer -> index [answer_index] [i] != '\0'; i++)
if (has_prefix (&(answer -> index [answer_index] [i]), COMMENT_DELIMITER))
{answer -> index [answer_index] [i] = '\0';
break;}
/* Re-format the answer's first two fields, which specify the test instance which is being answered */
for (i = 0; answer -> index [answer_index] [i] != '\0'; i++)
if (answer -> index [answer_index] [i] == ' ')
{answer -> index [answer_index] [i] = '_';
while (answer -> index [answer_index] [i] != '\0')
{if (answer -> index [answer_index] [i] == ' ')
{answer -> index [answer_index] [i] = '\0';
i++;
break;}
i++;}
break;}
/* Parse the raw answer, separating out sense tags and their associated weights, if any */
answer_tag = tokenize (&(answer -> index [answer_index] [i]));
i = 0;
while (answer_tag [i] != NULL) i++;
answer_weight = (float *) calloc (i + 1, sizeof (float));
total_weight = 0.0;
invalid_argument_seen = FALSE;
for (i = 0; answer_tag [i] != NULL; i++)
{for (j = 0; answer_tag [i] [j] != '\0'; j++)
if (answer_tag [i] [j] == WEIGHT_DELIMITER) break;
if (answer_tag [i] [j] != WEIGHT_DELIMITER)
invalid_argument_seen = TRUE;
else
{answer_tag [i] [j] = '\0';
answer_weight [i] = atof (&(answer_tag [i] [j + 1]));
total_weight += answer_weight [i];}
/* Discard any answer which contains an unknown sense tag, or a sense tag with a suffix */
/* But don't check if we don't have a sense table */
/*if (sense_table) {
sense_index = search_string_table (answer_tag [i], sense_table);
if (sense_index == NOT_FOUND &&
!(strequal (answer_tag [i], UNASSIGNABLE_SENSE_TAG)) &&
!(strequal (answer_tag [i], TYPO_SENSE_TAG)) &&
!(strequal (answer_tag [i], PROPER_SENSE_TAG))) {
answer_tag [0] = NULL;
break;
}} */
}
/* If not all sense tags have weights, set the weights for all of them to be equal */
if (invalid_argument_seen)
{for (i = 0; answer_tag [i] != NULL; i++) answer_weight [i] = 1.0;
total_weight = 1.0 * i;}
/* Convert answer tags to appropriate granularity, collapsing duplicates and merging their weights */
if (strequal (sense_granularity, COARSE_GRAIN_FLAG))
for (i = 0; answer_tag [i] != NULL; i++)
{coarse_sense_tag = coarse_sense_tag_for (answer_tag [i], sense_table);
for (j = 0; j < i; j++)
if (strequal (coarse_sense_tag, answer_tag [j])) break;
if (j < i)
{answer_weight [j] += answer_weight [i];
for (j = i; answer_tag [j] != NULL; j++)
{answer_tag [j] = answer_tag [j + 1];
answer_weight [j] = answer_weight [j + 1];}
i--;}
else
if (!(strequal (coarse_sense_tag, answer_tag [i])))
answer_tag [i] = coarse_sense_tag;}
/* If weights don't form a probability distribution on sense tags (plus implicit no-guess tag), normalize them */
if (total_weight > 1.0)
{for (i = 0; answer_tag [i] != NULL; i++) answer_weight [i] /= total_weight;
total_weight = 1.0;}
/* Look up the correct answer for this test instance in the key */
key_index = search_string_table (answer -> index [answer_index], key);
/* If there is such a test instance, and it hasn't been scored already or excluded by filter criteria */
if (key_index != NOT_FOUND && answer_tag [0] != NULL && key_tag [key_index] [0] != NULL)
{instances_attempted += total_weight;
score = 0.0;
/* Check each answer sense tag against the correct sense tags in the key */
for (i = 0; answer_tag [i] != NULL; i++)
{subscore = 0.0;
for (j = 0; key_tag [key_index] [j] != NULL; j++)
/* If a tag in the key is equal to the answer tag, give full credit */
{if (strequal (answer_tag [i], key_tag [key_index] [j]))
subscore += 1.0;
else
/* If correct tag subsumes the answer, give full credit under mixed-grain scoring */
if (strequal (sense_granularity, MIXED_GRAIN_FLAG) &&
subsumption_probability_of_tags (key_tag [key_index] [j], answer_tag [i], sense_table) > 0.0)
subscore += 1.0;
else
/* If answer subsumes correct tag, give partial credit under mixed-grain scoring */
if (strequal (sense_granularity, MIXED_GRAIN_FLAG))
subscore += subsumption_probability_of_tags (answer_tag [i], key_tag [key_index] [j], sense_table);}
if (subscore > 1.0) subscore = 1.0;
score += subscore * answer_weight [i];}
if (score > 1.0) score = 1.0;
global_score += score;
if (line_by_line_trace_desired)
{printf ("score for \"%s\": %.3f", key -> index [key_index], score);
newline ();
printf (" key =");
for (i = 0; key_tag [key_index] [i] != NULL; i++)
printf (" %s", key_tag [key_index] [i]);
newline ();
printf (" guess =");
for (i = 0; answer_tag [i] != NULL; i++)
printf (" %s%c%.3f", answer_tag [i], WEIGHT_DELIMITER, answer_weight [i]);
newline ();
newline ();}
/* Reset the key for this test instance so a second answer to this instance won't be counted redundantly */
key_tag [key_index] [0] = NULL;}
else
{fprintf (stderr, "Unable to score answer for \"%s\" (line %ld):", answer -> index [answer_index], answer_index);
if (key_index == NOT_FOUND)
fprintf (stderr, " Does not exist.");
else
if (answer_tag [0] == NULL)
fprintf (stderr, " Invalid answer.");
else
fprintf (stderr, " Excluded or already scored.");
fnewline (stderr);
if (line_by_line_trace_desired)
{printf ("Unable to score answer for \"%s\" (line %ld):", answer -> index [answer_index], answer_index);
if (key_index == NOT_FOUND)
printf (" Does not exist.");
else
if (answer_tag [0] == NULL)
printf (" Invalid answer.");
else
printf (" Excluded or already scored.");
newline ();
newline ();}}
reclaim (answer_tag);
reclaim (answer_weight);}
newline ();
if (strequal (sense_granularity, COARSE_GRAIN_FLAG))
printf ("Coarse-grained");
else
if (strequal (sense_granularity, MIXED_GRAIN_FLAG))
printf ("Mixed-grained");
else
printf ("Fine-grained");
if (minimal_scoring_desired) printf (" minimal");
printf (" score for \"%s\" using key \"%s\":", argv [1], argv [2]);
newline ();
printf (" precision: %.3f", global_score / instances_attempted);
printf (" (%.2f correct of %.2f attempted)", global_score, instances_attempted);
newline ();
printf (" recall: %.3f", global_score / instances);
printf (" (%.2f correct of %.2f in total)", global_score, instances);
newline ();
printf (" attempted: %.2f %%", 100.0 * instances_attempted / instances);
printf (" (%.2f attempted of %.2f in total)", instances_attempted, instances);
newline ();
newline ();
reclaim_string_table (answer);
for (key_index = 0; key_index < key -> last_entry; key_index++) reclaim (key_tag [key_index]);
reclaim (key_tag);
reclaim_string_table (key);
reclaim_string_table (sense_table);
}
}