diff --git a/README.md b/README.md index 8382e6e..ef97443 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,8 @@ next call to `next()`. Copy it if you need to keep it. - Quoted fields may contain the column separator, the row separator, and the quote character itself when doubled (`"He said ""hi"""`). - The column separator and quote are configurable, but **only as single - bytes** — a multi-byte column separator is not supported. + bytes** — a multi-byte column separator is not supported. Setting `quote` + makes that byte the quote and leaves `"` as ordinary field data. - The field buffer must be longer than the longest field in the input; otherwise `next()` fails with `error.ShortBuffer`. - An empty line is not skipped: it yields a single zero-length `field` diff --git a/src/main.zig b/src/main.zig index 0edc722..c8d1d41 100644 --- a/src/main.zig +++ b/src/main.zig @@ -148,7 +148,7 @@ pub const CsvReader = struct { if (c == quote) { // double quotes, shift forward // print("PEEK {c}\n", .{buffer[idx + 1]}); - if (idx < self.current.len - 1 and self.current[idx + 1] == '"') { + if (idx < self.current.len - 1 and self.current[idx + 1] == quote) { // print("DOUBLE QUOTES\n", .{}); contains_quotes = true; idx += 1; @@ -265,7 +265,7 @@ pub const CsvTokenizer = struct { }, } - terminal_chars[len] = '"'; + terminal_chars[len] = config.quote; len += 1; var terminal_set: CsvReader.TerminalSet = @splat(false); @@ -408,7 +408,7 @@ pub const CsvTokenizer = struct { fn parseField(self: *Self) !CsvToken { const first = (try self.reader.peek()).?; - if (first != '"') { + if (first != self.config.quote) { var field = try self.reader.untilAny(&self.terminal_set); while (field == null) { // No terminator among what has been read so far, which means diff --git a/test/csv_tokenizer.zig b/test/csv_tokenizer.zig index 34c4a02..db7fe4b 100644 --- a/test/csv_tokenizer.zig +++ b/test/csv_tokenizer.zig @@ -366,3 +366,22 @@ test "An unclosed quoted field is an error" { try expectToken(csv_mod.CsvToken{ .field = "1" }, try csv.next()); try testing.expectError(csv_mod.CsvError.ShortBuffer, csv.next()); } + +test "A configured quote is honored in place of the default" { + const single: csv_mod.CsvConfig = .{ .quote = '\'' }; + + try expectStream("'a,b',c\n", single, 64, &.{ "a,b", "c", null }); + try expectStream("'a''b'\n", single, 64, &.{ "a'b", null }); + // The default quote becomes ordinary data once it is not the quote. + try expectStream("a\"b,c\n", single, 64, &.{ "a\"b", "c", null }); + try expectStream("'a;b';c\n", .{ .quote = '\'', .col_sep = ';' }, 64, &.{ "a;b", "c", null }); +} + +test "A configured quote still rejects what the default would" { + var reader = std.Io.Reader.fixed("'a'x\n"); + var buffer: [64]u8 = undefined; + var csv = try getTokenizer(&reader, &buffer, .{ .quote = '\'' }); + + try expectToken(csv_mod.CsvToken{ .field = "a" }, try csv.next()); + try testing.expectError(csv_mod.CsvError.NoSeparatorAfterField, csv.next()); +}