diff --git a/package.json b/package.json index d1fd829..ac1f172 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@angular/platform-browser-dynamic": "^6.1.4", "@angular/router": "^6.1.4", "core-js": "^2.5.4", - "micro-dash": "^4.1.0", + "micro-dash": "^4.2.0", "rxjs": "^6.0.0", "zone.js": "~0.8.26" }, diff --git a/projects/s-js-utils/src/lib/to-csv.spec.ts b/projects/s-js-utils/src/lib/to-csv.spec.ts new file mode 100644 index 0000000..cadca8a --- /dev/null +++ b/projects/s-js-utils/src/lib/to-csv.spec.ts @@ -0,0 +1,72 @@ +import { toCsv } from "./to-csv"; + +describe("toCsv()", () => { + it("works", () => { + expect(toCsv([["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]])).toBe( + "a,b,c\nd,e,f\ng,h,i", + ); + }); + + it("handles empty cells properly", () => { + expect( + toCsv([ + ["middle", "", "empty"], + ["", "start", "empty"], + ["end", "empty", ""], + ]), + ).toBe("middle,,empty\n,start,empty\nend,empty,"); + }); + + it("handles empty rows properly", () => { + expect(toCsv([["row"], [], ["another row"], []])).toBe( + "row\n\nanother row\n", + ); + }); + + it("properly escapes double quotes", () => { + expect(toCsv([["escape", "csv", `"quotes"`]])).toBe( + `escape,csv,"""quotes"""`, + ); + }); + + it("properly escapes commas", () => { + expect( + toCsv([["eats shoots and leaves", "eats, shoots, and leaves"]]), + ).toBe(`eats shoots and leaves,"eats, shoots, and leaves"`); + }); + + it("properly escapes new lines", () => { + expect(toCsv([["one", "1"], ["two", "1\n2"], ["three", "1\n2\n3"]])).toBe( + `one,1\ntwo,"1\n2"\nthree,"1\n2\n3"`, + ); + expect(toCsv([["one", "1"], ["two", "1\r2"], ["three", "1\r2\r3"]])).toBe( + `one,1\ntwo,"1\r2"\nthree,"1\r2\r3"`, + ); + expect( + toCsv([["one", "1"], ["two", "1\r\n2"], ["three", "1\r\n2\r\n3"]]), + ).toBe(`one,1\ntwo,"1\r\n2"\nthree,"1\r\n2\r\n3"`); + }); + + it("escapes properly if there are multiple special things", () => { + expect( + toCsv([ + ["one", "with, comma"], + ["another", "with\nnewline"], + ["last", `with "quotes"`], + ]), + ).toBe( + `one,"with, comma"\nanother,"with\nnewline"\nlast,"with ""quotes"""`, + ); + }); + + it("handles things that aren't strings", () => { + expect( + toCsv([ + [undefined, null], + [true, false], + [1, 2, 3], + [{}, { hi: "there" }], + ]), + ).toBe(",\ntrue,false\n1,2,3\n[object Object],[object Object]"); + }); +}); diff --git a/projects/s-js-utils/src/lib/to-csv.ts b/projects/s-js-utils/src/lib/to-csv.ts new file mode 100644 index 0000000..efedbe1 --- /dev/null +++ b/projects/s-js-utils/src/lib/to-csv.ts @@ -0,0 +1,34 @@ +import { toString } from "micro-dash"; + +/** + * Converts a 2D array to a csv string. Values are converted using micro-dash's toString(). + * + * ```ts + * toCsv([["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]]); // "a,b,c\nd,e,f\ng,h,i" + * toCsv([ + * ["a", "", "string"] + * [undefined, null], + * [true, false], + * [1, 2, 3], + [{}, { hi: "there" }] + * ]) // "a,,string\n,\ntrue,false\n1,2,3\n[object Object],[object Object]" + * ``` + */ +export function toCsv(content: any[][]) { + return content + .map((row) => row.map((cell) => toCellString(cell)).join(",")) + .join("\n"); +} + +const specialCsvCharactersRegexp = /["|,|\n|\r]/; +const allDoubleQuotes = /"/g; + +function toCellString(value: any) { + const string = toString(value); + + if (specialCsvCharactersRegexp.test(string)) { + return `"${string.replace(allDoubleQuotes, `""`)}"`; + } else { + return string; + } +} diff --git a/projects/s-js-utils/src/public_api.ts b/projects/s-js-utils/src/public_api.ts index 03f7dd6..70553cc 100644 --- a/projects/s-js-utils/src/public_api.ts +++ b/projects/s-js-utils/src/public_api.ts @@ -9,3 +9,4 @@ export * from "./lib/round-to-multiple-of"; export * from "./lib/sleep"; export * from "./lib/test-factory"; export * from "./lib/time-utils"; +export * from "./lib/to-csv"; diff --git a/yarn.lock b/yarn.lock index 411990b..566188e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4377,9 +4377,9 @@ methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micro-dash@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/micro-dash/-/micro-dash-4.1.0.tgz#6b69d9440d60ba83d6bc3295fa34ce4240936030" +micro-dash@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/micro-dash/-/micro-dash-4.2.0.tgz#84da0a853dc6a869cb365b7349518d1b4ed8548b" dependencies: tslib "^1.9.0"