diff --git a/homework_4/CMakeLists.txt b/homework_4/CMakeLists.txt index 4d4fd12..944093a 100644 --- a/homework_4/CMakeLists.txt +++ b/homework_4/CMakeLists.txt @@ -3,3 +3,4 @@ project(homework_4) set(homeworkName "${PROJECT_NAME}") add_subdirectory(task_1) +add_subdirectory(task_2) diff --git a/homework_4/README.md b/homework_4/README.md index 33f4ec2..e7fc184 100644 --- a/homework_4/README.md +++ b/homework_4/README.md @@ -1,3 +1,5 @@ # Homework 4 [Task 1. Binary representation](/homework_4/task_1) + +[Task 2. Phone database](/homework_4/task_2) diff --git a/homework_4/task_2/.gitignore b/homework_4/task_2/.gitignore new file mode 100644 index 0000000..407e6bb --- /dev/null +++ b/homework_4/task_2/.gitignore @@ -0,0 +1 @@ +phoneDatabase diff --git a/homework_4/task_2/CMakeLists.txt b/homework_4/task_2/CMakeLists.txt new file mode 100644 index 0000000..fdaea50 --- /dev/null +++ b/homework_4/task_2/CMakeLists.txt @@ -0,0 +1,9 @@ +project("${homeworkName}_task_2") + +add_library(phoneBook database.c personEntry.c) + +add_executable(${PROJECT_NAME} main.c) +target_link_libraries(${PROJECT_NAME} phoneBook) + +add_executable(${PROJECT_NAME}_test test.c) +target_link_libraries(${PROJECT_NAME}_test phoneBook) diff --git a/homework_4/task_2/database.c b/homework_4/task_2/database.c new file mode 100644 index 0000000..3ea68dd --- /dev/null +++ b/homework_4/task_2/database.c @@ -0,0 +1,149 @@ +#include "database.h" + +#include +#include +#include +#include + +#define DB_SIGNATURE "#!hw_phonebook" +#define DB_BEGIN "{{" +#define DB_END "}}" + +Database *createDatabase() { + Database *database = malloc(sizeof(Database)); + if (database == NULL) { + return NULL; + } + database->entriesCount = 0; + + return database; +} + +bool addEntry(Database *database, PersonEntry entry) { + if (database == NULL) { + return false; + } + + // entries count cannot be more than DB_MAX_ENTRIES, but check this case too + if (database->entriesCount >= DB_MAX_ENTRIES) { + return false; + } + + database->entries[database->entriesCount] = entry; + ++database->entriesCount; + return true; +} + +#pragma region Save/Load + +bool saveDatabase(FILE *stream, Database *database) { + if (stream == NULL) { + return false; + } + + if (database == NULL) { + return false; + } + + fprintf(stream, "%s\n", DB_SIGNATURE); + fprintf(stream, "%s\n", DB_BEGIN); + + for (int i = 0; i < database->entriesCount; ++i) { + PersonEntry entry = database->entries[i]; + fprintf(stream, "%s\n", entry.personName); + fprintf(stream, "%s\n", entry.phoneNumber); + } + + fprintf(stream, "%s\n", DB_END); + + return true; +} + +// reads line and sets first '\n' to '\0' +bool tryReadLine(char *buffer, int count, FILE *file) { + if (fgets(buffer, count, file) == NULL || feof(file)) { + return false; + } + + for (int i = 0; i < count; ++i) { + if (buffer[i] == '\n') { + buffer[i] = '\0'; + break; + } + } + + return true; +} + +Database *loadDatabase(FILE *stream) { + if (stream == NULL) { + return NULL; + } + + Database *database = createDatabase(); + + if (database == NULL) { + return NULL; + } + + char buffer[256] = { 0 }; + + if (!tryReadLine(buffer, sizeof(buffer), stream) && strcmp(buffer, DB_SIGNATURE) != 0) { + return NULL; + } + + if (!tryReadLine(buffer, sizeof(buffer), stream) && strcmp(buffer, DB_BEGIN) != 0) { + return NULL; + } + + while (true) { + // person name or db end anchor + if (!tryReadLine(buffer, sizeof(buffer), stream)) { + return NULL; + } + + if (strcmp(buffer, DB_END) == 0) { + break; + } + + char *personName = strdup(buffer); + if (personName == NULL) { + return NULL; + } + + // phone number + if (!tryReadLine(buffer, sizeof(buffer), stream)) { + return NULL; + } + + PhoneNumber phoneNumber; + if (!tryParsePhoneNumber(buffer, &phoneNumber)) { + return NULL; + } + + PersonEntry entry = { + .personName = personName, + .phoneNumber = phoneNumber + }; + + if (!addEntry(database, entry)) { + // no space left, not an error + break; + } + } + + return database; +} + +#pragma endregion + +void disposeDatabase(Database *database) { + if (database == NULL) { + return; + } + + for (int i = 0; i < database->entriesCount; ++i) { + disposeEntry(&database->entries[i]); + } + free(database); +} diff --git a/homework_4/task_2/database.h b/homework_4/task_2/database.h new file mode 100644 index 0000000..ce0efa7 --- /dev/null +++ b/homework_4/task_2/database.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "personEntry.h" + +#define DB_MAX_ENTRIES 100 + +typedef struct { + int entriesCount; + PersonEntry entries[DB_MAX_ENTRIES]; +} Database; + +/// @brief Creates empty database +/// @return `Database` if created successfully, `NULL` otherwise +Database *createDatabase(); + +/// @brief Adds a person entry to database +/// @param database Pointer to database +/// @param entry Entry to add +/// @return `true` if added successfully, `false` otherwise (no space in database left) +bool addEntry(Database *database, PersonEntry entry); + +/// @brief Saves database to specified stream +/// @param stream Stream to save database to +/// @param database Pointer to database +/// @return `true` if saved successfully, `false` otherwise +bool saveDatabase(FILE *stream, Database *database); + +/// @brief Loads database from specified file stream +/// @param stream Stream to load database from +/// @return `Database` if loaded successfully, `NULL` otherwise +Database *loadDatabase(FILE *stream); + +/// @brief Disposes database and all of its entries +/// @param database Database to dispose (can be `NULL`) +void disposeDatabase(Database *database); diff --git a/homework_4/task_2/main.c b/homework_4/task_2/main.c new file mode 100644 index 0000000..905f9b9 --- /dev/null +++ b/homework_4/task_2/main.c @@ -0,0 +1,283 @@ +#include +#include +#include +#include + +#include "database.h" +#include "personEntry.h" + +typedef enum { + Exit, + Error, + Menu, + AddingEntry, + PrintingAllEntries, + FindNumberByName, + FindNameByPhone, + SaveDatabase +} State; + +char *readLine(const char *prompt) { + char buffer[1024] = { 0 }; + printf("%s", prompt); + + bool overflow = true; + for (int i = 0; i < (int)sizeof(buffer) - 1; ++i) { + char c = getchar(); + if (c == '\n' || c == EOF) { + overflow = false; + break; + } + buffer[i] = c; + } + if (overflow) { + while (getchar() != '\n') {} + } + + return strdup(buffer); +} + +void printEntry(PersonEntry entry) { + printf("%s : %s\n", entry.personName, entry.phoneNumber); +} + +bool tryReadPhoneNumber(const char *prompt, PhoneNumber *phoneNumber) { + char *rawPhoneNumber = readLine(prompt); + if (!tryParsePhoneNumber(rawPhoneNumber, phoneNumber)) { + free(rawPhoneNumber); + return false; + } + return true; +} + +State readCommand(void) { + int command = -1; + if (scanf("%d", &command) != 1) { + } + while (getchar() != '\n') {} + switch (command) + { + case 0: + return Exit; + case 1: + return AddingEntry; + case 2: + return PrintingAllEntries; + case 3: + return FindNumberByName; + case 4: + return FindNameByPhone; + case 5: + return SaveDatabase; + default: + printf("Error: unknown command\n"); + return Menu; + } +} + +State addEntryCommand(Database *database) { + if (database->entriesCount == DB_MAX_ENTRIES) { + printf("Error: entries slots maxed out\n"); + return Menu; + } + + char *name = readLine("Enter name: "); + if (name == NULL) { + printf("Error: cannot read name"); + return Menu; + } + + char *rawPhoneNumber = readLine("Enter phone number: "); + if (rawPhoneNumber == NULL) { + printf("Error: cannot read phone number"); + return Menu; + } + + PhoneNumber phoneNumber; + if (!tryParsePhoneNumber(rawPhoneNumber, &phoneNumber)) { + printf("Error: phone number is in incorrect format"); + free(rawPhoneNumber); + return Menu; + } + free(rawPhoneNumber); + + PersonEntry entry = { + .personName = name, + .phoneNumber = phoneNumber + }; + + if (!addEntry(database, entry)) { + printf("Error: cannot add new entry\n"); + return Menu; + } + + printf("Added entry successfully\n"); + + int slotsLeft = DB_MAX_ENTRIES - database->entriesCount; + if (slotsLeft == 0) { + printf("Warning: no entry slots left in database for new entries\n"); + } else if (slotsLeft <= 10) { + printf("Warning: %d entry slots left in database\n", slotsLeft); + } + + return Menu; +} + +State printDatabaseCommand(Database *database) { + if (database->entriesCount == 0) { + printf("Empty database\n"); + } + for (int i = 0; i < database->entriesCount; ++i) { + PersonEntry entry = database->entries[i]; + printEntry(entry); + } + + return Menu; +} + +State findNameByPhoneCommand(Database *database) { + PhoneNumber phoneNumber; + if (!tryReadPhoneNumber("Input phone number: ", &phoneNumber)) { + printf("Incorrect phone number format"); + return Menu; + } + + bool foundAny = false; + printf("Found entries:\n"); + for (int i = 0; i < database->entriesCount; ++i) { + PersonEntry entry = database->entries[i]; + if (strstr(entry.phoneNumber, phoneNumber)) { + foundAny = true; + printEntry(entry); + } + } + + if (!foundAny) { + printf("None\n"); + } + return Menu; +} + +State findNumberByNameCommand(Database *database) { + char *personName = readLine("Input name: "); + + bool foundAny = false; + printf("Found entries:\n"); + for (int i = 0; i < database->entriesCount; ++i) { + PersonEntry entry = database->entries[i]; + if (strstr(entry.personName, personName)) { + foundAny = true; + printEntry(entry); + } + } + + if (!foundAny) { + printf("None\n"); + } + free(personName); + return Menu; +} + +State saveDatabaseCommand(Database *database, const char *path) { + FILE *file = fopen(path, "w"); + if (file == NULL) { + printf("IO error: cannot save database\n"); + return Menu; + } + if (!saveDatabase(file, database)) { + printf("Error: cannot save database\n"); + } else { + printf("Saved successfully\n"); + } + fclose(file); + return Menu; +} + +bool doConversation(void) { + const char *databasePath = "./phoneDatabase"; + Database *database; + FILE *file = fopen(databasePath, "r"); + if (file == NULL) { + database = createDatabase(); + } else { + database = loadDatabase(file); + fclose(file); + + if (database == NULL) { + printf("Error: cannot load database; create new? (y/n): "); + char choice = 'n'; + if (scanf("%c", &choice) != 1) { + return false; + } + if (choice == 'y') { + database = createDatabase(); + } else { + return false; + } + } + } + + if (database == NULL) { + printf("Error: cannot create database\n"); + return false; + } + + printf("Phone database\n"); + printf("Available commands: \n"); + printf(" 0 - exit;\n"); + printf(" 1 - add new entry;\n"); + printf(" 2 - print all entries;\n"); + printf(" 3 - find phone number by name;\n"); + printf(" 4 - find name by phone number;\n"); + printf(" 5 - save database.\n"); + + State state = Menu; + while (true) { + switch (state) + { + case Exit: + printf("Exiting...\n"); + disposeDatabase(database); + return true; + + case Error: + disposeDatabase(database); + return false; + + case Menu: + printf("phonedb> "); + state = readCommand(); + break; + + case AddingEntry: + state = addEntryCommand(database); + break; + + case PrintingAllEntries: + state = printDatabaseCommand(database); + break; + + case FindNumberByName: + state = findNumberByNameCommand(database); + break; + + case FindNameByPhone: + state = findNameByPhoneCommand(database); + break; + + case SaveDatabase: + state = saveDatabaseCommand(database, databasePath); + break; + + default: + printf("Error: Unknown state\n"); + disposeDatabase(database); + return false; + } + } +} + +int main(void) { + bool conversationResult = doConversation(); + return conversationResult ? 0 : 1; +} diff --git a/homework_4/task_2/personEntry.c b/homework_4/task_2/personEntry.c new file mode 100644 index 0000000..d9ce780 --- /dev/null +++ b/homework_4/task_2/personEntry.c @@ -0,0 +1,84 @@ +#include "personEntry.h" + +#include +#include +#include + +bool tryParsePhoneNumber(const char *input, PhoneNumber *phoneNumber) { + if (input == NULL) { + return false; + } + if (phoneNumber == NULL) { + return false; + } + + char buffer[1024] = { 0 }; + + bool hasOpenParen = false, hyphen = false; + + for (int i = 0, j = 0; i < (int)sizeof(buffer) - 1; ++i) { + char c = input[i]; + if (c == '\0') { + break; + } + + if (c == '-') { + // no two consecutive hyphens + if (hyphen) { + return false; + } + hyphen = true; + continue; + } else { + hyphen = false; + } + + if (c == '(') { + if (hasOpenParen) { + return false; + } + hasOpenParen = true; + continue; + } + + if (c == ')') { + hasOpenParen = false; + continue; + } + + if ((c >= '0' && c <= '9') || c == '+') { + buffer[j] = c; + ++j; + continue; + } + + // all other allowed character(s) + if (c == ' ') { + continue;; + } + + // fallback + return false; + } + + if (hyphen || hasOpenParen) { + return false; + } + + char *result = strdup(buffer); + + if (result == NULL) { + return false; + } + *phoneNumber = result; + return true; +} + +void disposeEntry(PersonEntry *entry) { + if (entry == NULL) { + return; + } + + free(entry->personName); + free(entry->phoneNumber); +} diff --git a/homework_4/task_2/personEntry.h b/homework_4/task_2/personEntry.h new file mode 100644 index 0000000..5a23d69 --- /dev/null +++ b/homework_4/task_2/personEntry.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +typedef char* PhoneNumber; + +typedef struct { + char *personName; + PhoneNumber phoneNumber; +} PersonEntry; + +/// @brief Parses phone number from input string +/// @param input Input string to be parsed +/// @param phoneNumber Pointer to write result to +/// @return `true` if parsed successfully, `false` otherwise +bool tryParsePhoneNumber(const char *input, PhoneNumber *phoneNumber); + +/// @brief Disposes person entry and all of its phone numbers +/// @param entry Entry to dipose (can be `NULL`) +void disposeEntry(PersonEntry *entry); diff --git a/homework_4/task_2/test.c b/homework_4/task_2/test.c new file mode 100644 index 0000000..92975a2 --- /dev/null +++ b/homework_4/task_2/test.c @@ -0,0 +1,126 @@ +#define CTEST_MAIN +#define CTEST_SEGFAULT +#include "../../ctest/ctest.h" + +#include "database.h" +#include "personEntry.h" + +int main(int argc, const char *argv[]) { + return ctest_main(argc, argv); +} + +#define NAME_1 "Test Name" +#define PHONE_1 "+12345678900" + +#define NAME_2 "Test Name 2" +#define PHONE_2 "+98765432100" + +#define SAVE_PATH "test_tmp_db" + +Database *createNewDatabase(void) { + Database *db = createDatabase(); + ASSERT_NOT_NULL(db); + return db; +} + +void assertAddEntry(Database *database, const char *personName, const PhoneNumber phoneNumber) { + PersonEntry entry = { + .personName = strdup(personName), + .phoneNumber = strdup(phoneNumber) + }; + + ASSERT_TRUE(addEntry(database, entry)); + ASSERT_STR(database->entries[database->entriesCount - 1].personName, personName); + ASSERT_STR(database->entries[database->entriesCount - 1].phoneNumber, phoneNumber); +} + +void assertAddNamesAndPhones(Database *db) { + assertAddEntry(db, NAME_1, PHONE_1); + assertAddEntry(db, NAME_2, PHONE_2); +} + +void assertSave(Database *db) { + FILE *file = fopen(SAVE_PATH, "w"); + ASSERT_NOT_NULL(file); + + ASSERT_TRUE(saveDatabase(file, db)); + ASSERT_EQUAL(fclose(file), 0); +} + +Database *loadTestDatabase(void) { + FILE *file = fopen(SAVE_PATH, "r"); + ASSERT_NOT_NULL(file); + + Database *db = loadDatabase(file); + + ASSERT_NOT_NULL(db); + ASSERT_EQUAL(fclose(file), 0); + return db; +} + +CTEST(databaseTests, addEntriesTest) { + Database *db = createNewDatabase(); + assertAddNamesAndPhones(db); + disposeDatabase(db); +} + +CTEST(databaseTests, saveDatabaseTest) { + Database *db = createNewDatabase(); + + assertAddNamesAndPhones(db); + assertSave(db); + + disposeDatabase(db); +} + +CTEST(databaseTests, loadDatabaseTest) { + Database *db = createNewDatabase(); + + assertAddNamesAndPhones(db); + assertSave(db); + + Database *db2 = loadTestDatabase(); + + ASSERT_EQUAL(db->entriesCount, db2->entriesCount); + for (int i = 0; i < db->entriesCount; ++i) { + PersonEntry entry = db->entries[i], + entry2 = db2->entries[i]; + ASSERT_STR(entry.personName, entry2.personName); + ASSERT_STR(entry.phoneNumber, entry2.phoneNumber); + } + + disposeDatabase(db2); + ASSERT_EQUAL(remove(SAVE_PATH), 0); + + disposeDatabase(db); +} + +CTEST(phoneParsingTest, incorrectPhoneTest) { + PhoneNumber phoneNumber; + ASSERT_FALSE(tryParsePhoneNumber("0--0", &phoneNumber)); +} + +CTEST(phoneParsingTest, incorrectPhoneTest2) { + PhoneNumber phoneNumber; + ASSERT_FALSE(tryParsePhoneNumber("+1 ((12))-14", &phoneNumber)); +} + +CTEST(phoneParsingTest, incorrectPhoneTest3) { + PhoneNumber phoneNumber; + ASSERT_FALSE(tryParsePhoneNumber("834-14-", &phoneNumber)); +} + +CTEST(phoneParsingTest, correctPhoneTest) { + PhoneNumber phoneNumber; + ASSERT_TRUE(tryParsePhoneNumber("135-67-54", &phoneNumber)); +} + +CTEST(phoneParsingTest, correctPhoneTest2) { + PhoneNumber phoneNumber; + ASSERT_TRUE(tryParsePhoneNumber("(135) 67 54", &phoneNumber)); +} + +CTEST(phoneParsingTest, correctPhoneTest3) { + PhoneNumber phoneNumber; + ASSERT_TRUE(tryParsePhoneNumber("+5 (432) 234 67-54", &phoneNumber)); +}