I recently stumbled across this article on how to write a spelling corrector, and figured I'd try to have a go at it in C (mainly because the link at the end of the page for the C code is broken).
Here is what I would like reviewed:
Accuracy: What can I do to make the program output a more accurate prediction of what it thinks the correct word should be?
Speed: Are there any improvements that could be made to improve runtime speeds?
Refinement: In what ways could I shorten my code down a bit?
Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <search.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define SIZE_DICT 235886
char *dictionary = "/usr/share/dict/words";
const char alphabet[] = "abcdefghijklmnopqrstuvwxyz0123456789";
char *strtolower(char *word)
{
for (char *s = word; *s; ++s) *s = tolower(*s);
return word;
}
ENTRY *find(char *word)
{
return hsearch((ENTRY){.key = word}, FIND);
}
int update(char *word)
{
ENTRY *e = find(word);
if (!e) return 0;
e->data++;
return 1;
}
int readFile(const char* fileName, ENTRY dict)
{
int fd = open(fileName, O_RDONLY);
if (fd < 0) return 0;
struct stat sb;
if (stat(dictionary, &sb)) return 0;
char *result = strdup(mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0));
if (result != MAP_FAILED)
{
char *w = NULL;
char *delimiter = "\n";
char *word = strtok(result, delimiter);
while(word)
{
w = strtolower(strdup(word));
if (!update(w))
{
dict.key = w;
dict.data = 0;
hsearch(dict, ENTER);
}
word = strtok(NULL, delimiter);
}
close(fd);
return 1;
}
return -1;
}
char *substr(char *str, size_t offset, size_t limit)
{
char *new_str;
size_t str_size = strlen(str);
if ((limit > str_size) || ((offset + limit) > str_size) || (str_size < 1) || (limit == 0)) return NULL;
new_str = malloc(limit+1);
if (!new_str) return NULL;
strncpy(new_str, str+offset, limit);
*(new_str + limit) = '\0';
return new_str;
}
char *concat(char *str1, char *str2)
{
if (!str1) {
str1 = malloc(sizeof(char));
*str1 = '\0';
}
if (!str2) {
str2 = malloc(sizeof(char));
*str2 = '\0';
}
str1 = realloc(str1, strlen(str1) + strlen(str2) + 1);
return strcat(str1, str2);
}
int deletion(char *word, char **array, int start_idx)
{
int i = 0;
size_t word_len = strlen(word);
for (; i < word_len; i++)
{
array[i + start_idx] = concat(substr(word, 0, i), substr(word, i+1, word_len-(i+1)));
}
return i;
}
int transposition(char *word, char **array, int start_idx)
{
int i = 0;
size_t word_len = strlen(word);
for (; i < word_len-1; i++)
{
array[i + start_idx] = concat(concat(substr(word, 0, i), substr(word, i+1, 1)), concat(substr(word, i, 1), substr(word, i+2, word_len-(i+2))));
}
return i;
}
int alteration(char *word, char **array, int start_idx)
{
int k = 0;
size_t word_len = strlen(word);
char c[2] = {};
for (int i = 0; i < word_len; ++i)
{
for (int j = 0; j < sizeof(alphabet); ++j, ++k)
{
c[0] = alphabet[j];
array[start_idx + k] = concat(concat(substr(word, 0, i), (char *) &c), substr(word, i+1, word_len - (i+1)));
}
}
return k;
}
int insertion(char *word, char **array, int start_idx)
{
int k = 0;
size_t word_len = strlen(word);
char c[2] = {};
for (int i = 0; i <= word_len; ++i)
{
for (int j = 0; j < sizeof(alphabet); ++j, ++k)
{
c[0] = alphabet[j];
array[start_idx + k] = concat(concat(substr(word, 0, i), (char *) &c), substr(word, i, word_len - i));
}
}
return k;
}
size_t edits1_rows(char *word)
{
size_t size = strlen(word);
return (size) + // deletion
(size - 1) + // transposition
(size * sizeof(alphabet)) + // alteration
(size + 1) * sizeof(alphabet); // insertion
}
char **edits1(char *word)
{
int next_idx;
char **array = malloc(edits1_rows(word) * sizeof(char *));
if (!array) return NULL;
next_idx = deletion(word, array, 0);
next_idx += transposition(word, array, next_idx);
next_idx += alteration(word, array, next_idx);
insertion(word, array, next_idx);
return array;
}
int array_exist(char **array, int rows, char *word)
{
for (int i = 0; i < rows; ++i)
{
if (!strcmp(array[i], word)) return 1;
}
return 0;
}
char **known_edits2(char **array, int rows, int *e2_rows)
{
size_t e1_rows = 0;
int res_size = 0;
char **res = NULL, **e1 = NULL;
for (int i = 0; i < rows; i++)
{
e1 = edits1(array[i]);
e1_rows = edits1_rows(array[i]);
for (int j = 0; j < e1_rows; j++)
{
if (find(e1[j]) && !array_exist(res, res_size, e1[j]))
{
res = realloc(res, sizeof(char *) * (res_size + 1));
res[res_size++] = e1[j];
}
}
}
*e2_rows = res_size;
return res;
}
char *max(char **array, int rows)
{
char *max_word = NULL;
int max_size = 0;
ENTRY *e;
for (int i = 0; i < rows; i++)
{
e = find(array[i]);
if (e && ((int) e->data > max_size))
{
max_size = (int) e->data;
max_word = e->key;
}
}
return max_word;
}
void array_cleanup(char **array, int rows)
{
for (int i = 0; i < rows; i++)
{
free(array[i]);
}
}
char *correct(char *word)
{
char **e1 = NULL;
char **e2 = NULL;
char *e1_word = NULL;
char *e2_word = NULL;
char *res_word = word;
int e1_rows = 0;
char e2_rows = 0;
if (find(word)) return word;
e1_rows = (unsigned)edits1_rows(word);
if (e1_rows)
{
e1 = edits1(word);
e1_word = max(e1, e1_rows);
if (e1_word)
{
array_cleanup(e1, e1_rows);
free(e1);
return e1_word;
}
}
e2 = known_edits2(e1, e1_rows, (int*)&e2_rows);
if (e2_rows)
{
e2_word = max(e2, e2_rows);
if (e2_word)
res_word = e2_word;
}
array_cleanup(e1, e1_rows);
array_cleanup(e2, e2_rows);
free(e1);
free(e2);
return res_word;
}
int main(int argc, char **argv)
{
if (argc != 2)
{
puts("Usage: ./check <word>");
return 1;
}
ENTRY dict = {};
hcreate(SIZE_DICT);
if (!readFile(dictionary, dict)) return -1;
char *corrected_word = correct(argv[argc - 1]);
puts(corrected_word);
}
Test run:
As you can see, the accuracy is somewhat low. The runtime speeds are faster than the original Python program in the post above, but I feel could still be improved upon.
time ./check miror miro real 0m0.118s user 0m0.092s sys 0m0.014s