Skip to content

Commit

Permalink
feat: Change default delimiter to semicolon (#64)
Browse files Browse the repository at this point in the history
closes #61
  • Loading branch information
zachelrath authored Jul 31, 2024
1 parent 7c6a93f commit d1221b8
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 40 deletions.
19 changes: 19 additions & 0 deletions migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,22 @@ const props = {
}
csvDownload(props);
```

# Migration to 3.x

## Breaking Changes

* The default delimiter was changed from semicolon (;) to comma (,) to comply with RFC-4180.

## Steps

* update to the new version (3.x)
* if your code is expecting a semicolon delimiter, add `delimiter: ";"` to the options object provided to `csvDownload`:

```ts
csvDownload({
data: someData,
filename: "with-semicolons.csv",
delimiter: ";",
});
```
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ Download Data
| # | Property | Type | Requirement | Default | Description |
| -- |-----------|--------------| ----------- |---------------------------|-------------------------------------------------------------------------------|
| 1 | data | `[]` | `required` | | array of objects |
| 2 | filename | `string` | `optional` | "export.csv" | The filename. The `.csv` extention will be edded if not included in file name |
| 3 | delimiter | `string` | `optional` | ";" | fields separator |
| 2 | filename | `string` | `optional` | "export.csv" | The filename. The `.csv` extention will be added if not included in file name |
| 3 | delimiter | `string` | `optional` | "," | fields separator |
| 4 | headers | `string[]` | `optional` | provided data object keys | List of columns that will be used in the final CSV file. |

## Migration from version 1.x to 2.x
Expand Down
30 changes: 23 additions & 7 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@ export const csvGenerateRow = (
row: any,
headerKeys: string[],
delimiter: string,
) =>
headerKeys
.map((fieldName) =>
typeof row[fieldName] === "number"
? row[fieldName]
: `"${String(row[fieldName]).replace(/"/g, '""')}"`,
)
) => {
const needsQuoteWrapping = new RegExp(`["${delimiter}\r\n]`);
return headerKeys
.map((fieldName) => {
let value = row[fieldName];
if (typeof value === "number" || typeof value === "boolean") return `${value}`;
if (!value) return "";
if (typeof value !== "string") {
value = String(value);
}
/* RFC-4180
6. Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes.
7. If double-quotes are used to enclose fields, then a double-quote inside a field must be escaped by preceding it with
another double quote. For example: "aaa","b""bb","ccc"
In order to support something other than commas as delimiters, we will substitute delimiter for comma in rule 6,
although use of a double quotes or CRLF as delimiter is unsupported. */
if (needsQuoteWrapping.test(value)) {
return `"${value.replace(/"/g, '""')}"`;
} else {
return value;
}
})
.join(delimiter);
}

export const csvGenerate = (
data: any[],
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { csvGenerate } from "./generate";
interface CsvDownloadProps {
data: any[];
filename?: string;
/** Cell delimiter to use. Defaults to comma for RFC-4180 compliance. */
delimiter?: string;
headers?: string[];
}
Expand All @@ -12,7 +13,7 @@ const CSV_FILE_TYPE = "text/csv;charset=utf-8;";
const csvDownload = ({
data,
filename = "export.csv",
delimiter = ";",
delimiter = ",",
headers,
}: CsvDownloadProps): void => {
const formattedFilename = getFilename(filename);
Expand Down
83 changes: 66 additions & 17 deletions test/generate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,72 @@
import { csvGenerateRow } from "../src/generate";

test("csv generator correctly handles custom delimiters", () => {
const mockData = {
id: 1,
text: "Lee Perry",
};
describe("csvGenerateRow", () => {

expect(csvGenerateRow(mockData, ["id", "text"], ",")).toEqual(
`1,"Lee Perry"`
);
});
test("correctly handles empty, number, and boolean values", () => {
const mockData = {
id: 0,
one: true,
two: false,
empty: "",
};

expect(csvGenerateRow(mockData, ["id", "one", "two", "empty"], ",")).toEqual(
`0,true,false,`
);
});

test("correctly handles custom delimiters", () => {
const mockData = {
id: 1,
text: "Lee Perry",
};

expect(csvGenerateRow(mockData, ["id", "text"], ";")).toEqual(
`1;Lee Perry`
);
});

test("correctly handles data with double-quotes", () => {
const mockData = {
id: 1,
text: 'Lee "Scratch" Perry',
};
expect(csvGenerateRow(mockData, ["id", "text"], ";")).toEqual(
`1;"Lee ""Scratch"" Perry"`
);
});

test("row generator correctly handles data with double-quotes", () => {
const mockData = {
id: 1,
text: 'Lee "Scratch" Perry',
};
test("correctly handles data with carriage return / newline and whitespace", () => {
const mockData = {
id: 1,
text: `Lee
Perry `,
};
expect(csvGenerateRow(mockData, ["id", "text"], ",")).toEqual(
`1,"Lee
Perry "`
);
});

test("correctly handles data containing the delimiter (semicolon)", () => {
const mockData = {
id: 1,
text: 'Bond; James Bond',
};
expect(csvGenerateRow(mockData, ["id", "text"], ";")).toEqual(
`1;"Bond; James Bond"`
);
});

expect(csvGenerateRow(mockData, ["id", "text"], ";")).toEqual(
`1;"Lee ""Scratch"" Perry"`
);
test("correctly handles data containing the delimiter (comma)", () => {
const mockData = {
id: 1,
name: 'Baggins, Frodo',
location: 'The Shire; Eriador',
};
expect(csvGenerateRow(mockData, ["id", "name", "location"], ",")).toEqual(
`1,"Baggins, Frodo",The Shire; Eriador`
);
});

});
49 changes: 36 additions & 13 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import csvDownload from "../src/index";
import mockData from "./__mocks__/mockData";

// current version of JSDom doesn't support Blob.text(), so this is a FileReader-based workaround.
const getBlobAsText = async (blob: Blob, encoding = "text/csv;charset=utf-8;"): Promise<string> => {
const fileReader = new FileReader();
return new Promise((resolve) => {
fileReader.onload = () => {
resolve(fileReader.result as string);
};
fileReader.readAsText(blob, encoding);
});
};

describe("csvDownload", () => {
const _URL = global.URL;
let capturedBlob: Blob | null;
let link;

beforeAll(() => {
global.URL.createObjectURL = (blob: Blob) => {
capturedBlob = blob;
return "test/url";
};
})

global.URL.createObjectURL = () => "test/url";
beforeEach(() => {
document.onclick = (e) => {
link = e.target as HTMLAnchorElement;
};
capturedBlob = null;
});

afterEach(() => {
global.URL = _URL;
Expand All @@ -18,29 +43,27 @@ describe("csvDownload", () => {
csvDownload({ data: [] });
});

test("with data", async () => {
let link;

document.onclick = (e) => {
link = e.target as HTMLAnchorElement;
};
test("with data, using comma delimiter as default", async () => {
csvDownload({ data: mockData });
expect(link.href).toContain("test/url");
expect(link.download).toEqual("export.csv");
expect(capturedBlob).not.toBe(null);
const generatedCsvString = await getBlobAsText(capturedBlob as Blob);
expect(generatedCsvString.startsWith(`id,First Name,Last Name,Email,Gender,IP Address`)).toBeTruthy();
expect(generatedCsvString.includes(`1,Blanch,Elby,belby0@bing.com,Female,112.81.107.207`)).toBeTruthy();
});

test("with all properties provided", async () => {
let link;

document.onclick = (e) => {
link = e.target as HTMLAnchorElement;
};
csvDownload({
data: mockData,
headers: ["ID", "Name", "Surname", "E-mail", "Gender", "IP"],
filename: "custom-name",
delimiter: ",",
delimiter: ";",
});
expect(link.download).toEqual("custom-name.csv");
expect(capturedBlob).not.toBe(null);
const generatedCsvString = await getBlobAsText(capturedBlob as Blob);
expect(generatedCsvString.startsWith(`ID;Name;Surname;E-mail;Gender;IP`)).toBeTruthy();
expect(generatedCsvString.includes(`1;Blanch;Elby;belby0@bing.com;Female;112.81.107.207`)).toBeTruthy();
});
});

0 comments on commit d1221b8

Please sign in to comment.