Want to view this project? In-Memory Record Database
Finishing Up the List Functionality
We’re nearly complete with the list and search functionality. Now we just have to give the user the ability to search and list the records for themselves instead of just having it automatically be done for us when we create or delete a record.
We’re going to dive into creating the console UI a bit for this portion since we’re getting closer to being able to create the UI itself. Let’s create a function that will print our sorted search results to the screen. We’ll be using this function in a few places.
void printSortedRecords(const struct SearchResults *results)
{
if (results->records == NULL)
{
printf(">>> No records to view <<<\n");
return;
}
printf("\n%-10s | %-10s | %-10s | %-10s\n", "ID", "FirstName", "LastName", "Age");
printf("--------------------------------------------------\n");
for (int i = 0; i < results->count; i++)
{
printf("\n%-10d | %-10.10s | %-10.10s | %-10d\n", results->records[i]->id, results->records[i]->firstName,
results->records[i]->lastName, results->records[i]->age);
}
printf("--------------------------------------------------\n");
}
Nothing too complicated about this hopefully. Feel free to format your tables however you want.
Now we can create the function that prints out the list of sorted records. This function utilizes listRecordsSorted from the previous part in order to grab the search results. We’ll optionally clear the console window, print the sorted records to the screen, and finally, free the memory allocated for the search results.
static void printListRecords(const struct RecordTable *table, enum SortField sortField)
{
struct SearchResults results = {0};
enum SearchResultStatus listRecordsResult = listRecordsSorted(table, sortField, &results);
if (listRecordsResult != SEARCH_SUCCESS)
{
printf("*** COULD NOT LIST RECORDS ***\n");
return;
}
clearScreen();
printSortedRecords(&results);
freeSearchResults(&results);
return;
}
static void clearScreen(void)
{
#ifdef _WIN32
system("cls"); // Windows command
#else
system("clear"); // Linux / macOS command
#endif
}
This function is ready to be used in our UI which we’ll go over soon.
Finishing Up the Search Functionality
Almost finished with listing, sorting, searching, and all of that mess. They’re very intertwined together in this program, so I apologize if this feels a bit all over the place. You should’ve seen me when I was writing this program!
Alright, we still need to give the user the ability to search for records themselves. We’re going to start with the simplest one first: searching by ID. searchById utilizes findRecordById and, of course, initializeSearchResults. Since we’re only grabbing one record, it really is as simple as assigning the first index of struct Record **records.
enum SearchResultStatus searchById(const struct RecordTable *table, const int id, struct SearchResults *results)
{
struct Record *foundRecord = findRecordById(table, id);
if (foundRecord == NULL)
{
results->records = NULL;
results->count = 0;
return SEARCH_SUCCESS;
}
enum SearchInitStatus searchResultsInitialized = initializeSearchResults(results, 1);
if (searchResultsInitialized == INITIALIZATION_FAILED)
{
return SEARCH_FAILED;
}
results->records[0] = foundRecord;
return SEARCH_SUCCESS;
}
As for searching by first name, last name, and age, we’re basically repeating the same thing we did with countNodes and collectNodesInOrder except now we’re adding our individual columns into the equation. For this example, we’ll look at first name only. The same concept shown here applies to both last name and age.
We’ll start with our countFirstNameMatches function that uses a pre-order traversal method to count how many struct Record*s contain the first name that was searched by the user.
The tree traversal method does not matter in this instance since we’re only counting the number of matches.
static void countFirstNameMatches(const struct RecordNode *root, const char *firstName, int *count)
{
if (root == NULL)
{
return;
}
int comparison = strcmp(firstName, root->record->firstName);
if (comparison < 0)
{
countFirstNameMatches(root->left, firstName, count);
}
else if (comparison > 0)
{
countFirstNameMatches(root->right, firstName, count);
}
else
{
// Pre-order traversal since we're only getting a count
(*count)++;
countFirstNameMatches(root->left, firstName, count);
countFirstNameMatches(root->right, firstName, count);
}
}
Then we’ll follow up with collectFirstNameMatches which will actually grab the struct Record*s for us. This method uses in-order traversal since we would ideally like for the names to be in order when we display them for the user. Maybe you don’t care about that though and you just like to see the world burn. In any case, here’s the function.
static void collectFirstNameMatches(const struct RecordNode *root, const char *firstName, struct SearchResults *results, int *index)
{
if (root == NULL)
{
return;
}
int comparison = strcmp(firstName, root->record->firstName);
if (comparison < 0)
{
collectFirstNameMatches(root->left, firstName, results, index);
}
else if (comparison > 0)
{
collectFirstNameMatches(root->right, firstName, results, index);
}
else
{
// In-order traversal so the IDs can be in order
collectFirstNameMatches(root->left, firstName, results, index);
results->records[*index] = root->record;
(*index)++;
collectFirstNameMatches(root->right, firstName, results, index);
}
}
We now have the two functions we need to create our final searchByFirstName function. This function counts how many first name matches exist in the table, initializes the search results using that count, and collects the search results. Like I said before, the same concept applies to both last name and age searching, so we won’t beat a dead horse. I think you get the idea.
enum SearchResultStatus searchByFirstName(const struct RecordTable *table, const char *firstName, struct SearchResults *results)
{
int firstNameMatchesCount = 0;
countFirstNameMatches(table->firstNameTree.root, firstName, &firstNameMatchesCount);
enum SearchInitStatus searchResultsInitialized = initializeSearchResults(results, firstNameMatchesCount);
if (searchResultsInitialized == INITIALIZATION_FAILED)
{
return SEARCH_FAILED;
}
int searchResultsIndex = 0;
collectFirstNameMatches(table->firstNameTree.root, firstName, results, &searchResultsIndex);
return SEARCH_SUCCESS;
}
Excellent. Let’s wrap everything up in a new function that will be utilized by our console UI to print the search records to the screen. This is similar to listRecordsSorted where we use a switch statement to decide on what column we’re dealing with.
static void printSearchRecords(const struct RecordTable *table, const enum SearchField searchField, const struct Record record)
{
struct SearchResults results = {0};
enum SearchResultStatus searchStatus = 0;
switch (searchField)
{
case SEARCH_ID:
searchStatus = searchById(table, record.id, &results);
break;
case SEARCH_FIRSTNAME:
searchStatus = searchByFirstName(table, record.firstName, &results);
break;
case SEARCH_LASTNAME:
searchStatus = searchByLastName(table, record.lastName, &results);
break;
case SEARCH_AGE:
searchStatus = searchByAge(table, record.age, &results);
break;
default:
printf("*** UNKNOWN SEARCH PARAMETER ***\n");
return;
}
if (searchStatus != SEARCH_SUCCESS)
{
printf("*** ERROR: SEARCH FAILED ***\n");
}
clearScreen();
printSortedRecords(&results);
freeSearchResults(&results);
return;
}
We now have the ability to create, delete, list, and search for records! There’s only one last feature we need to implement before creating the console UI itself and that’s editing records. We’ll use everything we’ve seen throughout these articles to create our edit functions.