std/csv/stringify_test.ts

584 lines
16 KiB
TypeScript

// Test ported from Golang
// https://github.com/golang/go/blob/2cc15b1/src/encoding/csv/reader_test.go
// Copyright 2011 The Go Authors. All rights reserved. BSD license.
// https://github.com/golang/go/blob/master/LICENSE
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import {
assert,
assertEquals,
assertStringIncludes,
assertThrows,
} from "@std/assert";
import { stringify } from "./stringify.ts";
const CRLF = "\r\n";
const BYTE_ORDER_MARK = "\ufeff";
Deno.test({
name: "stringify() handles various inputs",
async fn(t) {
await t.step({
name: "Access array index using string",
fn() {
const columns = ["a"];
const data = [["foo"], ["bar"]];
const errorMessage = 'Property accessor is not of type "number"';
assertThrows(
() => stringify(data, { columns }),
TypeError,
errorMessage,
);
},
});
await t.step(
{
name: "Double quote in separator",
fn() {
const columns = [0];
const data = [["foo"], ["bar"]];
const errorMessage = [
"Separator cannot include the following strings:",
' - U+0022: Quotation mark (")',
" - U+000D U+000A: Carriage Return + Line Feed (\\r\\n)",
].join("\n");
const options = { separator: '"', columns };
assertThrows(
() => stringify(data, options),
TypeError,
errorMessage,
);
},
},
);
await t.step(
{
name: "CRLF in separator",
fn() {
const columns = [0];
const data = [["foo"], ["bar"]];
const errorMessage = [
"Separator cannot include the following strings:",
' - U+0022: Quotation mark (")',
" - U+000D U+000A: Carriage Return + Line Feed (\\r\\n)",
].join("\n");
const options = { separator: "\r\n", columns };
assertThrows(
() => stringify(data, options),
TypeError,
errorMessage,
);
},
},
);
await t.step(
{
name: "Invalid data, no columns",
fn() {
const data = [{ a: 1 }, { a: 2 }];
assertThrows(
() => stringify(data),
TypeError,
"No property accessor function was provided for object",
);
},
},
);
await t.step(
{
name: "Invalid data, no columns",
fn() {
const data = [{ a: 1 }, { a: 2 }];
assertThrows(
() => stringify(data),
TypeError,
"No property accessor function was provided for object",
);
},
},
);
await t.step(
{
name: "No data, no columns",
fn() {
const columns: string[] = [];
const data: string[][] = [];
const output = "";
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "No data, no columns, no headers",
fn() {
const columns: string[] = [];
const data: string[][] = [];
const output = ``;
const options = { headers: false, columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "No data, columns",
fn() {
const columns = ["a"];
const data: string[][] = [];
const output = `a${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "No data, columns, no headers",
fn() {
const columns = ["a"];
const data: string[][] = [];
const output = ``;
const options = { headers: false, columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "Separator: CR",
fn() {
const columns = [0, 1];
const data = [["foo", "bar"], ["baz", "qux"]];
const output = `0\r1${CRLF}foo\rbar${CRLF}baz\rqux${CRLF}`;
const options = { separator: "\r", columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "Separator: LF",
fn() {
const columns = [0, 1];
const data = [["foo", "bar"], ["baz", "qux"]];
const output = `0\n1${CRLF}foo\nbar${CRLF}baz\nqux${CRLF}`;
const options = { separator: "\n", columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "Column: number accessor",
fn() {
const columns = [1];
const data = [{ 1: 1 }, { 1: 2 }];
const output = `1${CRLF}1${CRLF}2${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Explicit header value, no headers",
fn() {
const columns = [{ header: "Value", prop: "value" }];
const data = [{ value: "foo" }, { value: "bar" }];
const output = `foo${CRLF}bar${CRLF}`;
const options = { headers: false, columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "Column: number accessor,const data = array",
fn() {
const columns = [1];
const data = [["key", "foo"], ["key", "bar"]];
const output = `1${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: array number accessor",
fn() {
const columns = [[1]];
const data = [{ 1: 1 }, { 1: 2 }];
const output = `1${CRLF}1${CRLF}2${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: array number accessor,const data = array",
fn() {
const columns = [[1]];
const data = [["key", "foo"], ["key", "bar"]];
const output = `1${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: array number accessor,const data = array",
fn() {
const columns = [[1, 1]];
const data = [["key", ["key", "foo"]], ["key", ["key", "bar"]]];
const output = `1${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: string accessor",
fn() {
const columns = ["value"];
const data = [{ value: "foo" }, { value: "bar" }];
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: array string accessor",
fn() {
const columns = [["value"]];
const data = [{ value: "foo" }, { value: "bar" }];
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: array string accessor",
fn() {
const columns = [["msg", "value"]];
const data = [{ msg: { value: "foo" } }, { msg: { value: "bar" } }];
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Column: array string accessor via prop field",
fn() {
const columns = [{ prop: ["msg", "value"] }];
const data = [{ msg: { value: "foo" } }, { msg: { value: "bar" } }];
const output = `value${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Explicit header",
fn() {
const columns = [
{
header: "Value",
prop: ["msg", "value"],
},
];
const data = [{ msg: { value: "foo" } }, { msg: { value: "bar" } }];
const output = `Value${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: object",
fn() {
const columns = [0];
const data = [[{ value: "foo" }], [{ value: "bar" }]];
const output =
`0${CRLF}"{""value"":""foo""}"${CRLF}"{""value"":""bar""}"${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: arary of objects",
fn() {
const columns = [0];
const data = [
[[{ value: "foo" }, { value: "bar" }]],
[[{ value: "baz" }, { value: "qux" }]],
];
const output =
`0${CRLF}"[{""value"":""foo""},{""value"":""bar""}]"${CRLF}"[{""value"":""baz""},{""value"":""qux""}]"${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: array",
fn() {
const columns = [0];
const data = [[["foo", "bar"]], [["baz", "qux"]]];
const output =
`0${CRLF}"[""foo"",""bar""]"${CRLF}"[""baz"",""qux""]"${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: array, separator: tab",
fn() {
const columns = [0];
const data = [[["foo", "bar"]], [["baz", "qux"]]];
const output =
`0${CRLF}"[""foo"",""bar""]"${CRLF}"[""baz"",""qux""]"${CRLF}`;
const options = { separator: "\t", columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "Targeted value: undefined",
fn() {
const columns = [0];
const data = [[], []];
const output = `0${CRLF}${CRLF}${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: null",
fn() {
const columns = [0];
const data = [[null], [null]];
const output = `0${CRLF}${CRLF}${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "The case when the entire item is null",
fn() {
const columns = [0];
const data = [null, null];
const output = `0${CRLF}${CRLF}${CRLF}`;
// deno-lint-ignore no-explicit-any
assertEquals(stringify(data as any, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: hex number",
fn() {
const columns = [0];
const data = [[0xa], [0xb]];
const output = `0${CRLF}10${CRLF}11${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: BigInt",
fn() {
const columns = [0];
const data = [[BigInt("1")], [BigInt("2")]];
const output = `0${CRLF}1${CRLF}2${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: boolean",
fn() {
const columns = [0];
const data = [[true], [false]];
const output = `0${CRLF}true${CRLF}false${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: string",
fn() {
const columns = [0];
const data = [["foo"], ["bar"]];
const output = `0${CRLF}foo${CRLF}bar${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: symbol",
fn() {
const columns = [0];
const data = [[Symbol("foo")], [Symbol("bar")]];
const output = `0${CRLF}Symbol(foo)${CRLF}Symbol(bar)${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Targeted value: function",
fn() {
const columns = [0];
const data = [[(n: number) => n]];
const output = `0${CRLF}(n)=>n${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Value with double quote",
fn() {
const columns = [0];
const data = [['foo"']];
const output = `0${CRLF}"foo"""${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Value with CRLF",
fn() {
const columns = [0];
const data = [["foo\r\n"]];
const output = `0${CRLF}"foo\r\n"${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Value with CR",
fn() {
const columns = [0];
const data = [["foo\r"]];
const output = `0${CRLF}foo\r${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Value with LF",
fn() {
const columns = [0];
const data = [["foo\n"]];
const output = `0${CRLF}"foo\n"${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Value with comma",
fn() {
const columns = [0];
const data = [["foo,"]];
const output = `0${CRLF}"foo,"${CRLF}`;
assertEquals(stringify(data, { columns }), output);
},
},
);
await t.step(
{
name: "Value with comma, tab separator",
fn() {
const columns = [0];
const data = [["foo,"]];
const output = `0${CRLF}foo,${CRLF}`;
const options = { separator: "\t", columns };
assertEquals(stringify(data, options), output);
},
},
);
await t.step({
name: "Valid data, no columns",
fn() {
const data = [[1, 2, 3], [4, 5, 6]];
const output = `1,2,3${CRLF}4,5,6${CRLF}`;
assertEquals(stringify(data), output);
},
});
await t.step({
name: "The case when each item is single data, no columns",
fn() {
const data = [1, 2, 3, 4, 5, 6];
const output = `1${CRLF}2${CRLF}3${CRLF}4${CRLF}5${CRLF}6${CRLF}`;
// deno-lint-ignore no-explicit-any
assertEquals(stringify(data as any), output);
},
});
await t.step(
{
name: "byte-order mark with bom=true",
fn() {
const data = [["abc"]];
const output = `${BYTE_ORDER_MARK}abc${CRLF}`;
const options = { headers: false, bom: true };
assertStringIncludes(stringify(data, options), BYTE_ORDER_MARK);
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "no byte-order mark with omitted bom option",
fn() {
const data = [["abc"]];
const output = `abc${CRLF}`;
const options = { headers: false };
assert(!stringify(data, options).includes(BYTE_ORDER_MARK));
assertEquals(stringify(data, options), output);
},
},
);
await t.step(
{
name: "no byte-order mark with bom=false",
fn() {
const data = [["abc"]];
const output = `abc${CRLF}`;
const options = { headers: false, bom: false };
assert(!stringify(data, options).includes(BYTE_ORDER_MARK));
assertEquals(stringify(data, options), output);
},
},
);
},
});