diff --git a/csv/_io.ts b/csv/_io.ts index 5e80959c55b0..631bb99bcb7e 100644 --- a/csv/_io.ts +++ b/csv/_io.ts @@ -228,16 +228,26 @@ export function convertRowToObject( row: readonly string[], headers: readonly string[], zeroBasedLine: number, + /* allow less fields in a row than headers */ + allowLessRowFields = false, ) { - if (row.length !== headers.length) { + if (!allowLessRowFields && row.length !== headers.length) { throw new Error( `Syntax error on line ${ zeroBasedLine + 1 }: The record has ${row.length} fields, but the header has ${headers.length} fields`, ); } + if (allowLessRowFields && row.length > headers.length) { + throw new Error( + `Syntax error on line ${ + zeroBasedLine + 1 + }: The record has ${row.length} fields, but the header allows maximum ${headers.length} fields`, + ); + } const out: Record = {}; for (const [index, header] of headers.entries()) { + if (index === row.length) break; out[header] = row[index]; } return out; diff --git a/csv/parse.ts b/csv/parse.ts index 2ad28afd411e..27ba2c219445 100644 --- a/csv/parse.ts +++ b/csv/parse.ts @@ -524,7 +524,12 @@ export function parse( const zeroBasedFirstLineIndex = options.skipFirstRow ? 1 : 0; return r.map((row, i) => { - return convertRowToObject(row, headers, zeroBasedFirstLineIndex + i); + return convertRowToObject( + row, + headers, + zeroBasedFirstLineIndex + i, + (options?.fieldsPerRecord ?? 0) < 0, + ); }) as ParseResult; } return r as ParseResult; diff --git a/csv/parse_test.ts b/csv/parse_test.ts index 1912758b8100..8d2352c79c22 100644 --- a/csv/parse_test.ts +++ b/csv/parse_test.ts @@ -309,6 +309,29 @@ Deno.test({ ); }, }); + + await t.step({ + name: "Allow less row fields than columns", + fn() { + const input = "a,b,c\nd,e"; + const output = [{ + foo: "a", + bar: "b", + baz: "c", + }, { + foo: "d", + bar: "e", + }]; + assertEquals( + parse(input, { + skipFirstRow: false, + columns: ["foo", "bar", "baz"], + fieldsPerRecord: -1, + }), + output, + ); + }, + }); await t.step({ name: "NegativeFieldsPerRecord", fn() { @@ -320,6 +343,19 @@ Deno.test({ assertEquals(parse(input, { fieldsPerRecord: -1 }), output); }, }); + await t.step({ + name: "LessFieldsPerRecordThanFirstLine", + fn() { + const input = `a,b,c\nd,e`; + const output = [ + { a: "d", "b": "e" }, + ]; + assertEquals( + parse(input, { skipFirstRow: true, fieldsPerRecord: -1 }), + output, + ); + }, + }); await t.step({ name: "FieldCount", fn() { @@ -860,6 +896,22 @@ c"d,e`; ); }, }); + await t.step({ + name: "mismatching number of headers and fields 3", + fn() { + const input = "a,b,c\nd,e,,g"; + assertThrows( + () => + parse(input, { + skipFirstRow: true, + columns: ["foo", "bar", "baz"], + fieldsPerRecord: -1, + }), + Error, + "Syntax error on line 2: The record has 4 fields, but the header allows maximum 3 fields", + ); + }, + }); await t.step({ name: "Strips leading byte-order mark with bare cell", fn() {