]> git.ardis.lu - csvio/commitdiff
Add `aliases` to `CSVNormalizerHeader`
authorArdis Lu <ardisglu@gmail.com>
Thu, 28 May 2026 07:00:41 +0000 (00:00 -0700)
committerArdis Lu <ardisglu@gmail.com>
Thu, 28 May 2026 07:00:41 +0000 (00:00 -0700)
This feature is useful when input CSVs may have completely different column names for the same column, beyond standard normalization issues (e.g., i18n).

src/normalization.js
test/normalization.test.js

index e22c41168b97d2e01b8921404f7fcf38cc630f5d..a2e249d4fe8ac82843eadee6763fbbc57e0730a5 100644 (file)
@@ -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<string>} [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;
         }
index a50332efbef43fb0894feee474bde445bf7cc7ee..c71035933cdfa386bc69dfc8f533b55cb9196ed2 100644 (file)
@@ -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'],
This page took 0.048808 seconds and 4 git commands to generate.