1
\$\begingroup\$

(See the next iteration.)

This question is a slight improvement over A telephone book command line program in ANSI C

I have incorporated almost all points stated by Toby Speight.

Now I have this:

telephone_book.h
#ifndef TELEPHONE_BOOK_H
#define TELEPHONE_BOOK_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);

/*******************************************************************************
* 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 "telephone_book_utils.h"
#include <stdlib.h>
#include <string.h>

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

/*******************************************************************************
* 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;
}

/*******************************************************************************
* 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"

#ifdef _WIN32
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif

/*******************************************************************************
* 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 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 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] =
        PATH_SEPARATOR;

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

    return telephone_record_book_file_path;
}

static char* write_separator(char* str, char c, size_t n)
{
    memset(str, c, n);
    return str + n;
}

/*******************************************************************************
* 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)
{
    char* save_separator_string;

    /* The separator string is composed of 4 horizontal bars: one bar for the
     * last name, one for first name, one for phone number, and one for the ID
     * (in that order). The magic constants 1 and 2, are the additional padding
     * so that for each attribute (column) there is a space before and after the
     * column title. 'max_first_name_token_length + 2' means that the longest
     * first name (or column) title is no longer than 
     * 'max_first_name_token_length', and it will be preceded and followed by 
     * one space.
     *
     * The magic constant 4, counts 3 bars between the columns and a final 
     * '\0' terminator.
     */
    char* separator_string =
    malloc((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)
    {
        return NULL;
    }

    save_separator_string = separator_string;
    separator_string = write_separator(separator_string,
                                       '-',
                                       max_last_name_token_length + 1);

    separator_string = write_separator(separator_string, '+', 1);
    separator_string = write_separator(separator_string,
                                       '-',
                                       max_first_name_token_length + 2);

    separator_string = write_separator(separator_string, '+', 1);
    separator_string = write_separator(separator_string,
                                       '-',
                                       max_telephone_number_token_length + 2);

    separator_string = write_separator(separator_string, '+', 1);
    separator_string = write_separator(separator_string,
                                       '-',
                                       max_telephone_contact_id_length + 1);

    write_separator(separator_string, '\0', 1);
    return save_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;

    /* The format string used to output the actual telephone book records. */
    char* record_format_string;

    /* The title string. */
    char* title_string;

    /* The separating horizontal bar. */
    char* separator_string;
    char* id_holder_string;

    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)
    {
        return NULL;
    }

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

    if (!id_holder_string)
    {
        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(FORMAT_STRING_CAPACITY);

    if (!record_format_string)
    {
        free(output_table);
        return NULL;
    }

    /* This is really ad hoc, yet we want to keep the listing code as simple 
       as possible, so we opt to do the format magic in this routine. */
    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((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)
    {
        free(output_table);
        free(record_format_string);
        return NULL;
    }

    sprintf(title_string,
            "%-*s | %-*s | %-*s | %-*s",
            (int) max_last_name_token_length,
            TITLE_LAST_NAME,
            (int) max_first_name_token_length,
            TITLE_FIRST_NAME,
            (int) max_telephone_number_token_length,
            TITLE_TELEPHONE_NUMBER,
            (int) max_telephone_contact_id_length,
            TITLE_CONTACT_ID);

    /* @ALLOC: output_table, record_format_string, title_string, 
               separator_string*/
    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)
    {
        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)
    {
        return NULL;
    }

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

    if (!id_holder_string)
    {
        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;
}
telephone_book_io.h
#ifndef telephone_book_io_h
#define telephone_book_io_h

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

/*******************************************************************************
* 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);


#endif /* telephone_book_io_h */
telephone_book_io.c
#include "telephone_book_io.h"
#include <stdio.h>

#define MAX_RECORD_TOKEN_LENGTH_STRING "64"
#define MAX_RECORD_TOKEN_LENGTH 65

/*******************************************************************************
* 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)
            {
                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;
}
main.c
#include "telephone_book.h"
#include "telephone_book_io.h"
#include "telephone_book_utils.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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 = PATH_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);
        telephone_book_record_free(record);
        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);
}

As always, any critique is much appreciated.

\$\endgroup\$
1
\$\begingroup\$

Architecture

With the function set posted, there is no need for the struct telephone_book_record_list_node* prev field. Iterating through a linked link can keep track of the previous node if needed rather than obliging millions of records to maintain a prev field.

Functionality

If the record scanned does not return 4, exit the loop or handle the error - do not ignore it. Code has no handling of reading only 1 to 3 fields and instead code could create subsequent bogus records.

//while (!feof(f) && !ferror(f)) {
//    read_result = fscanf(f,
//    ...
//    if (read_result == 4)
while (fscanf(f, ... ) == 4) {

puts() always appends a '\n' to the output. This prevents separator_string and others from printing without a line-feed if desired. Suggest allowing such formatting to occur without an obligatory '\n' should the user opt for that.

// puts(output_strings->separator_string); // \n appended
fputs(output_strings->separator_string, stdout);  // \n not appended

Consider allowing other sorting funcitons. Example

Defensive Coding

Test for valid telephone number characters. E. g. "0123456789*#-,."

Minor

1) Do not repeat. The same comment exist in 2 files. This obliges a maintainer to update comments twice, all too often resulting in their content diverging. Suggest referencing comments.

/************...
* See telephone_book.h
*************...*/
void telephone_book_record_free(telephone_book_record* record) {
  ...
}

2) if(NULL-ness test) not need. free(NULL); is OK. (Used in multiple places.)

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

3) Check the result of *scanf() and friends. But this is in the test driver, so a minor point.

// sscanf(argv[arg_index], "%d", &id);
if (sscanf(argv[arg_index], "%d", &id) != 1) Handle_Error();

4) Not a fan of style MAX_RECORD_TOKEN_LENGTH_STRING code

//#define MAX_RECORD_TOKEN_LENGTH_STRING "64"
//    read_result = fscanf(f,
//                         "%" MAX_RECORD_TOKEN_LENGTH_STRING "s\
//                         %"  MAX_RECORD_TOKEN_LENGTH_STRING "s\

// Alternative
#define TOKEN_SCAN_FMT "%64s"

read_result = fscanf(f, TOKEN_SCAN_FMT TOKEN_SCAN_FMT TOKEN_SCAN_FMT "%d\n", 
    last_name_token, first_name_token, phone_number_token, &id_holder);

5) * sizeof (char) serves little purpose - it is always (size_t)1. Drop it or if code should correlate to the type of telephone_record_book_file_path references, than use that directly.

// telephone_record_book_file_path = malloc(sizeof(char) * 
//    (home_directory_name_length ...));
telephone_record_book_file_path = malloc(home_directory_name_length ...);
// or
telephone_record_book_file_path = malloc(sizeof *telephone_record_book_file_path * 
    (home_directory_name_length ...));

Comment

Good use of size_t and allocation coding style. Nicely laid out code although I find the indenting not amenable to auto formatting.

\$\endgroup\$
  • \$\begingroup\$ read_result = fscanf(f, TOKEN_SCAN_FMT, TOKEN_SCAN_FMT, TOKEN_SCAN_FMT "%d\n", Are you sure that there should be commas between TOKEN_SCAN_FMT s? \$\endgroup\$ – coderodde Oct 5 '16 at 13:07
  • \$\begingroup\$ @coderodde Good eye. Removed the 2 commas to allow string literal concatenation. \$\endgroup\$ – chux - Reinstate Monica Oct 5 '16 at 13:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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