4
\$\begingroup\$

(See the next iteration.)

Introduction

I was in the mood for some C code and wrote this program for handling a personal telephone record book. One of the goals is strict portability; the code compiles with gcc -ansi -pedantic -Wall ... silently. The second goal is being prepared for dealing with problems (out of memory, file I/O errors).

Usage

The program creates and works on a file called .telephone_book that is placed in the user's home directory.

In order to add new record:

./tbook --add Claus Santa 040-1234567

In order to list all records:

./tbook # list all
./tbook blah # list all records whose last name is closest to "blah" by Levenshtein distance
./tbook - blah # list all records whose first name is closest to "blah" by Levenshtein distance
./tbook bluh blah # list all records whose both last and first names are closest to "bluh blah" by Levenshtein distance

The output may be something like:

----------+------------+------------------+---
Last name | First name | Telephone number | ID
----------+------------+------------------+---
Bro       | Vector     | 11               | 1 
Efremov   | Rodion     | 8055             | 2 
Funk      | Funky      | 12321            | 3 
----------+------------+------------------+---
Gates     | Bill       | 677              | 4 
Minogue   | Kylie      | 4401             | 5 
Ryazanov  | Viktor     | 3454             | 6

In order to delete records by their IDs:

./tbook --remove ID1 ID2 ... IDn

Here are all 5 source files:

telephone_book.h:

#ifndef TELEPHONE_BOOK_H
#define TELEPHONE_BOOK_H

#include <stdio.h>

/*******************************************************************************
* This structure holds a single telephone book record.                         *
*******************************************************************************/
typedef struct {
    char* first_name;
    char* last_name;
    char* telephone_number;
    int id;
} telephone_book_record;

/*******************************************************************************
* This structure defines a linked list node for the telephone book record      *
* list.                                                                        *
*******************************************************************************/
typedef struct telephone_book_record_list_node {
    telephone_book_record* record;
    struct telephone_book_record_list_node* next;
    struct telephone_book_record_list_node* prev;
} telephone_book_record_list_node;

/*******************************************************************************
* This structure holds a doubly-linked list of telephone book records.         *
*******************************************************************************/
typedef struct {
    struct telephone_book_record_list_node* head;
    struct telephone_book_record_list_node* tail;
    int size;
} telephone_book_record_list;




/*******************************************************************************
* Returns the length of the telephone book record list.                        *
*******************************************************************************/
int telephone_book_record_list_size(telephone_book_record_list* list);

/*******************************************************************************
* Allocates and initializes a new telephone book record.                       *
* ---                                                                          *
* Returns a new telephone book record or NULL if something goes wrong.         *
*******************************************************************************/
telephone_book_record* telephone_book_record_alloc(const char* last_name,
                                                   const char* first_name,
                                                   const char* phone_number,
                                                   int id);

/*******************************************************************************
* Frees the memory occupied by the telephone book record: all existing fields  *
* and the actual record.                                                       *
*******************************************************************************/
void telephone_book_record_free(telephone_book_record* record);




/*******************************************************************************
* Allocates and initializes an empty telephone book record list.               *
* ---                                                                          *
* Returns a new empty telephone book record list or NULL if something goes     *
* wrong.                                                                       *
*******************************************************************************/
telephone_book_record_list* telephone_book_record_list_alloc();

/*******************************************************************************
* Appends the argument telephone book record to the tail of the argument       *
* telephone book record list.                                                  *
* ---                                                                          *
* Returns a zero value if the operation was successfull. A non-zero value is   *
* returned if something fails.                                                 *
*******************************************************************************/
int telephone_book_record_list_add_record(telephone_book_record_list* list,
                                          telephone_book_record* record);

/*******************************************************************************
* Removes and returns the telephone book record that has 'id' as its record ID.*
* ---                                                                          *
* Returns NULL if something fails or the list does not contain record with ID  *
* 'id'. Otherwise, a removed record is returned.                               *
*******************************************************************************/
telephone_book_record*
telephone_book_record_list_remove_entry(telephone_book_record_list* list,
                                        int id);

/*******************************************************************************
* Reconstructs the telephone book record list from a file pointed to by the    *
* argument file handle.                                                        *
* ---                                                                          *
* Returns the record list on success, and NULL on failure.                     *
*******************************************************************************/
telephone_book_record_list* telephone_book_record_list_read_from_file(FILE* f);

/*******************************************************************************
* Writes the entire contents of the telephone record list to a specified file  *
* handle.                                                                      *
* ---                                                                          *
* Returns zero on success, and a non-zero value if something fails.            *
*******************************************************************************/
int telephone_book_record_list_write_to_file(telephone_book_record_list* list,
                                             FILE* f);

/*******************************************************************************
* Sorts the telephone records. The last name of each record is the primary     *
* sorting key, and the first name of each record is the secondary sorting key. *
* ---                                                                          *
* Returns zero on success, and a non-zero value if the sorting could not be    *
* completed.                                                                   *
*******************************************************************************/
int telephone_book_record_list_sort(telephone_book_record_list* list);

/*******************************************************************************
* Makes sure that each telephone book record has a unique ID.                  *
* ---                                                                          *
* Returns zero on success, and a non-zero value if something fails.            *
*******************************************************************************/
int telephone_book_record_list_fix_ids(telephone_book_record_list* list);

/*******************************************************************************
* Frees all the memory occupied by the argument telephone book record list.    *
*******************************************************************************/
void telephone_book_record_list_free(telephone_book_record_list* list);

#endif /* TELEPHONE_BOOK_H */

telephone_book.c:

#include "telephone_book.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MAX_RECORD_TOKEN_LENGTH_STRING "64"
#define MAX_RECORD_TOKEN_LENGTH 65

/*******************************************************************************
* Returns the length of the telephone book record list.                        *
*******************************************************************************/
int telephone_book_record_list_size(telephone_book_record_list* list)
{
    return list ? list->size : -1;
}

static telephone_book_record_list_node*
telephone_book_record_list_node_alloc(telephone_book_record* record)
{
    telephone_book_record_list_node* node;

    if (!record)
    {
        return NULL;
    }

    node = malloc(sizeof *node);

    if (!node)
    {
        return NULL;
    }

    node->record = record;
    node->next = NULL;
    node->prev = NULL;

    return node;
}

/*******************************************************************************
* Allocates and initializes a new telephone book record.                       *
* ---                                                                          *
* Returns a new telephone book record or NULL if something goes wrong.         *
*******************************************************************************/
telephone_book_record* telephone_book_record_alloc(const char* last_name,
                                                   const char* first_name,
                                                   const char* phone_number,
                                                   int id)
{
    telephone_book_record* record = malloc(sizeof *record);

    if (!record)
    {
        return NULL;
    }

    record->last_name        = malloc(sizeof(char) * (strlen(last_name) + 1));
    record->first_name       = malloc(sizeof(char) * (strlen(first_name) + 1));
    record->telephone_number = malloc(sizeof(char) *
                                      (strlen(phone_number) + 1));
    record->id = id;

    strcpy(record->last_name, last_name);
    strcpy(record->first_name, first_name);
    strcpy(record->telephone_number, phone_number);

    return record;
}

/*******************************************************************************
* Frees the memory occupied by the telephone book record: all existing fields  *
* and the actual record.                                                       *
*******************************************************************************/
void telephone_book_record_free(telephone_book_record* record)
{
    if (!record)
    {
        return;
    }

    if (record->first_name)
    {
        free(record->first_name);
    }

    if (record->last_name)
    {
        free(record->last_name);
    }

    if (record->telephone_number)
    {
        free(record->telephone_number);
    }

    free(record);
}

/*******************************************************************************
* Allocates and initializes an empty telephone book record list.               *
* ---                                                                          *
* Returns a new empty telephone book record list or NULL if something goes     *
* wrong.                                                                       *
*******************************************************************************/
telephone_book_record_list* telephone_book_record_list_alloc()
{
    telephone_book_record_list* record_list = malloc(sizeof *record_list);

    if (!record_list)
    {
        return NULL;
    }

    record_list->head = NULL;
    record_list->tail = NULL;
    record_list->size = 0;
    return record_list;
}

/*******************************************************************************
* Appends the argument telephone book record to the tail of the argument       *
* telephone book record list.                                                  *
* ---                                                                          *
* Returns a zero value if the operation was successfull. A non-zero value is   *
* returned if something fails.                                                 *
*******************************************************************************/
int telephone_book_record_list_add_record(telephone_book_record_list* list,
                                          telephone_book_record* record)
{
    telephone_book_record_list_node* new_node;

    if (!list || !record)
    {
        return 1;
    }

    new_node = telephone_book_record_list_node_alloc(record);

    if (!new_node)
    {
        return 1;
    }

    if (list->head)
    {
        new_node->prev = list->tail;
        list->tail->next = new_node;
    }
    else
    {
        list->head = new_node;
    }

    list->tail = new_node;
    list->size++;
    return 0;
}

/*******************************************************************************
* Removes a telephone book record that has 'id' as its record ID.              *
* ---                                                                          *
* On error or ID mismatch returns NULL. Otherwise the record with the same ID  *
* (which is removed) is returned.                                              *
*******************************************************************************/
telephone_book_record*
telephone_book_record_list_remove_entry(telephone_book_record_list* list,
                                        int id)
{
    telephone_book_record_list_node* current_node;
    telephone_book_record_list_node* next_node;
    telephone_book_record* removed_record;

    if (!list)
    {
        return NULL;
    }

    current_node = list->head;

    while (current_node)
    {
        next_node = current_node->next;

        /* Since the program makes sure that all IDs are unique, we can */
        /* return as soon as we remove the first match. */
        if (current_node->record->id == id)
        {
            if (current_node->prev)
            {
                current_node->prev->next = current_node->next;
            }
            else
            {
                list->head = current_node->next;
            }

            if (current_node->next)
            {
                current_node->next->prev = current_node->prev;
            }
            else
            {
                list->tail = current_node->prev;
            }

            removed_record = current_node->record;
            free(current_node);
            return removed_record;
        }

        current_node = next_node;
    }

    return NULL;
}

/*******************************************************************************
* Reconstructs the telephone book record list from a file pointed to by the    *
* argument file handle.                                                        *
* ---                                                                          *
* Returns the record list on success, and NULL on failure.                     *
*******************************************************************************/
telephone_book_record_list* telephone_book_record_list_read_from_file(FILE* f)
{
    telephone_book_record_list* record_list;
    telephone_book_record* current_record;

    char last_name_token   [MAX_RECORD_TOKEN_LENGTH];
    char first_name_token  [MAX_RECORD_TOKEN_LENGTH];
    char phone_number_token[MAX_RECORD_TOKEN_LENGTH];
    int  id_holder;
    int read_result;

    if (!f)
    {
        return NULL;
    }

    record_list = telephone_book_record_list_alloc();

    if (!record_list)
    {
        return NULL;
    }

    while (!feof(f) && !ferror(f))
    {
        read_result = fscanf(f,
                             "%" MAX_RECORD_TOKEN_LENGTH_STRING "s\
                             %"  MAX_RECORD_TOKEN_LENGTH_STRING "s\
                             %"  MAX_RECORD_TOKEN_LENGTH_STRING "s\
                             %d\n",
                             last_name_token,
                             first_name_token,
                             phone_number_token,
                             &id_holder);

        if (read_result == 4)
        {
            current_record = telephone_book_record_alloc(last_name_token,
                                                         first_name_token,
                                                         phone_number_token,
                                                         id_holder);
            if (!current_record)
            {
                fputs("ERROR: Cannot allocate memory for a telephone book "
                      "record.", stderr);
                telephone_book_record_list_free(record_list);
                return NULL;
            }

            telephone_book_record_list_add_record(record_list, current_record);
        }
    }

    return record_list;
}

/*******************************************************************************
* Writes the entire contents of the telephone record list to a specified file  *
* handle.                                                                      *
* ---                                                                          *
* Returns zero on success, and a non-zero value if something fails.            *
*******************************************************************************/
int telephone_book_record_list_write_to_file(telephone_book_record_list* list,
                                             FILE* f)
{
    telephone_book_record_list_node* current_node;

    if (!list || !f)
    {
        return 1;
    }

    if (list->size == 0)
    {
        return 0;
    }

    current_node = list->head;

    while (current_node)
    {
        fprintf(f,
                "%s %s %s %d\n",
                current_node->record->last_name,
                current_node->record->first_name,
                current_node->record->telephone_number,
                current_node->record->id);

        current_node = current_node->next;
    }

    return 0;
}

/*******************************************************************************
* Defines a telephone book record comparator. The last name of a record is the *
* primary sorting key, and the first name is the secondary sorting key.        *
* ---
* Returns zero, if both the input records have the same last and first name.   *
* If the first entry should precede the second, a negative value is returned.  *
* If the second entry should precede the first, a positive value is returned.  *
*******************************************************************************/
static int record_cmp(const void* pa, const void* pb)
{
    int c;

    telephone_book_record_list_node* a =
    *(telephone_book_record_list_node *const *) pa;

    telephone_book_record_list_node* b =
    *(telephone_book_record_list_node *const *) pb;

    c = strcmp(a->record->last_name, b->record->last_name);

    if (c)
    {
        return c;
    }

    return strcmp(a->record->first_name, b->record->first_name);
}

/*******************************************************************************
* Sorts the telephone records. The last name of each record is the primary     *
* sorting key, and the first name of each record is the secondary sorting key. *
* ---                                                                          *
* Returns zero on success, and a non-zero value if the sorting could not be    *
* completed.                                                                   *
*******************************************************************************/
int telephone_book_record_list_sort(telephone_book_record_list* list)
{
    telephone_book_record_list_node** array;
    telephone_book_record_list_node* current_node;

    int list_length;
    int index;

    if (!list)
    {
        return 1;
    }

    list_length = telephone_book_record_list_size(list);

    if (list_length == 0)
    {
        /* Nothing to sort. */
        return 0;
    }

    array = malloc(list_length * sizeof *array);

    if (!array)
    {
        return 1;
    }

    current_node = list->head;

    for (index = 0;
         index < list_length;
         index++, current_node = current_node->next)
    {
        array[index] = current_node;
    }

    qsort(array, list_length, sizeof *array, record_cmp);

    /* Relink the nodes: */
    list->head = array[0];
    list->tail = array[list_length - 1];

    list->head->prev = NULL;
    list->tail->next = NULL;

    for (index = 0; index < list_length - 1; ++index)
    {
        array[index]->next = array[index + 1];
    }

    for (index = 1; index < list_length; ++index)
    {
        array[index]->prev = array[index - 1];
    }

    /* Freeing memory! */
    free(array);
    return 0;
}

/*******************************************************************************
* Makes sure that each telephone book record has an unique ID.                 *
* ---                                                                          *
* Returns zero on success, and a non-zero value if something fails.            *
*******************************************************************************/
int telephone_book_record_list_fix_ids(telephone_book_record_list* list)
{
    int id;
    telephone_book_record_list_node* current_node;

    if (!list)
    {
        return 1;
    }

    id = 0;
    current_node = list->head;

    while (current_node)
    {
        current_node->record->id = ++id;
        current_node = current_node->next;
    }

    return 0;
}

/*******************************************************************************
* Frees all the memory occupied by the argument telephone book record list.    *
*******************************************************************************/
void telephone_book_record_list_free(telephone_book_record_list* list)
{
    telephone_book_record_list_node* current_node;
    telephone_book_record_list_node* next_node;

    if (!list)
    {
        return;
    }

    current_node = list->head;

    while (current_node)
    {
        next_node = current_node->next;
        telephone_book_record_free(current_node->record);
        free(current_node);
        current_node = next_node;
    }

    free(list);
}

telephone_book_utils.h:

#ifndef TELEPHONE_BOOK_UTILS_H
#define TELEPHONE_BOOK_UTILS_H

#include <stdio.h>
#include "telephone_book.h"

/*******************************************************************************
* This structures holds the string required for neat result output.            *
*******************************************************************************/
typedef struct {
    char* title_string;
    char* separator_string;
    char* record_format_string;
} output_table_strings;

/*******************************************************************************
* Returns the character representing a path separator character ('/' or '\')   *
* used on the current platform.                                                *
*******************************************************************************/
char separator();

/*******************************************************************************
* Returns a C string representing the full path to the telephone book record   *
* file.                                                                        *
*******************************************************************************/
char* get_telephone_record_book_file_path();

/*******************************************************************************
* Creates and returns all format strings for printing the record list.         *
*******************************************************************************/
output_table_strings*
output_table_strings_create(telephone_book_record_list* list);

/*******************************************************************************
* Creates and returns a structure containing all format strings necessary for  *
* printing the telephone book record list.                                     *
*******************************************************************************/
void output_table_strings_free(output_table_strings* output_table_strs);

/*******************************************************************************
* Returns the format string for nifty printing the removed records.            *
*******************************************************************************/
char* get_removed_record_output_format_string(telephone_book_record_list* list);

#endif /* TELEPHONE_BOOK_UTILS_H */

telephone_book_utils.c:

#include "telephone_book_utils.h"
#include "telephone_book.h"
#include <stdlib.h>
#include <string.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

const char* TELEPHONE_RECORD_BOOK_FILE_NAME = ".telephone_book";

static const char* TITLE_LAST_NAME          = "Last name";
static const char* TITLE_FIRST_NAME         = "First name";
static const char* TITLE_TELEPHONE_NUMBER   = "Telephone number";
static const char* TITLE_CONTACT_ID         = "ID";

static const size_t ID_HOLDER_STRING_CAPACITY = 40;
static const size_t FORMAT_STRING_CAPACITY    = 100;

/*******************************************************************************
* Returns the character representing a path separator character ('/' or '\')   *
* used on the current platform.                                                *
*******************************************************************************/
char separator()
{
#ifdef _WIN32
    return '\\';
#else
    return '/';
#endif
}

/*******************************************************************************
* Returns a C string representing the full path to the telephone book record   *
* file.                                                                        *
*******************************************************************************/
char* get_telephone_record_book_file_path()
{
    char* home_directory;
    char* telephone_record_book_file_path;
    size_t home_directory_name_length;

    home_directory = getenv("HOME");

    if (!home_directory)
    {
        return NULL;
    }

    home_directory_name_length = strlen(home_directory);

    /* Allocate the space for the entire */
    telephone_record_book_file_path =
    malloc(sizeof(char)
            * (home_directory_name_length
               + 2 + strlen(TELEPHONE_RECORD_BOOK_FILE_NAME)));

    if (!telephone_record_book_file_path)
    {
        return NULL;
    }

    strcpy(telephone_record_book_file_path, home_directory);
    telephone_record_book_file_path[home_directory_name_length] = separator();
    strcpy(&telephone_record_book_file_path[home_directory_name_length + 1],
           TELEPHONE_RECORD_BOOK_FILE_NAME);

    return telephone_record_book_file_path;
}



/*******************************************************************************
* Allocates and sets a row separator string.                                   *
*******************************************************************************/
char* load_separator_string(size_t max_last_name_token_length,
                            size_t max_first_name_token_length,
                            size_t max_telephone_number_token_length,
                            size_t max_telephone_contact_id_length)
{
    size_t index;
    size_t i;
    char* separator_string =
    malloc(sizeof(char) *
           (max_last_name_token_length + 1) +
           (max_first_name_token_length + 2) +
           (max_telephone_number_token_length + 2) +
           (max_telephone_contact_id_length + 1) + 4);

    if (!separator_string)
    {
        fputs("ERROR: Cannot allocate memory for the row separator string.",
              stderr);
        return NULL;
    }

    for (i = 0; i != max_last_name_token_length + 1; ++i)
    {
        separator_string[i] = '-';
    }

    separator_string[max_last_name_token_length + 1] = '+';

    index = max_last_name_token_length + 2;

    for (i = 0; i != max_first_name_token_length + 2; ++i)
    {
        separator_string[index++] = '-';
    }

    separator_string[index++] = '+';

    for (i = 0; i != max_telephone_number_token_length + 2; ++i)
    {
        separator_string[index++] = '-';
    }

    separator_string[index++] = '+';

    for (i = 0; i != max_telephone_contact_id_length + 1; ++i)
    {
        separator_string[index++] = '-';
    }

    separator_string[index++] = '\0';
    return separator_string;
}

/*******************************************************************************
* Creates and returns a structure containing all format strings necessary for  *
* printing the telephone book record list.                                     *
*******************************************************************************/
output_table_strings*
output_table_strings_create(telephone_book_record_list* list)
{
    size_t max_last_name_token_length        = strlen(TITLE_LAST_NAME);
    size_t max_first_name_token_length       = strlen(TITLE_FIRST_NAME);
    size_t max_telephone_number_token_length = strlen(TITLE_TELEPHONE_NUMBER);
    size_t max_telephone_contact_id_length   = strlen(TITLE_CONTACT_ID);

    size_t last_name_token_length;
    size_t first_name_token_length;
    size_t telephone_number_token_length;
    size_t telephone_contact_id_length;

    output_table_strings* output_table;

    char* record_format_string;
    char* title_string;
    char* separator_string;
    char* id_holder_string;
    char* title_string_format;

    telephone_book_record_list_node* current_node;
    telephone_book_record* current_record;

    if (!list)
    {
        return NULL;
    }

    /* ALLOCATED: output_table */
    output_table = malloc(sizeof *output_table);

    if (!output_table)
    {
        fputs("ERROR: Cannot allocate memory for the output format structures.",
              stderr);
        return NULL;
    }

    /* ALLOCATED: output_table, id_holder_string */
    id_holder_string = malloc(ID_HOLDER_STRING_CAPACITY);

    if (!id_holder_string)
    {
        fputs("ERROR: Cannot allocate memory for the ID field holder.", stderr);
        free(output_table);
        return NULL;
    }

    current_node = list->head;

    while (current_node)
    {
        current_record = current_node->record;

        last_name_token_length  = strlen(current_record->last_name);
        first_name_token_length = strlen(current_record->first_name);
        telephone_number_token_length =
        strlen(current_record->telephone_number);

        sprintf(id_holder_string, "%d", current_record->id);
        telephone_contact_id_length = strlen(id_holder_string);

        max_last_name_token_length = MAX(max_last_name_token_length,
                                         last_name_token_length);

        max_first_name_token_length = MAX(max_first_name_token_length,
                                          first_name_token_length);

        max_telephone_number_token_length =
            MAX(max_telephone_number_token_length,
                telephone_number_token_length);

        max_telephone_contact_id_length = MAX(max_telephone_contact_id_length,
                                              telephone_contact_id_length);

        current_node = current_node->next;
    }

    /* ALLOCATED: output_table */
    free(id_holder_string);

    /* ALLOCATED: output_table, record_format_string */
    record_format_string = malloc(sizeof(char) * FORMAT_STRING_CAPACITY);

    if (!record_format_string)
    {
        fputs("ERROR: Cannot allocate memory for the format string.", stderr);
        free(output_table);
        return NULL;
    }

    sprintf(record_format_string,
            "%%-%zus | %%-%zus | %%-%zus | %%-%zuzu\n",
            max_last_name_token_length,
            max_first_name_token_length,
            max_telephone_number_token_length,
            max_telephone_contact_id_length);

    /* @ALLOC: output_table, record_format_string, title_string */
    title_string =
        malloc(sizeof(char) *
               (max_last_name_token_length + 1) +
               (max_first_name_token_length + 2) +
               (max_telephone_number_token_length + 2) +
               (max_telephone_contact_id_length + 1) + 4);

    if (!title_string)
    {
        fputs("ERROR: Cannot allocate memory for the title string.", stderr);
        free(output_table);
        free(record_format_string);
        return NULL;
    }

    /* ALLOCATED: output_table, record_format_string,
                  title_string, title_string_format */
    title_string_format = malloc(sizeof(char) * FORMAT_STRING_CAPACITY);

    if (!title_string_format)
    {
        fputs("ERROR: Cannot allocate memory for the title string format.",
              stderr);
        free(output_table);
        free(record_format_string);
        free(title_string);
        return NULL;
    }

    sprintf(title_string_format,
            "%%-%zus | %%-%zus | %%-%zus | %%-%zus",
            max_last_name_token_length,
            max_first_name_token_length,
            max_telephone_number_token_length,
            max_telephone_contact_id_length);

    sprintf(title_string,
            title_string_format,
            TITLE_LAST_NAME,
            TITLE_FIRST_NAME,
            TITLE_TELEPHONE_NUMBER,
            TITLE_CONTACT_ID);

    /* ALLOCATED: output_table, record_format_string, title_string */
    free(title_string_format);

    separator_string =
        load_separator_string(max_last_name_token_length,
                              max_first_name_token_length,
                              max_telephone_number_token_length,
                              max_telephone_contact_id_length);

    if (!separator_string)
    {
        fputs("ERROR: Cannot allocate memory for the separator string.",
              stderr);
        free(output_table);
        free(record_format_string);
        free(title_string);
        return NULL;
    }

    output_table->title_string = title_string;
    output_table->separator_string = separator_string;
    output_table->record_format_string = record_format_string;

    return output_table;
}

/*******************************************************************************
* Frees all the memory alloated by the output table format strings.            *
*******************************************************************************/
void output_table_strings_free(output_table_strings* output_table_strs)
{
    if (!output_table_strs)
    {
        return;
    }

    if (output_table_strs->record_format_string)
    {
        free(output_table_strs->record_format_string);
    }

    if (output_table_strs->separator_string)
    {
        free(output_table_strs->separator_string);
    }

    if (output_table_strs->title_string)
    {
        free(output_table_strs->title_string);
    }

    free(output_table_strs);
}

/*******************************************************************************
* Returns the format string for nifty printing the removed records.            *
*******************************************************************************/
char* get_removed_record_output_format_string(telephone_book_record_list* list)
{
    size_t max_last_name_token_length        = 0;
    size_t max_first_name_token_length       = 0;
    size_t max_telephone_number_token_length = 0;
    size_t max_telephone_contact_id_length   = 0;

    size_t last_name_token_length;
    size_t first_name_token_length;
    size_t telephone_number_token_length;
    size_t telephone_contact_id_length;

    /* ALLOCATED: format_string */
    char* format_string = malloc(sizeof(char) * FORMAT_STRING_CAPACITY);
    char* id_holder_string;

    telephone_book_record_list_node* current_node;
    telephone_book_record* current_record;

    if (!format_string)
    {
        fputs("ERROR: Cannot allocate memory for the format string.", stderr);
        return NULL;
    }

    /* ALLOCATED: format_string, id_holder_string */
    id_holder_string = malloc(ID_HOLDER_STRING_CAPACITY);

    if (!id_holder_string)
    {
        fputs("ERROR: Cannot allocate memory for the ID field holder.", stderr);
        return NULL;
    }

    current_node = list->head;

    while (current_node)
    {
        current_record = current_node->record;

        last_name_token_length  = strlen(current_record->last_name);
        first_name_token_length = strlen(current_record->first_name);
        telephone_number_token_length =
                                  strlen(current_record->telephone_number);

        sprintf(id_holder_string, "%d", current_record->id);
        telephone_contact_id_length = strlen(id_holder_string);

        max_last_name_token_length = MAX(max_last_name_token_length,
                                         last_name_token_length);

        max_first_name_token_length = MAX(max_first_name_token_length,
                                          first_name_token_length);

        max_telephone_number_token_length =
        MAX(max_telephone_number_token_length,
            telephone_number_token_length);

        max_telephone_contact_id_length = MAX(max_telephone_contact_id_length,
                                              telephone_contact_id_length);
        current_node = current_node->next;
    }

    sprintf(format_string,
            "%%-%zus %%-%zus %%-%zus %%-%zuzu\n",
            max_last_name_token_length,
            max_first_name_token_length,
            max_telephone_number_token_length,
            max_telephone_contact_id_length);

    /* ALLOCATED: format_string */
    free(id_holder_string);
    return format_string;
}

main.c:

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "telephone_book.h"
#include "telephone_book_utils.h"

static const char* OPTION_ADD_SHORT = "-a";
static const char* OPTION_ADD_LONG  = "--add";

static const char* OPTION_REMOVE_SHORT = "-r";
static const char* OPTION_REMOVE_LONG  = "--remove";

static const char* OPTION_HELP_SHORT = "-h";
static const char* OPTION_HELP_LONG  = "--help";

static const size_t RECORDS_PER_BLOCK = 3;

/*******************************************************************************
* Returns the smallest of 'a', 'b', and 'c'.                                   *
*******************************************************************************/
static size_t min3(size_t a, size_t b, size_t c)
{
    if (a < b)
    {
        if (c < a)
        {
            return c;
        }

        return a;
    }

    /* b <= a */
    if (c < b)
    {
        return c;
    }

    return b;
}

/*******************************************************************************
* Prints the help message to the standard output.                              *
*******************************************************************************/
static void print_help(char* executable_name)
{
    char path_separator = separator();
    int executable_name_length = (int) strlen(executable_name);
    int i;

    /* Doing my own basename for the sake of portability. */
    for (i = executable_name_length - 1; i >= 0; --i)
    {
        if (executable_name[i] == path_separator)
        {
            executable_name = &executable_name[i + 1];
            break;
        }
    }

    printf("Usage: %s -a    LAST FIRST NUMBER\n", executable_name);
    printf("       %s --add LAST FIRST NUMBER\n", executable_name);
    puts("");
    printf("       %s -r       ID1 ID2 ... IDn\n", executable_name);
    printf("       %s --remove ID1 ID2 ... IDn\n", executable_name);
    puts("");
    printf("(1)    %s\n",                      executable_name);
    printf("(2)    %s LAST_EXPR\n",            executable_name);
    printf("(3)    %s - FIRST_EXPR\n",         executable_name);
    printf("(4)    %s LAST_EXPR FIRST_EXPR\n", executable_name);
    puts("");
    puts("Where: -a or --add for adding one new book entry.");
    puts("       -r or --remove for removing book entries by their IDs.");
    puts("");
    puts("(1) List all book entries in order.");
    puts("(2) Match by last name and list the closest book entries.");
    puts("(3) Match by first name and list the closest book entries.");
    puts("(4) Match by both last and first names and list the closest book "
         "entries");
}

/*******************************************************************************
* Implements the Levenshtein distance algorithm.                               *
*******************************************************************************/
static size_t edit_distance(char* word1,
                            char* word2,
                            size_t length1,
                            size_t length2)
{
    int cost;

    if (length1 == 0)
    {
        return length2;
    }

    if (length2 == 0)
    {
        return length1;
    }

    cost = tolower(word1[length1 - 1]) ==
           tolower(word2[length2 - 1]) ? 0 : 1;

    return min3(edit_distance(word1, word2, length1, length2 - 1) + 1,
                edit_distance(word1, word2, length1 - 1, length2) + 1,
                edit_distance(word1, word2, length1 - 1, length2 - 1) + cost);
}

/*******************************************************************************
* Computes the Levenshtein disance between words 'word1' and 'word2'.          *
*******************************************************************************/
static size_t compute_edit_distance(char* word1, char* word2)
{
    return edit_distance(word1, word2, strlen(word1), strlen(word2));
}

/*******************************************************************************
* Implements listing the telephone book records.                               *
*******************************************************************************/
int command_list_telephone_book_records_impl(
                        telephone_book_record_list* record_list,
                        char* last_name,
                        char* first_name)
{
    size_t best_tentative_distance = 1000 * 1000 * 1000;
    size_t temp_distance;
    size_t i;
    output_table_strings* output_strings;
    telephone_book_record* record;
    telephone_book_record_list_node* current_node;
    telephone_book_record_list* best_record_list =
        telephone_book_record_list_alloc();

    /* ALLOCATED: best_record_list */
    if (!best_record_list)
    {
        fputs("ERROR: Cannot allocate the best record list.", stderr);
        return EXIT_FAILURE;
    }

    current_node = record_list->head;

    while (current_node)
    {
        temp_distance =
        (last_name ?
            compute_edit_distance(last_name,
                                  current_node->record->last_name) : 0) +
        (first_name ?
            compute_edit_distance(first_name,
                                  current_node->record->first_name) : 0);

        if (best_tentative_distance > temp_distance)
        {
            /* 'temp_distance' improves the best known edit distance,     */
            /* clear the current best list and append the current record: */
            telephone_book_record_list_free(best_record_list);
            best_record_list = telephone_book_record_list_alloc();

            if (!best_record_list)
            {
                fputs("ERROR: Cannot allocate new best record list.", stderr);
                return EXIT_FAILURE;
            }

            record = telephone_book_record_alloc(
                            current_node->record->last_name,
                            current_node->record->first_name,
                            current_node->record->telephone_number,
                            current_node->record->id);

            if (!record)
            {
                fputs("ERROR: Cannot allocate a copy record.", stderr);
                telephone_book_record_list_free(best_record_list);
                return EXIT_FAILURE;
            }

            if (telephone_book_record_list_add_record(best_record_list, record))
            {
                fputs("ERROR: Cannot add a new record to the best list.",
                      stderr);
                telephone_book_record_list_free(best_record_list);
                telephone_book_record_free(record);
                return EXIT_FAILURE;
            }

            best_tentative_distance = temp_distance;
        }
        else if (best_tentative_distance == temp_distance)
        {
            /* Append the current record to the best list: */
            record = telephone_book_record_alloc(
                            current_node->record->last_name,
                            current_node->record->first_name,
                            current_node->record->telephone_number,
                            current_node->record->id);

            if (!record)
            {
                fputs("ERROR: Cannot allocate a copy record.", stderr);
                telephone_book_record_list_free(best_record_list);
                return EXIT_FAILURE;
            }


            if (telephone_book_record_list_add_record(best_record_list, record))
            {
                fputs("ERROR: Cannot add a new record to the best list.",
                      stderr);
                telephone_book_record_list_free(best_record_list);
                telephone_book_record_free(record);
                return EXIT_FAILURE;
            }
        }

        current_node = current_node->next;
    }

    output_strings = output_table_strings_create(best_record_list);

    if (!output_strings)
    {
        telephone_book_record_list_free(best_record_list);
        return EXIT_FAILURE;
    }

    puts(output_strings->separator_string);
    puts(output_strings->title_string);
    puts(output_strings->separator_string);

    current_node = best_record_list->head;
    i = 0;

    while (current_node)
    {
        printf(output_strings->record_format_string,
               current_node->record->last_name,
               current_node->record->first_name,
               current_node->record->telephone_number,
               current_node->record->id);

        current_node = current_node->next;
        ++i;

        if (current_node && i % RECORDS_PER_BLOCK == 0)
        {
            puts(output_strings->separator_string);
        }
    }

    output_table_strings_free(output_strings);
    telephone_book_record_list_free(best_record_list);
    return EXIT_SUCCESS;
}

/*******************************************************************************
* Handles the command for listing the records.                                 *
*******************************************************************************/
static int command_list_telephone_book_records(int argc, char* argv[])
{
    char* file_name;
    FILE* f;
    telephone_book_record_list* record_list;
    char* last_name;
    char* first_name;

    /* ALLOCATED: file_name */
    file_name = get_telephone_record_book_file_path();

    if (!file_name)
    {
        fputs("ERROR: Cannot allocate memory for the telephone book file name.",
              stderr);
        return EXIT_FAILURE;
    }

    f = fopen(get_telephone_record_book_file_path(), "r");

    if (!f)
    {
        fprintf(stderr,
                "ERROR: Cannot open the record book file '%s'.\n",
                file_name);

        free(file_name);
        return EXIT_FAILURE;
    }

    /* ALLOCATED: file_name, record_list */
    record_list = telephone_book_record_list_read_from_file(f);
    fclose(f);

    if (!record_list)
    {
        fputs("ERROR: Cannot read the record book file.", stderr);
        free(file_name);
        return EXIT_FAILURE;
    }

    /* If fails, silently ignore: */
    telephone_book_record_list_sort(record_list);
    /* Does not ask for resources, should be OK: */
    telephone_book_record_list_fix_ids(record_list);

    /* "w" means overwrite the file. */
    f = fopen(file_name, "w");

    if (f)
    {
        /* Write the file back. It will update the order of the records and */
        /* fix the record IDs, if needed. */
        telephone_book_record_list_write_to_file(record_list, f);
        fclose(f);
    }

    last_name  = argc >= 2 ? argv[1] : NULL;
    first_name = argc >= 3 ? argv[2] : NULL;

    if (argc > 1 && strcmp(argv[1], "-") == 0)
    {
        /* Match all last names: */
        last_name = NULL;
    }

    /* ALLOCATED: record_list */
    free(file_name);

    return command_list_telephone_book_records_impl(record_list,
                                                    last_name,
                                                    first_name);
}

/*******************************************************************************
* Handles the command for adding a new record.                                 *
*******************************************************************************/
static int command_add_record(int argc, char* argv[])
{
    char* file_name;
    FILE* f;
    telephone_book_record_list* record_list;
    telephone_book_record* record;

    if (argc != 5)
    {
        print_help(argv[0]);
        return EXIT_FAILURE;
    }

    /* ALLOCATED: file_name */
    file_name = get_telephone_record_book_file_path();

    if (!file_name)
    {
        fputs("ERROR: Cannot allocate memory for the telephone book file name.",
              stderr);
        return EXIT_FAILURE;
    }

    f = fopen(file_name, "r");

    if (!f)
    {
        fprintf(stderr,
                "ERROR: Cannot open the record book file '%s'.\n",
                file_name);

        free(file_name);
        return EXIT_FAILURE;
    }

    /* ALLOCATED: file_name, record_list */
    record_list = telephone_book_record_list_read_from_file(f);
    fclose(f);

    if (!record_list)
    {
        fputs("ERROR: Cannot read the record book file.", stderr);
        free(file_name);
        return EXIT_FAILURE;
    }

    /* ALLOCATED: file_name, record_list, record */
    record = telephone_book_record_alloc(argv[2], argv[3], argv[4], -1);

    if (!record)
    {
        fputs("ERROR: Cannot allocate memory for the new record.", stderr);
        free(file_name);
        telephone_book_record_list_free(record_list);
        return EXIT_FAILURE;
    }

    if (telephone_book_record_list_add_record(record_list, record))
    {
        fputs("ERROR: Cannot add the new entry to the record book.", stderr);
        free(file_name);
        telephone_book_record_list_free(record_list);
        telephone_book_record_free(record);
        return EXIT_FAILURE;
    }

    /* Even if the following two functions fail, proceed further. */
    telephone_book_record_list_sort(record_list);
    telephone_book_record_list_fix_ids(record_list);

    f = fopen(file_name, "w");
    free(file_name);

    if (!f)
    {
        fputs("ERROR: Cannot open the record book file.", stderr);
        /* 'record' is contained in 'record_list' so is freed by it: */
        telephone_book_record_list_free(record_list);
        return EXIT_FAILURE;
    }

    if (telephone_book_record_list_write_to_file(record_list, f))
    {
        fputs("ERROR: Cannot update the record book file.", stderr);
    }

    fclose(f);
    /* 'record' is contained in 'record_list' so is freed by it: */
    telephone_book_record_list_free(record_list);
    return EXIT_SUCCESS;
}

/*******************************************************************************
* Handles the commmand for removing records by their IDs.                      *
*******************************************************************************/
static int command_remove_records(int argc, char* argv[])
{
    char* file_name;
    FILE* f;
    telephone_book_record_list* record_list;
    telephone_book_record_list* removed_record_list;
    telephone_book_record* removed_record;
    telephone_book_record_list_node* current_node;
    char* removed_record_format;
    int arg_index;
    int id;

    if (argc < 3)
    {
        puts("Warning: no record IDs given. Nothing to remove.");
        return EXIT_SUCCESS;
    }

    /* ALLOCATED: file_name */
    file_name = get_telephone_record_book_file_path();

    if (!file_name)
    {
        fputs("ERROR: Cannot allocate memory for the telephone book file name.",
              stderr);
        return EXIT_FAILURE;
    }

    f = fopen(file_name, "r");

    if (!f)
    {
        fprintf(stderr,
                "ERROR: Cannot open the record book file '%s'.\n",
                file_name);

        free(file_name);
        return EXIT_FAILURE;
    }

    /* ALLOCATED: file_name, record_list */
    record_list = telephone_book_record_list_read_from_file(f);
    fclose(f);

    if (!record_list)
    {
        fputs("ERROR: Cannot read the record book file.", stderr);
        free(file_name);
        return EXIT_FAILURE;
    }

    /* ALLOCATED: file_name, record_list, removed_record_list */
    removed_record_list = telephone_book_record_list_alloc();

    if (!removed_record_list)
    {
        fputs("ERROR: Cannot allocate memory for the list of removed records.",
              stderr);
        free(file_name);
        telephone_book_record_list_free(record_list);
        return EXIT_FAILURE;
    }

    /* Even if the following two functions fail, proceed further. */
    telephone_book_record_list_sort(record_list);
    telephone_book_record_list_fix_ids(record_list);

    f = fopen(file_name, "w");

    /* ALLOCATED: record_list, removed_record_list */
    free(file_name); /* We do not need 'file_name' anymore. */

    if (!f)
    {
        fputs("ERROR: Cannot open the record book file for writing.", stderr);
        telephone_book_record_list_free(record_list);
        telephone_book_record_list_free(removed_record_list);
        return EXIT_FAILURE;
    }

    for (arg_index = 2; arg_index < argc; ++arg_index)
    {
        sscanf(argv[arg_index], "%d", &id);
        removed_record = telephone_book_record_list_remove_entry(record_list,
                                                                 id);

        if (removed_record)
        {
            telephone_book_record_list_add_record(removed_record_list,
                                                  removed_record);
        }
    }

    if (telephone_book_record_list_write_to_file(record_list, f))
    {
        fputs("ERROR: Cannot update the record book file.", stderr);
        telephone_book_record_list_free(record_list);
        telephone_book_record_list_free(removed_record_list);
        fclose(f);
        return EXIT_FAILURE;
    }

    fclose(f);

    printf("Number of records to remove: %d, removed: %d.\n",
           argc - 2,
           telephone_book_record_list_size(removed_record_list));
    puts("List of removed entries:");
    current_node = removed_record_list->head;
    removed_record_format =
        get_removed_record_output_format_string(removed_record_list);

    while (current_node)
    {
        if (removed_record_format)
        {
            printf(removed_record_format,
                   current_node->record->last_name,
                   current_node->record->first_name,
                   current_node->record->telephone_number,
                   current_node->record->id);
        }
        else
        {
            /* Fallback format output: */
            printf("%s, %s - %s, ID %d\n",
                   current_node->record->last_name,
                   current_node->record->first_name,
                   current_node->record->telephone_number,
                   current_node->record->id);
        }

        current_node = current_node->next;
    }

    if (removed_record_format)
    {
        free(removed_record_format);
    }

    telephone_book_record_list_free(record_list);
    telephone_book_record_list_free(removed_record_list);
    return EXIT_SUCCESS;
}

int main(int argc, char* argv[]) {
    if (argc == 1)
    {
        return command_list_telephone_book_records(argc, argv);
    }

    if (strcmp(argv[1], OPTION_HELP_SHORT) == 0 ||
        strcmp(argv[1], OPTION_HELP_LONG) == 0)
    {
        print_help(argv[0]);
        return EXIT_SUCCESS;
    }

    if (strcmp(argv[1], OPTION_ADD_SHORT) == 0 ||
        strcmp(argv[1], OPTION_ADD_LONG) == 0)
    {
        return command_add_record(argc, argv);
    }

    if (strcmp(argv[1], OPTION_REMOVE_SHORT) == 0 ||
        strcmp(argv[1], OPTION_REMOVE_LONG) == 0)
    {
        return command_remove_records(argc, argv);
    }

    return command_list_telephone_book_records(argc, argv);
}

Critique request

As always, I want to hear everything you have to say, for example:

  • idiomatic C
  • naming
  • API
  • modularity
  • robustness
  • efficiency
\$\endgroup\$
3
  • \$\begingroup\$ strongly suggest turning up the compiler warnings because the posted code has two calls to function: telephone_book_record_list_free() that do not have preceding prototype for the function. (for gcc, at a minimum use: -Wall -Wextra -pedantic I also use: -Wconversion -std=gnu99 ) \$\endgroup\$ Sep 29, 2016 at 20:05
  • \$\begingroup\$ @user3629249 Disagreed on -std=gnu99: I want to keep it ANSI. \$\endgroup\$
    – coderodde
    Sep 30, 2016 at 12:16
  • \$\begingroup\$ If you use -ansi, then expect that certain functions (all the posix functions for example) will not be available. \$\endgroup\$ Sep 30, 2016 at 16:43

1 Answer 1

5
\$\begingroup\$

You seem to have built in a cultural assumption that everybody has a first name and a last name, which is not true, and that first and last have the same role everywhere. The changes to fix that are probably quite extensive, unfortunately.


You're not consistent with your include ordering. I normally recommend that you include your own headers first, and Standard Library headers last - this helps identify where your own headers may have made unjustified assumptions that standard headers were included first.

On a related note, you might want to move telephone_book_record_list_write_to_file() and telephone_book_record_list_read_from_file() into a different header (perhaps called telephone_io.h), to avoid telephone.h dragging in <stdio.h> to every translation unit that includes it. Unfortunately, you can't just provide a compatible definition for FILE, or that would be preferable.


char separator();

Is there a reason you made this a function, rather than a simple constant?


In loops like this:

for (i = 0; i != max_telephone_contact_id_length + 1; ++i)

I normally prefer to use < or <= as the conditional. It's more idiomatic, and avoids some errors caused by modifying i within the loop body. And in modern C, you can declare i within the for, which makes it clearer that you won't be using the value afterwards.

Looking at the Pascal-style repetitive loops there, I'd be inclined to refactor with a small function:

/**
 * Write /n/ copies of character /c/ into string buffer /s/.
 * Note that no null is written.
 * Returns the next write position.
 */
char *write_separator(char *s, char c, int n) {
    while (n-->0)
        *s++ = c;
    return s;
}

or even (with #include <string.h>):

char *write_separator(char *s, char c, int n) {
    memset(s, c, n);
    return s+n;
}

I generally avoid this sort of thing at the low levels:

if (!separator_string)
{
    fputs("ERROR: Cannot allocate memory for the row separator string.",
          stderr);
    return NULL;
}

Prefer to return an error indication, and leave it up to the calling code to decide how to report it. In a GUI version, you probably want to display a GUI message rather than reporting to stderr.

Also, note that you don't free the successfully allocated memory in any of these error paths, giving a memory leak that's hard to track down.


This looks contorted:

sprintf(record_format_string,
        "%%-%zus | %%-%zus | %%-%zus | %%-%zuzu\n",

It's probably clearer to use %*s in the format string, and pass the length with each call.


This one's really weird:

   title_string =
        malloc(sizeof(char) *
               (max_last_name_token_length + 1) +
               (max_first_name_token_length + 2) +
               (max_telephone_number_token_length + 2) +
               (max_telephone_contact_id_length + 1) + 4);

sizeof (char) is 1 by definition; multiplying by it makes you look ignorant. Also, it's not clear why only the last-name token should be multiplied by 1 - the line breaks and parentheses serve to deceive the reader here! It may be simpler and more consistent to ask snprintf to compute that sum for you using the same format string you'll be printing with (at a small performance cost).

\$\endgroup\$
4
  • \$\begingroup\$ All good points. What comes to the cultural aspect, can you mention one culture/region where the first name/last name separation does not apply? \$\endgroup\$
    – coderodde
    Sep 28, 2016 at 19:07
  • \$\begingroup\$ @coderodde - I do know one person (residing in Great Britain) who has a single name rather than first (middle) last. And many names in East Asia are written with family name first - that's quite common in China and Korea. I'm not an internationalization expert, though, so willing to sit corrected! \$\endgroup\$ Sep 28, 2016 at 21:07
  • \$\begingroup\$ relevant article \$\endgroup\$ Sep 28, 2016 at 21:09
  • \$\begingroup\$ Where I am, people have two surnames, one from the father and one from the mother, and one or usually more forenames. \$\endgroup\$ Sep 29, 2016 at 0:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.