From c0cb0d4b5c14c4df35e49bb0f7e410e738616b99 Mon Sep 17 00:00:00 2001 From: Dogukan Karasakal Date: Thu, 17 Oct 2024 00:09:46 +0200 Subject: [PATCH 1/3] csv: stringify infer column names from object arrays --- csv/stringify.ts | 33 ++++++++++++++++++++----- csv/stringify_stream.ts | 2 +- csv/stringify_stream_test.ts | 12 +++++---- csv/stringify_test.ts | 48 ++++++++++++++++-------------------- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/csv/stringify.ts b/csv/stringify.ts index 6770535b42d5..11ee2162b3dd 100644 --- a/csv/stringify.ts +++ b/csv/stringify.ts @@ -108,9 +108,9 @@ export type StringifyOptions = { * column of output. This is also where you can provide an explicit header * name for the column. * - * @default {[]} + * @default {undefined} */ - columns?: readonly Column[]; + columns?: readonly Column[] | undefined; /** * Whether to add a * {@link https://en.wikipedia.org/wiki/Byte_order_mark | byte-order mark} to the @@ -167,7 +167,7 @@ function normalizeColumn(column: Column): NormalizedColumn { prop = [column]; } - return { header, prop }; + return {header, prop}; } /** @@ -429,8 +429,8 @@ export function stringify( data: readonly DataItem[], options?: StringifyOptions, ): string { - const { headers = true, separator: sep = ",", columns = [], bom = false } = - options ?? {}; + const {headers = true, separator: sep = ",", columns, bom = false} = + options ?? {}; if (sep.includes(QUOTE) || sep.includes(CRLF)) { const message = [ @@ -441,7 +441,12 @@ export function stringify( throw new TypeError(message); } - const normalizedColumns = columns.map(normalizeColumn); + if (columns && !Array.isArray(columns)) { + throw new TypeError("Invalid type: columns can only be an array."); + } + + const definedColumns = columns ?? inferColumns(data); + const normalizedColumns = definedColumns.map(normalizeColumn); let output = ""; if (bom) { @@ -465,3 +470,19 @@ export function stringify( return output; } + +/** + * Infers the columns from the first object element of the given array. + */ +function inferColumns(data: readonly DataItem[]) { + const firstElement = data.at(0); + if ( + firstElement && + typeof firstElement === "object" && + !Array.isArray(firstElement) + ) { + return Object.keys(firstElement); + } + + return []; +} diff --git a/csv/stringify_stream.ts b/csv/stringify_stream.ts index 3aa30e2ef4ff..ea57f91ea6ab 100644 --- a/csv/stringify_stream.ts +++ b/csv/stringify_stream.ts @@ -99,7 +99,7 @@ export class CsvStringifyStream * @param options Options for the stream. */ constructor(options?: TOptions) { - const { separator, columns = [] } = options ?? {}; + const { separator, columns } = options ?? {}; super( { diff --git a/csv/stringify_stream_test.ts b/csv/stringify_stream_test.ts index fab58f0e231c..821ce40719fa 100644 --- a/csv/stringify_stream_test.ts +++ b/csv/stringify_stream_test.ts @@ -64,7 +64,7 @@ Deno.test({ await assertRejects( async () => await Array.fromAsync(readable), TypeError, - "No property accessor function was provided for object", + "Invalid type: columns can only be an array.", ); }); @@ -90,10 +90,12 @@ Deno.test({ { id: 3, name: "baz" }, // @ts-expect-error `columns` option is required ]).pipeThrough(new CsvStringifyStream()); - await assertRejects( - async () => await Array.fromAsync(readable), - TypeError, - ); + const output = await Array.fromAsync(readable); + assertEquals(output, [ + "1,foo\r\n", + "2,bar\r\n", + "3,baz\r\n", + ]); }); }, }); diff --git a/csv/stringify_test.ts b/csv/stringify_test.ts index 08d525b1ba09..a0eefab76f9e 100644 --- a/csv/stringify_test.ts +++ b/csv/stringify_test.ts @@ -72,33 +72,6 @@ Deno.test({ }, }, ); - - 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", @@ -591,5 +564,26 @@ Deno.test({ }); }, }); + await t.step( + { + name: "Object array with no columns, should infer columns", + fn() { + const data = [{ a: 1 }, { a: 2 }, {b: 3}]; + const output = `a${CRLF}1${CRLF}2${CRLF}${CRLF}`; + assertEquals(stringify(data), output); + }, + }, + ); + await t.step( + { + name: "Object array with columns, shouldn't infer columns", + fn() { + const data = [{ a: 1 }, { a: 2 }, { b: 3 }]; + const columns = ["a"]; + const output = `a${CRLF}1${CRLF}2${CRLF}${CRLF}`; + assertEquals(stringify(data, { columns }), output); + }, + }, + ); }, }); From 26385d257406991ef5d238b748eafd187ec58bca Mon Sep 17 00:00:00 2001 From: Dogukan Karasakal Date: Thu, 17 Oct 2024 00:15:07 +0200 Subject: [PATCH 2/3] csv: run fmt --- csv/stringify.ts | 6 +++--- csv/stringify_test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/csv/stringify.ts b/csv/stringify.ts index 11ee2162b3dd..fb7792f64532 100644 --- a/csv/stringify.ts +++ b/csv/stringify.ts @@ -167,7 +167,7 @@ function normalizeColumn(column: Column): NormalizedColumn { prop = [column]; } - return {header, prop}; + return { header, prop }; } /** @@ -429,8 +429,8 @@ export function stringify( data: readonly DataItem[], options?: StringifyOptions, ): string { - const {headers = true, separator: sep = ",", columns, bom = false} = - options ?? {}; + const { headers = true, separator: sep = ",", columns, bom = false } = + options ?? {}; if (sep.includes(QUOTE) || sep.includes(CRLF)) { const message = [ diff --git a/csv/stringify_test.ts b/csv/stringify_test.ts index a0eefab76f9e..5317c09b3256 100644 --- a/csv/stringify_test.ts +++ b/csv/stringify_test.ts @@ -568,7 +568,7 @@ Deno.test({ { name: "Object array with no columns, should infer columns", fn() { - const data = [{ a: 1 }, { a: 2 }, {b: 3}]; + const data = [{ a: 1 }, { a: 2 }, { b: 3 }]; const output = `a${CRLF}1${CRLF}2${CRLF}${CRLF}`; assertEquals(stringify(data), output); }, From a35ac583025e521eeacb1cbc69a714150b469d96 Mon Sep 17 00:00:00 2001 From: Dogukan Karasakal Date: Thu, 17 Oct 2024 21:11:35 +0200 Subject: [PATCH 3/3] test: update old jsdoc example --- csv/stringify.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/csv/stringify.ts b/csv/stringify.ts index fb7792f64532..23bbfded070c 100644 --- a/csv/stringify.ts +++ b/csv/stringify.ts @@ -250,18 +250,14 @@ function getValuesFromItem( * @example Give an array of objects without specifying columns * ```ts * import { stringify } from "@std/csv/stringify"; - * import { assertThrows } from "@std/assert/throws"; + * import { assertEquals } from "@std/assert/equals"; * * const data = [ * { name: "Rick", age: 70 }, * { name: "Morty", age: 14 }, * ]; * - * assertThrows( - * () => stringify(data), - * TypeError, - * "No property accessor function was provided for object", - * ); + * assertEquals(stringify(data), `name,age\r\nRick,70\r\nMorty,14\r\n`); * ``` * * @example Give an array of objects and specify columns with `headers: false`