Skip to content

Commit

Permalink
Change generate() function interface to be same as parse(). Change pa…
Browse files Browse the repository at this point in the history
…rse() to parse headers by default
  • Loading branch information
koresar committed Mar 17, 2021
1 parent cea1cea commit bb5833c
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 54 deletions.
80 changes: 49 additions & 31 deletions src/lil-csv.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export const isString = (f) => typeof f === "string";
export const isNumber = (f) => typeof f === "number";
export const isBoolean = (f) => typeof f === "boolean";
export const isDate = (o) => o instanceof Date && !isNaN(o.valueOf());

export const isFunction = (f) => typeof f === "function";

export function parse(str, { header = false, escapeChar = "\\" } = {}) {
export const isString = (v) => typeof v === "string";
export const isNumber = (v) => typeof v === "number";
export const isBoolean = (v) => typeof v === "boolean";
export const isDate = (v) => v instanceof Date && !isNaN(v.valueOf());
export const isObject = (v) => v && typeof v === "object";
export const isFunction = (v) => typeof v === "function";

export function parse(str, { header = true, escapeChar = "\\" } = {}) {
const entries = [];
let quote = false; // 'true' means we're inside a quoted field
let newRow = false; // 'true' means we need to finish this line
Expand Down Expand Up @@ -85,33 +85,51 @@ export function parse(str, { header = false, escapeChar = "\\" } = {}) {
});
}

export function generate({ header, rows, lineTerminator = "\n", escapeChar = "\\" }) {
if (!header) header = "";
else {
if (Array.isArray(header)) header = header.map((h) => (isString(h) && h.includes(",") ? `"${h}"` : h)).join();
if (!isString(header)) throw new Error("Header must be either string or array of strings");
header = header + lineTerminator;
export function generate(rows, { header, lineTerminator = "\n", escapeChar = "\\" } = {}) {
if (header) {
if (isBoolean(header)) {
header = Array.from(rows.reduce((all, row) => new Set([...all, ...Object.keys(row)]), new Set()));
} else if (Array.isArray(header)) {
if (!header.every(isString)) throw new Error("If header is array all items must be strings");
} else if (isObject(header)) {
header = Object.entries(header)
.filter(([k, v]) => v)
.map(([k]) => k);
} else {
throw new Error("Header must be either boolean, or array, or object");
}

header = header.map((h) => {
h = h.replace(/"/g, escapeChar + '"');
return h.includes(",") ? `"${h}"` : h;
});
}

function valueToString(v) {
if (v == null || v === "" || !((isNumber(v) && !isNaN(v)) || isString(v) || isDate(v) || isBoolean(v)))
return ""; // ignore bad data

v = isDate(v) ? v.toISOString() : String(v); // convert any kind of value to string
v = v.replace(/"/g, escapeChar + '"'); // Escape quote character
if (v.includes(",")) v = '"' + v + '"'; // Add quotes if value has commas
return v;
}

const textHeader = header ? header.join() + lineTerminator : "";
return (
header +
textHeader +
rows
.map((row) =>
row
.map((v) => {
if (
v == null ||
v === "" ||
!((isNumber(v) && !isNaN(v)) || isString(v) || isDate(v) || isBoolean(v))
)
return ""; // ignore bad data
v = isDate(v) ? v.toISOString() : String(v); // convert any kind of value to string
v = v.replace(/"/g, escapeChar + '"'); // Escape quote character
if (v.includes(",")) v = '"' + v + '"'; // Add quotes if value has commas
return v;
})
.join()
)
.map((row, i) => {
if (Array.isArray(row)) {
if (header && row.length !== header.length)
throw new Error(`Each row array must have exactly ${header.length} items`);
return row.map(valueToString).join();
}
if (isObject(row)) {
return header.map((h) => valueToString(row[h])).join();
}
throw new Error(`Row ${i} must be either array or object`);
})
.join(lineTerminator)
);
}
44 changes: 21 additions & 23 deletions test/lil-csv.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe("parse", () => {
it("should parse", () => {
const text = `Column,Second Column,else\rhere, we go ,false\r\n"with,comma","with \\" escaped quotes",123\n"",empty,
`;
const rows = parse(text);
const rows = parse(text, { header: false });
assert.deepStrictEqual(rows, [
["Column", "Second Column", "else"],
["here", " we go ", "false"],
Expand All @@ -17,7 +17,7 @@ describe("parse", () => {

it("should parse funky data", () => {
const text = `"Column","Second" Column,Third "Column",Middle "quotes" column`;
const rows = parse(text);
const rows = parse(text, { header: false });
assert.deepStrictEqual(rows, [["Column", "Second Column", `Third Column`, `Middle quotes column`]]);
});

Expand Down Expand Up @@ -61,14 +61,13 @@ describe("parse", () => {
describe("generate", () => {
it("should generate and parse back", () => {
const rows = parse(
generate({
rows: [
[`Column`, `Second Column`, `else`],
["here", " we go ", "false"],
["with,comma", 'with " escaped quotes', "123"],
["", "empty", ""],
],
})
generate([
[`Column`, `Second Column`, `else`],
["here", " we go ", "false"],
["with,comma", 'with " escaped quotes', "123"],
["", "empty", ""],
]),
{ header: false }
);
assert.deepStrictEqual(rows, [
[`Column`, `Second Column`, `else`],
Expand All @@ -79,14 +78,14 @@ describe("generate", () => {
});

it("should generate with header", () => {
const rows = generate({
header: [`Column`, `Second Column`, `else`],
rows: [
const rows = generate(
[
["here", " we go ", "false"],
["with,comma", 'with " escaped quotes', "123"],
["", "empty", ""],
],
});
{ header: [`Column`, `Second Column`, `else`] }
);
assert.deepStrictEqual(
rows,
`Column,Second Column,else
Expand All @@ -97,9 +96,8 @@ here, we go ,false
});

it("should auto format some primitives", () => {
const rows = generate({
const rows = generate([[new Date("2020-12-12"), 123.123, false]], {
header: [`Column`, `Second Column`, `else`],
rows: [[new Date("2020-12-12"), 123.123, false]],
});
assert.deepStrictEqual(
rows,
Expand All @@ -109,22 +107,22 @@ here, we go ,false
});

it("should ignore bad data", () => {
const rows = generate({
rows: [[null, undefined, {}, [], () => {}, NaN, "", new Map(), new Set()]],
});
const rows = generate([[null, undefined, {}, [], () => {}, NaN, "", new Map(), new Set()]]);
assert.deepStrictEqual(rows, `,,,,,,,,`);
});
});

describe("generate + parse", () => {
it("should work on fully customised options", () => {
const text = generate({
header: [`A string`, `num`, `bool`, `date`, `date of birth`, `bad data`, `skip this`, `skip this too`],
rows: [
const text = generate(
[
["my str", -123.123, false, new Date("2020-12-12"), "1999-09-09", {}, "whatever", ""],
[-1, "not number", "False", new Date("invalid date"), "bad DOB", [], "whatever", ""],
],
});
{
header: [`A string`, `num`, `bool`, `date`, `date of birth`, `bad data`, `skip this`, `skip this too`],
}
);
const data = parse(text, {
header: {
"A string": { jsonName: "stringX" },
Expand Down

0 comments on commit bb5833c

Please sign in to comment.