diff --git a/csv_parser.c b/csv_parser.c index 7b8d1ec..b8ed383 100644 --- a/csv_parser.c +++ b/csv_parser.c @@ -220,7 +220,7 @@ CSVParseResult csv_parse_line_inplace(const char *line, Arena *arena, const CSVC return result; } -char* read_full_record(FILE *file, Arena *arena) { +char* read_full_record(FILE *file, Arena *arena, const CSVConfig *config) { if (!file || !arena) { return NULL; } @@ -251,14 +251,14 @@ char* read_full_record(FILE *file, Arena *arena) { record_capacity = new_capacity; } - if (c == '"') { + if (c == config->enclosure) { if (in_quotes) { int next_c = fgetc(file); - if (next_c == '"') { - record[record_len++] = '"'; - record[record_len++] = '"'; + if (next_c == config->enclosure) { + record[record_len++] = config->enclosure; + record[record_len++] = config->enclosure; } else { - record[record_len++] = '"'; + record[record_len++] = config->enclosure; in_quotes = false; if (next_c != EOF) { ungetc(next_c, file); @@ -266,7 +266,7 @@ char* read_full_record(FILE *file, Arena *arena) { } } else { in_quotes = true; - record[record_len++] = '"'; + record[record_len++] = config->enclosure; } } else if (c == '\n' || c == '\r') { if (!in_quotes) { diff --git a/csv_parser.h b/csv_parser.h index 9f6ab91..b47b9b8 100644 --- a/csv_parser.h +++ b/csv_parser.h @@ -59,7 +59,7 @@ typedef struct { ParseContext parse_ctx; } CSVParser; -char* read_full_record(FILE *file, Arena *arena); +char* read_full_record(FILE *file, Arena *arena, const CSVConfig *config); int parse_csv_line(const char *line, char **fields, int max_fields, Arena *arena, const CSVConfig *config); int parse_headers(const char *line, char **fields, int max_fields, Arena *arena, const CSVConfig *config); diff --git a/csv_reader.c b/csv_reader.c index 316d1d8..a7662b4 100644 --- a/csv_reader.c +++ b/csv_reader.c @@ -29,7 +29,7 @@ CSVReader* csv_reader_init_with_config(Arena *persistent_arena, Arena *temp_aren reader->owns_arenas = false; if (config->hasHeader) { - char *line = read_full_record(reader->file, reader->persistent_arena); + char *line = read_full_record(reader->file, reader->persistent_arena, config); if (line) { reader->line_number++; CSVParseResult result = csv_parse_line_inplace(line, reader->persistent_arena, config, reader->line_number); @@ -99,7 +99,7 @@ CSVReader* csv_reader_init_standalone(CSVConfig *config) { reader->owns_arenas = true; if (config->hasHeader) { - char *line = read_full_record(reader->file, reader->persistent_arena); + char *line = read_full_record(reader->file, reader->persistent_arena, config); if (line) { reader->line_number++; CSVParseResult result = csv_parse_line_inplace(line, reader->persistent_arena, config, reader->line_number); @@ -114,14 +114,14 @@ CSVReader* csv_reader_init_standalone(CSVConfig *config) { return reader; } -CSVRecord* csv_reader_next_record(CSVReader *reader) { +CSVRecord* csv_reader_next_record(CSVReader *reader, CSVConfig *config) { if (!reader || !reader->file) { return NULL; } arena_reset(reader->temp_arena); - char *line = read_full_record(reader->file, reader->temp_arena); + char *line = read_full_record(reader->file, reader->temp_arena, config); if (!line) { return NULL; } @@ -191,13 +191,13 @@ char** csv_reader_get_headers(CSVReader *reader, int *header_count) { return NULL; } -void csv_reader_rewind(CSVReader *reader) { +void csv_reader_rewind(CSVReader *reader, CSVConfig *config) { if (reader && reader->file) { rewind(reader->file); reader->line_number = 0; if (reader->config->hasHeader && reader->headers_loaded) { - char *line = read_full_record(reader->file, reader->persistent_arena); + char *line = read_full_record(reader->file, reader->persistent_arena, config); if (line) { reader->line_number = 1; } @@ -216,8 +216,8 @@ int csv_reader_set_config(CSVReader *reader, Arena *persistent_arena, Arena *tem return 1; } -long csv_reader_get_record_count(CSVReader *reader) { - if (!reader || !reader->file) { +long csv_reader_get_record_count(CSVReader *reader, CSVConfig *config) { + if (!reader || !reader->file || !config) { return -1; } @@ -231,7 +231,7 @@ long csv_reader_get_record_count(CSVReader *reader) { long record_count = 0; if (reader->config && reader->config->hasHeader) { - char *header_line = read_full_record(reader->file, reader->persistent_arena); + char *header_line = read_full_record(reader->file, reader->persistent_arena, config); if (!header_line) { fseek(reader->file, current_pos, SEEK_SET); return 0; @@ -239,7 +239,7 @@ long csv_reader_get_record_count(CSVReader *reader) { } while (1) { - char *line = read_full_record(reader->file, reader->persistent_arena); + char *line = read_full_record(reader->file, reader->persistent_arena, config); if (!line) { break; } @@ -273,16 +273,16 @@ long csv_reader_get_position(CSVReader *reader) { return reader->line_number; } -int csv_reader_seek(CSVReader *reader, long position) { - if (!reader || !reader->file || position < 0) { +int csv_reader_seek(CSVReader *reader, long position, CSVConfig *config) { + if (!reader || !reader->file || position < 0 || !config) { return 0; } - csv_reader_rewind(reader); + csv_reader_rewind(reader, config); for (long i = 0; i < position; i++) { arena_reset(reader->temp_arena); - char *line = read_full_record(reader->file, reader->temp_arena); + char *line = read_full_record(reader->file, reader->temp_arena, config); if (!line) { return 0; } diff --git a/csv_reader.h b/csv_reader.h index 29ee752..4d1954e 100644 --- a/csv_reader.h +++ b/csv_reader.h @@ -26,15 +26,15 @@ typedef struct { CSVReader* csv_reader_init_with_config(Arena *persistent_arena, Arena *temp_arena, CSVConfig *config); CSVReader* csv_reader_init_standalone(CSVConfig *config); void csv_reader_free(CSVReader *reader); -CSVRecord* csv_reader_next_record(CSVReader *reader); +CSVRecord* csv_reader_next_record(CSVReader *reader, CSVConfig *config); -void csv_reader_rewind(CSVReader *reader); +void csv_reader_rewind(CSVReader *reader, CSVConfig* config); int csv_reader_set_config(CSVReader *reader, Arena *persistent_arena, Arena *temp_arena, const CSVConfig *config); -long csv_reader_get_record_count(CSVReader *reader); +long csv_reader_get_record_count(CSVReader *reader, CSVConfig* config); long csv_reader_get_position(CSVReader *reader); char** csv_reader_get_headers(CSVReader *reader, int *header_count); -int csv_reader_seek(CSVReader *reader, long position); +int csv_reader_seek(CSVReader *reader, long position, CSVConfig *config); int csv_reader_has_next(CSVReader *reader); #endif diff --git a/tests/test_csv_parser.c b/tests/test_csv_parser.c index bac7fa9..3d1af84 100644 --- a/tests/test_csv_parser.c +++ b/tests/test_csv_parser.c @@ -173,25 +173,27 @@ void test_read_full_record() { rewind(test_file); Arena arena; - assert(arena_create(&arena, 4096) == ARENA_OK); + assert(arena_create(&arena, 4096*2) == ARENA_OK); + + CSVConfig *config = csv_config_create(&arena); // Read first record (should handle multi-line quoted field) - char *record1 = read_full_record(test_file, &arena); + char *record1 = read_full_record(test_file, &arena, config); assert(record1 != NULL); assert(strstr(record1, "field2\nwith newline") != NULL); // Read second record (simple line) - char *record2 = read_full_record(test_file, &arena); + char *record2 = read_full_record(test_file, &arena, config); assert(record2 != NULL); assert(strcmp(record2, "simple,line,here") == 0); // Read third record (multi-line) - char *record3 = read_full_record(test_file, &arena); + char *record3 = read_full_record(test_file, &arena, config); assert(record3 != NULL); assert(strstr(record3, "multi\nline\nfield") != NULL); // No more records - char *record4 = read_full_record(test_file, &arena); + char *record4 = read_full_record(test_file, &arena, config); assert(record4 == NULL); fclose(test_file); diff --git a/tests/test_csv_reader.c b/tests/test_csv_reader.c index aa5bccd..cc9ae9c 100644 --- a/tests/test_csv_reader.c +++ b/tests/test_csv_reader.c @@ -33,21 +33,21 @@ void test_csv_reader_optimized() { assert(strcmp(reader->cached_headers[1], "Age") == 0); assert(strcmp(reader->cached_headers[2], "City") == 0); - CSVRecord *record1 = csv_reader_next_record(reader); + CSVRecord *record1 = csv_reader_next_record(reader, config); assert(record1 != NULL); assert(record1->field_count == 3); assert(strcmp(record1->fields[0], "John") == 0); assert(strcmp(record1->fields[1], "25") == 0); assert(strcmp(record1->fields[2], "New York") == 0); - CSVRecord *record2 = csv_reader_next_record(reader); + CSVRecord *record2 = csv_reader_next_record(reader, config); assert(record2 != NULL); assert(record2->field_count == 3); assert(strcmp(record2->fields[0], "Jane") == 0); assert(strcmp(record2->fields[1], "30") == 0); assert(strcmp(record2->fields[2], "Los Angeles") == 0); - CSVRecord *record3 = csv_reader_next_record(reader); + CSVRecord *record3 = csv_reader_next_record(reader, config); assert(record3 == NULL); csv_reader_free(reader); @@ -56,6 +56,94 @@ void test_csv_reader_optimized() { printf("✓ Optimized CSV reader test passed\n"); } +void test_csv_reader_custom_enclosure() { + printf("Testing custom enclosure...\n"); + const char *test_content = "|N\"a||me|,|Age|,|City|\n|John|,|25|,|New York|\n|Jane|,|30|,|Los Angeles|\n"; + + Arena arena; + assert(arena_create(&arena, 4096) == ARENA_OK); + CSVConfig *config = csv_config_create(&arena); + csv_config_set_path(config, "test_custom_enclosure.csv"); + csv_config_set_has_header(config, true); + csv_config_set_enclosure(config, '|'); + + create_test_csv_file("test_custom_enclosure.csv", test_content); + + CSVReader *reader = csv_reader_init_standalone(config); + assert(reader != NULL); + assert(reader->headers_loaded == true); + assert(reader->cached_header_count == 3); + assert(strcmp(reader->cached_headers[0], "N\"a|me") == 0); + assert(strcmp(reader->cached_headers[1], "Age") == 0); + assert(strcmp(reader->cached_headers[2], "City") == 0); + + CSVRecord *record1 = csv_reader_next_record(reader, config); + assert(record1 != NULL); + assert(record1->field_count == 3); + assert(strcmp(record1->fields[0], "John") == 0); + assert(strcmp(record1->fields[1], "25") == 0); + assert(strcmp(record1->fields[2], "New York") == 0); + + CSVRecord *record2 = csv_reader_next_record(reader, config); + assert(record2 != NULL); + assert(record2->field_count == 3); + assert(strcmp(record2->fields[0], "Jane") == 0); + assert(strcmp(record2->fields[1], "30") == 0); + assert(strcmp(record2->fields[2], "Los Angeles") == 0); + + CSVRecord *record3 = csv_reader_next_record(reader, config); + assert(record3 == NULL); + + csv_reader_free(reader); + arena_destroy(&arena); + remove("test_custom_enclosure.csv"); + printf("✓ Custom enclosure test passed\n"); +} + +void test_csv_reader_custom_delimiter() { + printf("Testing custom delimiter...\n"); + const char *test_content = "Name|Age|City\nJohn|25|New York\nJane|30|Los Angeles\n"; + + Arena arena; + assert(arena_create(&arena, 4096) == ARENA_OK); + CSVConfig *config = csv_config_create(&arena); + csv_config_set_path(config, "test_custom_delimiter.csv"); + csv_config_set_has_header(config, true); + csv_config_set_delimiter(config, '|'); + + create_test_csv_file("test_custom_delimiter.csv", test_content); + + CSVReader *reader = csv_reader_init_standalone(config); + assert(reader != NULL); + assert(reader->headers_loaded == true); + assert(reader->cached_header_count == 3); + assert(strcmp(reader->cached_headers[0], "Name") == 0); + assert(strcmp(reader->cached_headers[1], "Age") == 0); + assert(strcmp(reader->cached_headers[2], "City") == 0); + + CSVRecord *record1 = csv_reader_next_record(reader, config); + assert(record1 != NULL); + assert(record1->field_count == 3); + assert(strcmp(record1->fields[0], "John") == 0); + assert(strcmp(record1->fields[1], "25") == 0); + assert(strcmp(record1->fields[2], "New York") == 0); + + CSVRecord *record2 = csv_reader_next_record(reader, config); + assert(record2 != NULL); + assert(record2->field_count == 3); + assert(strcmp(record2->fields[0], "Jane") == 0); + assert(strcmp(record2->fields[1], "30") == 0); + assert(strcmp(record2->fields[2], "Los Angeles") == 0); + + CSVRecord *record3 = csv_reader_next_record(reader, config); + assert(record3 == NULL); + + csv_reader_free(reader); + arena_destroy(&arena); + remove("test_custom_delimiter.csv"); + printf("✓ Custom delimiter test passed\n"); +} + void test_csv_reader_get_headers() { printf("Testing csv_reader_get_headers...\n"); const char *test_content = "ID,Name,Email\n1,Alice,alice@example.com\n2,Bob,bob@example.com\n"; @@ -99,18 +187,18 @@ void test_csv_reader_rewind() { assert(reader != NULL); // Read first record - CSVRecord *record1 = csv_reader_next_record(reader); + CSVRecord *record1 = csv_reader_next_record(reader, config); assert(record1 != NULL); assert(strcmp(record1->fields[0], "Alice") == 0); // Read second record - CSVRecord *record2 = csv_reader_next_record(reader); + CSVRecord *record2 = csv_reader_next_record(reader, config); assert(record2 != NULL); assert(strcmp(record2->fields[0], "Bob") == 0); // Rewind and read first record again - csv_reader_rewind(reader); - CSVRecord *record_after_rewind = csv_reader_next_record(reader); + csv_reader_rewind(reader, config); + CSVRecord *record_after_rewind = csv_reader_next_record(reader, config); assert(record_after_rewind != NULL); assert(strcmp(record_after_rewind->fields[0], "Alice") == 0); @@ -138,17 +226,17 @@ void test_csv_reader_has_next() { assert(csv_reader_has_next(reader) == 1); // Read first record - CSVRecord *record1 = csv_reader_next_record(reader); + CSVRecord *record1 = csv_reader_next_record(reader, config); assert(record1 != NULL); assert(csv_reader_has_next(reader) == 1); // Read second record - CSVRecord *record2 = csv_reader_next_record(reader); + CSVRecord *record2 = csv_reader_next_record(reader, config); assert(record2 != NULL); assert(csv_reader_has_next(reader) == 0); // No more records - CSVRecord *record3 = csv_reader_next_record(reader); + CSVRecord *record3 = csv_reader_next_record(reader, config); assert(record3 == NULL); assert(csv_reader_has_next(reader) == 0); @@ -173,16 +261,16 @@ void test_csv_reader_seek() { assert(reader != NULL); // Seek to position 2 (3rd data record) - int seek_result = csv_reader_seek(reader, 2); + int seek_result = csv_reader_seek(reader, 2, config); assert(seek_result == 1); // Should now read Charlie - CSVRecord *record = csv_reader_next_record(reader); + CSVRecord *record = csv_reader_next_record(reader, config); assert(record != NULL); assert(strcmp(record->fields[0], "Charlie") == 0); // Test seeking beyond available records - int invalid_seek = csv_reader_seek(reader, 100); + int invalid_seek = csv_reader_seek(reader, 100, config); assert(invalid_seek == 0); csv_reader_free(reader); @@ -209,12 +297,12 @@ void test_csv_reader_position() { assert(csv_reader_get_position(reader) == 1); // Read first record - CSVRecord *record1 = csv_reader_next_record(reader); + CSVRecord *record1 = csv_reader_next_record(reader, config); assert(record1 != NULL); assert(csv_reader_get_position(reader) == 2); // Read second record - CSVRecord *record2 = csv_reader_next_record(reader); + CSVRecord *record2 = csv_reader_next_record(reader, config); assert(record2 != NULL); assert(csv_reader_get_position(reader) == 3); @@ -268,11 +356,11 @@ void test_csv_reader_null_safety() { assert(csv_reader_get_headers(NULL, &header_count) == NULL); assert(csv_reader_get_headers(NULL, NULL) == NULL); - csv_reader_rewind(NULL); // Should not crash + csv_reader_rewind(NULL, NULL); // Should not crash - assert(csv_reader_get_record_count(NULL) == -1); + assert(csv_reader_get_record_count(NULL, NULL) == -1); assert(csv_reader_get_position(NULL) == -1); - assert(csv_reader_seek(NULL, 0) == 0); + assert(csv_reader_seek(NULL, 0, NULL) == 0); assert(csv_reader_has_next(NULL) == 0); csv_reader_free(NULL); // Should not crash @@ -296,7 +384,7 @@ void test_csv_reader_get_record_count() { CSVReader *reader = csv_reader_init_standalone(config); assert(reader != NULL); - long count = csv_reader_get_record_count(reader); + long count = csv_reader_get_record_count(reader, config); assert(count == 3); // Should count 3 data records, excluding header csv_reader_free(reader); @@ -315,7 +403,7 @@ void test_csv_reader_get_record_count() { reader = csv_reader_init_standalone(config); assert(reader != NULL); - count = csv_reader_get_record_count(reader); + count = csv_reader_get_record_count(reader, config); assert(count == 3); // Should count 3 records, no header to skip csv_reader_free(reader); @@ -333,7 +421,7 @@ void test_csv_reader_get_record_count() { reader = csv_reader_init_standalone(config); assert(reader != NULL); - count = csv_reader_get_record_count(reader); + count = csv_reader_get_record_count(reader, config); assert(count == 0); // Empty file should return 0 csv_reader_free(reader); @@ -353,7 +441,7 @@ void test_csv_reader_get_record_count() { reader = csv_reader_init_standalone(config); assert(reader != NULL); - count = csv_reader_get_record_count(reader); + count = csv_reader_get_record_count(reader, config); assert(count == 3); // Should skip empty lines and count 3 data records csv_reader_free(reader); @@ -366,6 +454,8 @@ void test_csv_reader_get_record_count() { int main() { printf("Running CSV Reader tests...\n\n"); test_csv_reader_optimized(); + test_csv_reader_custom_delimiter(); + test_csv_reader_custom_enclosure(); test_csv_reader_get_headers(); test_csv_reader_rewind(); test_csv_reader_has_next();