From 9e9c1b21569faeabd33716e4153a881e2eed7134 Mon Sep 17 00:00:00 2001 From: Filip Wandzio Date: Sun, 1 Mar 2026 17:45:00 +0100 Subject: Separate quiz logic from main function fo dedicated module Signed-off-by: Filip Wandzio --- .gitignore | 2 +- .idea/codeStyles/Project.xml | 106 ++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + build/exam | Bin 34952 -> 35720 bytes build/test_questions | Bin 34688 -> 35008 bytes include/questions.h | 12 +-- include/quiz.h | 34 ++++++ include/utils.h | 30 +++++- src/main.c | 94 +++------------- src/questions.c | 160 ++++++++++++++------------- src/quiz.c | 203 +++++++++++++++++++++++++++++++++++ src/utils.c | 125 +++++++++++++++++---- tests/test_questions.c | 34 +++--- 13 files changed, 599 insertions(+), 206 deletions(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 include/quiz.h create mode 100644 src/quiz.c diff --git a/.gitignore b/.gitignore index d483172..8e37b1f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ ### C++ ### # Prerequisites *.d - +build *.csv # Compiled Object files *.slo diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..54584fe --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,106 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/build/exam b/build/exam index 988b751..77b3548 100755 Binary files a/build/exam and b/build/exam differ diff --git a/build/test_questions b/build/test_questions index 04dac40..bdf53be 100755 Binary files a/build/test_questions and b/build/test_questions differ diff --git a/include/questions.h b/include/questions.h index 75bc751..95ce286 100644 --- a/include/questions.h +++ b/include/questions.h @@ -2,19 +2,19 @@ #include typedef struct { - char **general; - char **major; - size_t general_count; - size_t major_count; + char** general; + char** major; + size_t general_count; + size_t major_count; } QuestionSet; /** * Load questions from file. * Returns EXIT_SUCCESS or EXIT_FAILURE. */ -int load_questions(const char *filename, QuestionSet *qs); +int load_questions(const char* filename, QuestionSet* qs); /** * Free all allocated memory in QuestionSet. */ -void free_questions(QuestionSet *qs); +void free_questions(QuestionSet* qs); diff --git a/include/quiz.h b/include/quiz.h new file mode 100644 index 0000000..db69367 --- /dev/null +++ b/include/quiz.h @@ -0,0 +1,34 @@ +// +// Created by philw on 01/03/2026. +// + +#pragma once + +#include "../include/questions.h" +#include +#include +#include + +#define PERCENT_MULTIPLIER 100.0 +#define SCORE_FORMAT "Score: %zu/%zu (%.1f%%)\n" + +// ------------------------ +// Quiz state +// ------------------------ +typedef struct { + QuestionSet questions; + FILE* csv; + unsigned int seed; + size_t total_answered; + size_t total_correct; + int time_limit; +} QuizSession; + +// ------------------------ +// Quiz functions +// ------------------------ +bool initialize_quiz_session(QuizSession* session); +bool quiz_iteration(QuizSession* session); +void print_score(size_t correct, size_t total); +void print_final_score(const QuizSession* session); +void cleanup_session(QuizSession* session); diff --git a/include/utils.h b/include/utils.h index 74c0ca1..08895c0 100644 --- a/include/utils.h +++ b/include/utils.h @@ -1,8 +1,30 @@ #pragma once + +#include #include +/** Print a horizontal separator line */ void print_line(void); -void wait_enter(const char *msg); -char ask_yes_no(const char *msg); -void now_str(char *buf, size_t size); -size_t rand_index(size_t max); + +/** Pause until the user presses ENTER */ +void wait_for_enter(const char* prompt); + +/** Ask a yes/no question and return boolean */ +bool ask_yes_no(const char* prompt); + +/** + * Get the current timestamp for recording when a question was answered + * + * @param answer_time_buffer Buffer to store the formatted timestamp + * @param buffer_capacity Size of the buffer in bytes + */ +void get_answer_timestamp(char* answer_time_buffer, size_t buffer_capacity); + +/** + * Get a random question index in the quiz (thread-safe) + * + * @param seed Pointer to an unsigned int seed (per-thread) + * @param total_questions Number of questions in the current quiz session + * @return Random index in the range [0, total_questions-1] + */ +size_t get_random_question_index(unsigned int* seed, size_t total_questions); diff --git a/src/main.c b/src/main.c index 33d4b39..f69d734 100644 --- a/src/main.c +++ b/src/main.c @@ -1,83 +1,15 @@ -#include -#include -#include -#include -#include - -#include "questions.h" -#include "utils.h" - -int main(void) { - srand((unsigned)time(NULL)); - - // utwórz katalog data/ jeśli nie istnieje - struct stat st = {0}; - if (stat("data", &st) == -1) { -#ifdef _WIN32 - _mkdir("data"); -#else - mkdir("data", 0755); -#endif - } - - QuestionSet qs; - if (load_questions("data/questions.txt", &qs) != EXIT_SUCCESS) { - fprintf(stderr, "Error: cannot load questions file\n"); - return EXIT_FAILURE; - } - - FILE *csv = fopen("data/results.csv", "a"); - if (!csv) { - perror("CSV open"); - free_questions(&qs); - return EXIT_FAILURE; - } - - int limit = 0; - printf("Time limit (sec, 0 = unlimited): "); - if (scanf("%d", &limit) != 1) limit = 0; - wait_enter(NULL); - - size_t total = 0, correct = 0; - char cont = 'y'; - - while (cont == 'y' || cont == 'Y') { - const char *g = qs.general[rand_index(qs.general_count)]; - const char *m = qs.major[rand_index(qs.major_count)]; - - print_line(); - printf("GENERAL:\n%s\n\nMAJOR:\n%s\n", g, m); - print_line(); +#include "../include/quiz.h" // QuizSession and functions - wait_enter("Press ENTER to start..."); - time_t start = time(NULL); - - wait_enter("Press ENTER when done..."); - double duration = difftime(time(NULL), start); - - if (limit > 0 && duration > limit) - printf("Time exceeded!\n"); - - char ans = ask_yes_no("Correct? (y/n): "); - if (ans == 'y' || ans == 'Y') correct++; - total++; - - char ts[32]; - now_str(ts, sizeof(ts)); - fprintf(csv, "\"%s\",\"%s\",\"%s\",%c,%.0f\n", ts, g, m, ans, duration); - - printf("Score: %zu/%zu (%.1f%%)\n", - correct, total, - total ? (double)correct / total * 100 : 0.0); - - cont = ask_yes_no("Next? (y/n): "); - } - - printf("\nSession score: %.1f%%\n", - total ? (double)correct / total * 100 : 0.0); - - fclose(csv); - free_questions(&qs); +#include - return EXIT_SUCCESS; -} +int main(void) +{ + QuizSession session; + if (!initialize_quiz_session(&session)) + return EXIT_FAILURE; + while (quiz_iteration(&session)) { + } + print_final_score(&session); + cleanup_session(&session); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/src/questions.c b/src/questions.c index 88671a7..491c106 100644 --- a/src/questions.c +++ b/src/questions.c @@ -5,88 +5,94 @@ #define MAX_LINE 512 -static int add_question(char ***arr, size_t *count, const char *text) { - char **tmp = realloc(*arr, (*count + 1) * sizeof(char *)); - if (!tmp) - return EXIT_FAILURE; - *arr = tmp; - - (*arr)[*count] = malloc(strlen(text) + 1); - if (!(*arr)[*count]) - return EXIT_FAILURE; - - strcpy((*arr)[*count], text); - (*count)++; - return EXIT_SUCCESS; +static int add_question(char*** arr, size_t* count, const char* text) +{ + char** tmp = realloc(*arr, (*count + 1) * sizeof(char*)); + if (!tmp) + return EXIT_FAILURE; + *arr = tmp; + + (*arr)[*count] = malloc(strlen(text) + 1); + if (!(*arr)[*count]) + return EXIT_FAILURE; + + strcpy((*arr)[*count], text); + (*count)++; + return EXIT_SUCCESS; } -int load_questions(const char *filename, QuestionSet *qs) { - if (!filename || !qs) - return EXIT_FAILURE; - - FILE *f = fopen(filename, "r"); - if (!f) - return EXIT_FAILURE; - - qs->general = NULL; - qs->major = NULL; - qs->general_count = 0; - qs->major_count = 0; - - char line[MAX_LINE]; - enum { NONE, GENERAL, MAJOR } mode = NONE; - - while (fgets(line, sizeof(line), f)) { - line[strcspn(line, "\n")] = '\0'; - if (line[0] == '\0') - continue; - - if (strcmp(line, "#GENERAL") == 0) { - mode = GENERAL; - continue; - } - if (strcmp(line, "#MAJOR") == 0) { - mode = MAJOR; - continue; - } - - int res = EXIT_FAILURE; - switch (mode) { - case GENERAL: - res = add_question(&qs->general, &qs->general_count, line); - break; - case MAJOR: - res = add_question(&qs->major, &qs->major_count, line); - break; - default: - continue; // ignore lines before section - } - - if (res != EXIT_SUCCESS) { - fclose(f); - free_questions(qs); - return EXIT_FAILURE; - } - } - - fclose(f); - return (qs->general_count && qs->major_count) ? EXIT_SUCCESS : EXIT_FAILURE; +int load_questions(const char* filename, QuestionSet* qs) +{ + if (!filename || !qs) + return EXIT_FAILURE; + + FILE* f = fopen(filename, "r"); + if (!f) + return EXIT_FAILURE; + + qs->general = NULL; + qs->major = NULL; + qs->general_count = 0; + qs->major_count = 0; + + char line[MAX_LINE]; + enum { NONE, GENERAL, MAJOR } mode = NONE; + + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\n")] = '\0'; + if (line[0] == '\0') + continue; + + if (strcmp(line, "#GENERAL") == 0) { + mode = GENERAL; + continue; + } + if (strcmp(line, "#MAJOR") == 0) { + mode = MAJOR; + continue; + } + + int res = EXIT_FAILURE; + switch (mode) { + case GENERAL: + res = add_question( + &qs->general, &qs->general_count, line); + break; + case MAJOR: + res = add_question( + &qs->major, &qs->major_count, line); + break; + default: + continue; + } + + if (res != EXIT_SUCCESS) { + fclose(f); + free_questions(qs); + return EXIT_FAILURE; + } + } + + fclose(f); + return (qs->general_count && qs->major_count) ? EXIT_SUCCESS + : EXIT_FAILURE; } -void free_questions(QuestionSet *qs) { - if (!qs) - return; +void free_questions(QuestionSet* qs) +{ + if (!qs) + return; - for (size_t i = 0; i < qs->general_count; i++) - free(qs->general[i]); - for (size_t i = 0; i < qs->major_count; i++) - free(qs->major[i]); + for (size_t i = 0; i < qs->general_count; i++) + free(qs->general[i]); + for (size_t i = 0; i < qs->major_count; i++) + free(qs->major[i]); - free(qs->general); - free(qs->major); + free(qs->general); + free(qs->major); - qs->general = NULL; - qs->major = NULL; - qs->general_count = 0; - qs->major_count = 0; + qs->general = NULL; + qs->major = NULL; + qs->general_count = 0; + qs->major_count = 0; } diff --git a/src/quiz.c b/src/quiz.c new file mode 100644 index 0000000..7bc31b2 --- /dev/null +++ b/src/quiz.c @@ -0,0 +1,203 @@ +#include "../include/quiz.h" +#include "../include/utils.h" +#include +#include +#include +#include +#include +#include +#include + +#define INPUT_BUFFER_SIZE 64 +#define QUIZ_DATA_DIRECTORY "data" +#define QUESTIONS_FILE_NAME "questions.txt" +#define RESULTS_FILE_NAME "results.csv" +#define CSV_TIMESTAMP_BUFFER 32 +#define FILE_PATH_BUFFER 256 +#define PERMISSIONS_DATA_DIRECTORY 0755 +#define TIME_UNLIMITED 0 +#define BASE_VALUE 10 + +static const char* START_PROMPT = "Press ENTER to start..."; +static const char* DONE_PROMPT = "Press ENTER when done..."; +static const char* TIME_LIMIT_PROMPT = "Time limit (sec, 0 = unlimited): "; +static const char* CORRECT_PROMPT = "Correct? (y/n): "; +static const char* NEXT_QUESTION_PROMPT = "Next? (y/n): "; +static const char* TIME_EXCEEDED_MESSAGE = "Time exceeded!\n"; +static const char* CSV_OPEN_ERROR_MESSAGE = "CSV open"; +static const char* QUESTIONS_LOAD_ERROR = "Error: cannot load questions file\n"; +static const char* DATA_DIR_ERROR = "Error: cannot create data directory\n"; + +static bool ensure_data_directory_exists(void) +{ + struct stat st = {0}; + if (stat(QUIZ_DATA_DIRECTORY, &st) == -1) { +#ifdef _WIN32 + return _mkdir(QUIZ_DATA_DIRECTORY) == 0; +#else + return mkdir(QUIZ_DATA_DIRECTORY, PERMISSIONS_DATA_DIRECTORY) + == 0; +#endif + } + return true; +} + +static int read_time_limit(void) +{ + char buffer[INPUT_BUFFER_SIZE]; + printf("%s", TIME_LIMIT_PROMPT); + + if (!fgets(buffer, sizeof buffer, stdin)) + return TIME_UNLIMITED; + + errno = 0; + char* end = NULL; + long value = strtol(buffer, &end, BASE_VALUE); + + if (errno == ERANGE || end == buffer) + return TIME_UNLIMITED; + if (value < 0) + value = 0; + if (value > INT_MAX) + value = INT_MAX; + + return (int)value; +} + +static void write_csv_result(FILE* csv, + const char* timestamp, + const char* general_q, + const char* major_q, + const bool correct_answer, + const double duration) +{ + static const char* CSV_FORMAT = "\"%s\",\"%s\",\"%s\",%c,%.0f\n"; + fprintf(csv, + CSV_FORMAT, + timestamp, + general_q, + major_q, + correct_answer ? 'y' : 'n', + duration); +} + +void print_score(const size_t correct, size_t total) +{ + double percentage = 0.0; + if (total != 0) + percentage = (double)correct / total * PERCENT_MULTIPLIER; + + printf(SCORE_FORMAT, correct, total, percentage); +} + +void print_final_score(const QuizSession* session) +{ + double final_percentage = 0.0; + if (session->total_answered > 0) + final_percentage = (double)session->total_correct + / session->total_answered + * PERCENT_MULTIPLIER; + + printf("\nSession score: %.1f%%\n", final_percentage); +} + +// ------------------------ +// Initialization & Cleanup +// ------------------------ +bool initialize_quiz_session(QuizSession* session) +{ + session->seed = (unsigned int)time(NULL); + srand(session->seed); + + if (!ensure_data_directory_exists()) { + fprintf(stderr, "%s", DATA_DIR_ERROR); + return false; + } + + char questions_path[FILE_PATH_BUFFER]; + snprintf(questions_path, + sizeof questions_path, + "%s/%s", + QUIZ_DATA_DIRECTORY, + QUESTIONS_FILE_NAME); + + char results_path[FILE_PATH_BUFFER]; + snprintf(results_path, + sizeof results_path, + "%s/%s", + QUIZ_DATA_DIRECTORY, + RESULTS_FILE_NAME); + + if (load_questions(questions_path, &session->questions) + != EXIT_SUCCESS) { + fprintf(stderr, "%s", QUESTIONS_LOAD_ERROR); + return false; + } + + session->csv = fopen(results_path, "a"); + if (!session->csv) { + perror(CSV_OPEN_ERROR_MESSAGE); + free_questions(&session->questions); + return false; + } + + session->total_answered = 0; + session->total_correct = 0; + session->time_limit = read_time_limit(); + + return true; +} + +void cleanup_session(QuizSession* session) +{ + fclose(session->csv); + free_questions(&session->questions); +} + +// ------------------------ +// Single Quiz Iteration +// ------------------------ +bool quiz_iteration(QuizSession* session) +{ + const size_t general_index = get_random_question_index( + &session->seed, session->questions.general_count); + const size_t major_index = get_random_question_index( + &session->seed, session->questions.major_count); + + const char* general_question = + session->questions.general[general_index]; + const char* major_question = session->questions.major[major_index]; + + print_line(); + printf( + "GENERAL:\n%s\n\nMAJOR:\n%s\n", general_question, major_question); + print_line(); + + wait_for_enter(START_PROMPT); + const time_t start_time = time(NULL); + + wait_for_enter(DONE_PROMPT); + const double duration_seconds = difftime(time(NULL), start_time); + + if (session->time_limit > 0 && duration_seconds > session->time_limit) + printf("%s", TIME_EXCEEDED_MESSAGE); + + const bool correct = ask_yes_no(CORRECT_PROMPT); + if (correct) + session->total_correct++; + session->total_answered++; + + char timestamp[CSV_TIMESTAMP_BUFFER]; + get_answer_timestamp(timestamp, sizeof timestamp); + + write_csv_result(session->csv, + timestamp, + general_question, + major_question, + correct, + duration_seconds); + + print_score(session->total_correct, session->total_answered); + + return ask_yes_no(NEXT_QUESTION_PROMPT); +} diff --git a/src/utils.c b/src/utils.c index c139929..2e26e8e 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,34 +1,117 @@ #include "utils.h" +#include #include #include #include -void print_line(void) { - printf("--------------------------------------------------\n"); +#define SEPARATOR_CHAR '-' +#define SEPARATOR_WIDTH 50 +#define NEWLINE_CHAR '\n' +#define INPUT_BUFFER_SIZE 16 +#define TIME_FORMAT_STRING "%Y-%m-%d %H:%M:%S" +#define ASK_YES_NO_PROMPT "Please enter 'y' or 'n': " + +/** Clear leftover input from stdin */ +static void flush_stdin(void) +{ + int input_char; + do + input_char = getchar(); + while (input_char != NEWLINE_CHAR && input_char != EOF); +} + +/** Print a horizontal separator line for the quiz UI */ +static void print_separator_line(void) +{ + for (size_t current_column = 0; current_column < SEPARATOR_WIDTH; + ++current_column) + putchar(SEPARATOR_CHAR); + + putchar(NEWLINE_CHAR); } -void wait_enter(const char *msg) { - if (msg) - printf("%s", msg); - int c; - while ((c = getchar()) != '\n' && c != EOF) - ; +/** Print a horizontal separator line */ +void print_line(void) +{ + print_separator_line(); } -char ask_yes_no(const char *msg) { - char c = 'n'; - printf("%s", msg); - if (scanf(" %c", &c) != 1) - c = 'n'; - int flush; - while ((flush = getchar()) != '\n' && flush != EOF) - ; - return c; +/** + * Pause until the user presses ENTER + * + * @param prompt Optional message to display before waiting + */ +void wait_for_enter(const char* prompt) +{ + if (prompt != NULL) + fputs(prompt, stdout); + + flush_stdin(); +} + +/** + * Ask a yes/no question and return a boolean answer + * + * @param prompt Question message to display + * @return true if user answered 'y' or 'Y', false if 'n', default false + */ +bool ask_yes_no(const char* prompt) +{ + char input_line[INPUT_BUFFER_SIZE]; + + while (1) { + if (prompt != NULL) + fputs(prompt, stdout); + + if (fgets(input_line, sizeof(input_line), stdin) == NULL) + return false; + + const char first_char = input_line[0]; + + if (first_char == 'y' || first_char == 'Y') + return true; + + if (first_char == 'n' || first_char == 'N') + return false; + + fputs(ASK_YES_NO_PROMPT, stdout); + } } -void now_str(char *buf, size_t size) { - time_t t = time(NULL); - strftime(buf, size, "%Y-%m-%d %H:%M:%S", localtime(&t)); +/** + * Get the current timestamp for recording when a question was answered + * + * @param answer_time_buffer Buffer to store the formatted timestamp + * @param buffer_capacity Size of the buffer in bytes + */ +void get_answer_timestamp(char* answer_time_buffer, + const size_t buffer_capacity) +{ + if (answer_time_buffer == NULL || buffer_capacity == 0) + return; + + const time_t current_time = time(NULL); + const struct tm* local_time = localtime(¤t_time); + if (local_time != NULL) + strftime(answer_time_buffer, + buffer_capacity, + TIME_FORMAT_STRING, + local_time); } -size_t rand_index(size_t max) { return max ? (size_t)(rand() % max) : 0; } +/** + * Get a random question index in the quiz (thread-safe) + * + * @param seed Pointer to an unsigned int seed (per-thread) + * @param total_questions Number of questions in the current quiz session + * @return Random index in the range [0, total_questions-1] + */ +size_t get_random_question_index(unsigned int* seed, + const size_t total_questions) +{ + if (total_questions == 0 || seed == NULL) + return 0; + + const unsigned long random_value = (unsigned long)rand_r(seed); + return random_value % total_questions; +} diff --git a/tests/test_questions.c b/tests/test_questions.c index c605c1a..334ab86 100644 --- a/tests/test_questions.c +++ b/tests/test_questions.c @@ -1,24 +1,26 @@ -#include "questions.h" +#include "../include/questions.h" #include #include -int main(void) { - QuestionSet qs; +int main(void) +{ + QuestionSet qs; - if (load_questions("data/questions.txt", &qs) != EXIT_SUCCESS) { - fprintf(stderr, "TEST FAILED: cannot load file\n"); - return EXIT_FAILURE; - } + if (load_questions("data/questions.txt", &qs) != EXIT_SUCCESS) { + fprintf(stderr, "TEST FAILED: cannot load file\n"); + return EXIT_FAILURE; + } - if (qs.general_count == 0 || qs.major_count == 0) { - fprintf(stderr, "TEST FAILED: empty sets\n"); - free_questions(&qs); - return EXIT_FAILURE; - } + if (qs.general_count == 0 || qs.major_count == 0) { + fprintf(stderr, "TEST FAILED: empty sets\n"); + free_questions(&qs); + return EXIT_FAILURE; + } - printf("TEST OK: loaded %zu general, %zu major\n", qs.general_count, - qs.major_count); + printf("TEST OK: loaded %zu general, %zu major\n", + qs.general_count, + qs.major_count); - free_questions(&qs); - return EXIT_SUCCESS; + free_questions(&qs); + return EXIT_SUCCESS; } -- cgit v1.2.3