From: Ardis Lu Date: Thu, 28 May 2026 07:00:41 +0000 (-0700) Subject: Add `aliases` to `CSVNormalizerHeader` X-Git-Tag: v0.1.11~7 X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=3dd80ee1390a1ea81e680bcffdfc1424ee190fb3;p=csvio Add `aliases` to `CSVNormalizerHeader` This feature is useful when input CSVs may have completely different column names for the same column, beyond standard normalization issues (e.g., i18n). --- diff --git a/src/normalization.js b/src/normalization.js index e22c411..a2e249d 100644 --- a/src/normalization.js +++ b/src/normalization.js @@ -76,6 +76,8 @@ export function expandScientificNotation(str, truncate = false) { * @property {string} [displayName=name] Optional `string` value to indicate the desired header name in the output CSV. * @property {string} [defaultValue=''] Optional `string` value to use to fill empty CSV fields. If no value is provided, the * field value will be `''` (empty string) in the output CSV. + * @property {Array} [aliases=[]] Optional array of `string` values which are treated as alternate `name` values. A + * column name that matches an alias is treated the same as a column with the `name` name. */ /** @@ -136,11 +138,12 @@ export class CSVNormalizer extends TransformStream { type: 'string', displayName: name, defaultValue: '', - index: null + index: null, + aliases: [] }); } else { - const { name, type, displayName = name, defaultValue = '' } = header; + const { name, type, displayName = name, defaultValue = '', aliases = [] } = header; let normalizedType = type?.toLowerCase() ?? 'string'; if (!['string', 'number', 'bigint', 'date'].includes(normalizedType)) { console.warn(`Type "${normalizedType}" is not supported, defaulting to string.`); @@ -151,7 +154,8 @@ export class CSVNormalizer extends TransformStream { type: normalizedType, displayName, defaultValue, - index: null + index: null, + aliases: aliases.map(n => this.#useLiteralNames ? n : toCamelCase(n)) }); } } @@ -167,8 +171,8 @@ export class CSVNormalizer extends TransformStream { let i = 0; for (const header of normalizedRow) { const col = this.#useLiteralNames - ? this.#columns.find(c => c.name === header) - : this.#columns.find(c => c.name.toLocaleLowerCase() === header.toLocaleLowerCase()); + ? this.#columns.find(c => [c.name, ...c.aliases].includes(header)) + : this.#columns.find(c => [c.name, ...c.aliases].map(n => n.toLocaleLowerCase()).includes(header.toLocaleLowerCase())); if (col !== undefined) { // Drop columns provided in the CSV but not the headers input col.index = i; } diff --git a/test/normalization.test.js b/test/normalization.test.js index a50332e..c710359 100644 --- a/test/normalization.test.js +++ b/test/normalization.test.js @@ -446,6 +446,34 @@ suite('CSVNormalizer', { concurrency: true }, () => { ['a4'] ])); }); + test('removes columns that have the same name via aliases (last column prevails)', { concurrency: true }, async () => { + await createCSVMockStream([ + ['columnA', 'column A', 'column-B', 'columnb', 'columnC'], + ['a1', 'a2', 'a3', 'a4', 'a5'] + ]) + .pipeThrough(new CSVNormalizer([ + { name: 'columnA', aliases: ['columnB', 'columnC'] } + ])) + .pipeThrough(new CSVDenormalizer()) + .pipeTo(csvStreamEqualWritable([ + ['columnA'], + ['a5'] + ])); + }); + test('can match by column name alias', { concurrency: true }, async () => { + await createCSVMockStream([ + ['aaa', 'bbb', 'ccc', 'ddd', 'eee'], + ['a1', 'a2', 'a3', 'a4', 'a5'] + ]) + .pipeThrough(new CSVNormalizer([ + { name: 'zzz', aliases: ['yyy', 'ddd'] } + ])) + .pipeThrough(new CSVDenormalizer()) + .pipeTo(csvStreamEqualWritable([ + ['zzz'], + ['a4'] + ])); + }); test('can match by literal column names', { concurrency: true }, async () => { await createCSVMockStream([ ['columnA', 'column A', 'column-A', 'columna'], @@ -463,6 +491,23 @@ suite('CSVNormalizer', { concurrency: true }, () => { ['a1', 'a2', 'a3', 'a4'] ])); }); + test('can match by literal column name aliases', { concurrency: true }, async () => { + await createCSVMockStream([ + ['columnA', 'column A', 'column-A', 'columna'], + ['a1', 'a2', 'a3', 'a4'] + ]) + .pipeThrough(new CSVNormalizer([ + { name: 'aaa', aliases: ['columnA'] }, + { name: 'bbb', aliases: ['column A'] }, + { name: 'ccc', aliases: ['column-A'] }, + { name: 'ddd', aliases: ['columna'] } + ], { useLiteralNames: true })) + .pipeThrough(new CSVDenormalizer()) + .pipeTo(csvStreamEqualWritable([ + ['aaa', 'bbb', 'ccc', 'ddd'], + ['a1', 'a2', 'a3', 'a4'] + ])); + }); test('is NOT case sensitive on column names', { concurrency: true }, async () => { await createCSVMockStream([ ['datetime', 'dateTime'],