Csv library
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
180
src/core/core_csv.c
Normal 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
34
src/core/core_csv.h
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user