Csv library

This commit is contained in:
KK
2026-07-03 01:40:22 +02:00
parent 68f9136ba5
commit 0bea393c13
5 changed files with 273 additions and 0 deletions

View File

@@ -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"

View File

@@ -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"

180
src/core/core_csv.c Normal file
View File

@@ -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;
}

34
src/core/core_csv.h Normal file
View File

@@ -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);

View File

@@ -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;
}