diff --git a/src/core/core.c b/src/core/core.c index 64e283e..95e74b1 100644 --- a/src/core/core.c +++ b/src/core/core.c @@ -7,6 +7,7 @@ #include "core_string.c" #include "core_string16.c" #include "core_json.c" +#include "core_csv.c" #include "core_log.c" #include "core_lexer.c" #include "core_type_info.c" diff --git a/src/core/core.h b/src/core/core.h index 775be24..2ef7edc 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -4,6 +4,7 @@ #include "core_string.h" #include "core_string16.h" #include "core_json.h" +#include "core_csv.h" #include "core_math.h" #include "core_type_info.h" #include "core_lexer.h" diff --git a/src/core/core_csv.c b/src/core/core_csv.c new file mode 100644 index 0000000..72f517e --- /dev/null +++ b/src/core/core_csv.c @@ -0,0 +1,180 @@ +#include "core_csv.h" +#include "core_log.h" + +typedef struct csv_field_node_t csv_field_node_t; +struct csv_field_node_t { + csv_field_node_t *next; + s8_t field; +}; + +fn b32 csv_at_end(csv_parser_t *it) { + b32 result = it->at >= it->end; + return result; +} + +fn b32 csv_starts_with(csv_parser_t *it, s8_t string) { + if (string.len == 0) return false; + if (it->end - it->at < string.len) return false; + b32 result = s8_are_equal(s8_make(it->at, string.len), string); + return result; +} + +fn void csv_consume(csv_parser_t *it, i64 len) { + it->at += len; +} + +fn i64 csv_line_end_len(csv_parser_t *it) { + i64 result = 0; + if (it->line_terminator.len) { + if (csv_starts_with(it, it->line_terminator)) result = it->line_terminator.len; + } else if (!csv_at_end(it)) { + if (it->at[0] == '\r' && it->end - it->at >= 2 && it->at[1] == '\n') result = 2; + else if (it->at[0] == '\n') result = 1; + } + return result; +} + +fn void csv_errorf(csv_parser_t *it, const char *fmt, ...) { + if (it->has_error) return; + it->has_error = true; + it->is_valid = false; + + va_list args; + va_start(args, fmt); + s8_t msg = s8_vfmt(it->arena, fmt, args); + va_end(args); + + it->error = s8_printf(it->arena, "csv row %d: %S", it->row, msg); + errorf("%S", it->error); +} + +fn void csv_sb_putc(sb8_t *sb, char c) { + char *p = ma_push_type(sb->arena, char); + *p = c; + sb8_append(sb, s8_make(p, 1)); +} + +fn s8_t csv_parse_field(csv_parser_t *it) { + if (!csv_at_end(it) && it->at[0] == '"') { + // Quoted field. The outer quotes are removed and doubled quotes are + // unescaped. Quoted fields may contain delimiters and line endings. + csv_consume(it, 1); + sb8_t sb = {.arena = it->arena}; + + for (;;) { + if (csv_at_end(it)) { + csv_errorf(it, "unterminated quoted field"); + return s8_null; + } + + if (it->at[0] == '"') { + csv_consume(it, 1); + if (!csv_at_end(it) && it->at[0] == '"') { + csv_sb_putc(&sb, '"'); + csv_consume(it, 1); + continue; + } + break; + } + + csv_sb_putc(&sb, it->at[0]); + csv_consume(it, 1); + } + + s8_t result = sb8_merge(it->arena, &sb); + return result; + } + + // Unquoted field is a view into the input buffer. + char *first = it->at; + while (!csv_at_end(it) && !csv_starts_with(it, it->delimiter) && csv_line_end_len(it) == 0) { + csv_consume(it, 1); + } + s8_t result = s8_from_range(first, it->at); + return result; +} + +fn void csv_clear_row(csv_parser_t *it) { + if (it->has_row_temp) { + ma_end_temp(it->row_temp); + it->has_row_temp = false; + } + it->data = NULL; + it->len = 0; +} + +fn csv_parser_t csv_iter(ma_arena_t *arena, s8_t stream, csv_params_t params) { + assert(arena); + csv_parser_t result = {0}; + result.arena = arena; + result.at = stream.str; + result.end = stream.str + stream.len; + result.delimiter = params.delimiter.len ? params.delimiter : s8(","); + result.line_terminator = params.line_terminator; + result.row = 0; + + assert(result.delimiter.len > 0); + csv_advance(&result); + return result; +} + +fn b32 csv_is_valid(csv_parser_t *it) { + b32 result = it->is_valid && !it->has_error; + return result; +} + +fn void csv_advance(csv_parser_t *it) { + csv_clear_row(it); + if (it->has_error || csv_at_end(it)) { + it->is_valid = false; + return; + } + + it->row += 1; + it->row_temp = ma_begin_temp(it->arena); + it->has_row_temp = true; + + csv_field_node_t *first = NULL; + csv_field_node_t *last = NULL; + i32 count = 0; + + for (;;) { + s8_t field = csv_parse_field(it); + if (it->has_error) return; + + csv_field_node_t *node = ma_push_type(it->arena, csv_field_node_t); + node->field = field; + SLLQ_APPEND(first, last, node); + count += 1; + + if (csv_starts_with(it, it->delimiter)) { + csv_consume(it, it->delimiter.len); + continue; + } + + i64 line_end_len = csv_line_end_len(it); + if (line_end_len) { + csv_consume(it, line_end_len); + break; + } + + if (csv_at_end(it)) break; + + csv_errorf(it, "expected delimiter or line terminator"); + return; + } + + // Copy the scratch-like linked list representation into a tight row array. + it->data = ma_push_array(it->arena, s8_t, count); + it->len = count; + i32 idx = 0; + for (csv_field_node_t *node = first; node; node = node->next) { + it->data[idx++] = node->field; + } + it->is_valid = true; +} + +fn void csv_end(csv_parser_t *it) { + csv_clear_row(it); + it->is_valid = false; +} diff --git a/src/core/core_csv.h b/src/core/core_csv.h new file mode 100644 index 0000000..59c5367 --- /dev/null +++ b/src/core/core_csv.h @@ -0,0 +1,34 @@ +#pragma once +#include "core_basic.h" +#include "core_arena.h" +#include "core_string.h" + +typedef struct csv_params_t csv_params_t; +struct csv_params_t { + s8_t delimiter; // defaults to "," + s8_t line_terminator; // defaults to accepting "\n" and "\r\n" +}; + +typedef struct csv_parser_t csv_parser_t; +struct csv_parser_t { + ma_arena_t *arena; + ma_temp_t row_temp; + b32 has_row_temp; + + char *at; + char *end; + s8_t delimiter; + s8_t line_terminator; + + s8_t *data; + i32 len; + i32 row; + b32 is_valid; + b32 has_error; + s8_t error; +}; + +fn csv_parser_t csv_iter(ma_arena_t *arena, s8_t stream, csv_params_t params); +fn b32 csv_is_valid(csv_parser_t *it); +fn void csv_advance(csv_parser_t *it); +fn void csv_end(csv_parser_t *it); diff --git a/src/testing/testing_main.c b/src/testing/testing_main.c index 233cd53..6aeb8ea 100644 --- a/src/testing/testing_main.c +++ b/src/testing/testing_main.c @@ -131,6 +131,62 @@ fn void test_json(void) { ma_destroy(arena); } +fn void test_csv(void) { + ma_arena_t *arena = ma_create(ma_default_reserve_size); + + s8_t stream = s8("name,age,note\nBob,32,hello\nAlice,27,\"hi, there\"\n\"Q\"\"uote\"\"\",,tail\n"); + csv_params_t params = {0}; + + i32 row = 0; + for (csv_parser_t it = csv_iter(arena, stream, params); csv_is_valid(&it); csv_advance(&it)) { + if (row == 0) { + assert(it.len == 3); + assert(s8_are_equal(it.data[0], s8("name"))); + assert(s8_are_equal(it.data[1], s8("age"))); + assert(s8_are_equal(it.data[2], s8("note"))); + } else if (row == 1) { + assert(it.len == 3); + assert(s8_are_equal(it.data[0], s8("Bob"))); + assert(s8_are_equal(it.data[1], s8("32"))); + assert(s8_are_equal(it.data[2], s8("hello"))); + } else if (row == 2) { + assert(it.len == 3); + assert(s8_are_equal(it.data[0], s8("Alice"))); + assert(s8_are_equal(it.data[1], s8("27"))); + assert(s8_are_equal(it.data[2], s8("hi, there"))); + } else if (row == 3) { + assert(it.len == 3); + assert(s8_are_equal(it.data[0], s8("Q\"uote\""))); + assert(s8_are_equal(it.data[1], s8(""))); + assert(s8_are_equal(it.data[2], s8("tail"))); + } + row += 1; + } + assert(row == 4); + + s8_t crlf = s8("a,b\r\nc,d\r\n"); + row = 0; + for (csv_parser_t it = csv_iter(arena, crlf, params); csv_is_valid(&it); csv_advance(&it)) { + assert(it.len == 2); + if (row == 0) assert(s8_are_equal(it.data[1], s8("b"))); + if (row == 1) assert(s8_are_equal(it.data[1], s8("d"))); + row += 1; + } + assert(row == 2); + + s8_t semi = s8("a;b;\n1;2;\n"); + params.delimiter = s8(";"); + row = 0; + for (csv_parser_t it = csv_iter(arena, semi, params); csv_is_valid(&it); csv_advance(&it)) { + assert(it.len == 3); + assert(s8_are_equal(it.data[2], s8(""))); + row += 1; + } + assert(row == 2); + + ma_destroy(arena); +} + fn void test_s8(void) { ma_arena_t *arena = ma_create(ma_default_reserve_size); @@ -231,6 +287,7 @@ int main() { os_core_init(); test_s8(); test_json(); + test_csv(); os_test(); return 0; }