]> git.ardis.lu - csvio/commitdiff
Add `CSVNormalizer.toFieldMap()`
authorArdis Lu <ardisglu@gmail.com>
Sun, 5 Oct 2025 06:08:02 +0000 (23:08 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sun, 5 Oct 2025 06:08:02 +0000 (23:08 -0700)
examples/README.md
examples/data/ex4_4-in.csv [new file with mode: 0644]
examples/data/ex4_4-out.csv [new file with mode: 0644]
examples/ex4_4.js [new file with mode: 0644]
src/normalization.js
test/normalization.test.js

index 047d2ee5925ef6fa7b9289826fef67f2c49538e0..5a7a9027be5d1dd319165b637e2bec4b46ac3765 100644 (file)
@@ -59,3 +59,5 @@ These examples demonstrate the extended API.
 [Example 4.2: Using `CSVNormalizer` and `CSVDenormalizer` with `CSVTransformer`](./ex4_2.js) ([input](./data/ex4_2-in.csv), [output](./data/ex4_2-out.csv))
 
 [Example 4.3: Using `CSVNormalizer.toObject()`](./ex4_3.js) ([input](./data/ex4_3-in.csv), [output](./data/ex4_3-out.csv))
+
+[Example 4.4: Using `CSVNormalizer.toFieldMap()`](./ex4_4.js) ([input](./data/ex4_4-in.csv), [output](./data/ex4_4-out.csv))
diff --git a/examples/data/ex4_4-in.csv b/examples/data/ex4_4-in.csv
new file mode 100644 (file)
index 0000000..3805b7c
--- /dev/null
@@ -0,0 +1,4 @@
+a,b,c\r
+111,222,333\r
+111,222,333\r
+111,222,333\r
diff --git a/examples/data/ex4_4-out.csv b/examples/data/ex4_4-out.csv
new file mode 100644 (file)
index 0000000..2e903bf
--- /dev/null
@@ -0,0 +1,4 @@
+a,b,c,d\r
+intercepted: 111,intercepted: 222,intercepted: 333,new column: 444\r
+intercepted: 111,intercepted: 222,intercepted: 333,new column: 444\r
+intercepted: 111,intercepted: 222,intercepted: 333,new column: 444\r
diff --git a/examples/ex4_4.js b/examples/ex4_4.js
new file mode 100644 (file)
index 0000000..28b87bd
--- /dev/null
@@ -0,0 +1,39 @@
+// ## Example 4.4: Using `CSVNormalizer.toFieldMap()`
+// 
+// Use the static method `CSVNormalizer.toFieldMap()` to conveniently access field values (using a subset of the familiar
+// `Map` interface) while keeping the underlying `row` object in sync with updates.
+// 
+// The field map is convenient for straightforward reading and writing to field values. For more advanced usage, directly
+// interface with the `row` object.
+
+import { CSVReader, CSVWriter, CSVNormalizer, CSVDenormalizer, CSVTransformer } from '../src/index.js';
+/** @import { CSVNormalizerHeader, CSVNormalizerField } from '../src/index.js'; */
+
+/** @type {Array<CSVNormalizerHeader>} */
+const headers = [
+  { name: 'a' },
+  { name: 'b' },
+  { name: 'c' }
+];
+
+/**
+ * @param {Array<CSVNormalizerField>} row 
+ * @returns {Array<CSVNormalizerField>}
+ */
+function transform(row) {
+  const map = CSVNormalizer.toFieldMap(row);
+  const a = map.get('a'); // Use the familiar `Map` interface
+  const b = map.get('b');
+  const c = map.get('c');
+  map.set('a', `intercepted: ${a}`); // The underlying object in `row` is also updated
+  map.set('b', `intercepted: ${b}`);
+  map.set('c', `intercepted: ${c}`);
+  map.set('d', 'new column: 444'); // New names are automatically pushed as new field
+  return row; // Return the original input row, which stays in sync with changes made to the field map
+}
+
+await new CSVReader(new URL('./data/ex4_4-in.csv', import.meta.url))
+  .pipeThrough(new CSVNormalizer(headers))
+  .pipeThrough(new CSVTransformer(transform))
+  .pipeThrough(new CSVDenormalizer())
+  .pipeTo(new CSVWriter(new URL('./data/ex4_4-out.csv', import.meta.url)));
index 9312c04bd6aa1ff3adcea0e1d23eedfc8cb5e05f..70ce0871d083ffe5dd7607859c34b7934970f163 100644 (file)
@@ -325,6 +325,54 @@ export class CSVNormalizer extends TransformStream {
     }
     return object;
   }
+
+  /**
+   * Create a subset of the `Map` interface to get and set the `value` of a field using its `name`.
+   * 
+   * The return value of this method is a `Proxy` of a `Map` that intercepts the `Map` methods so that it gets and sets
+   * values by *reference* to the underlying `CSVNormalizerField`. This interception makes the `Map` stay in sync with the
+   * `row` array.
+   * 
+   * Only the methods `.get()`, `.set()`, `.has()` and the property `.size` are supported. All other methods are unsupported.
+   * For more advanced usage, use the underlying `row` object directly.
+   * 
+   * The `displayName` and `emptyField` values of a field cannot be accessed from the `Map`.
+   * 
+   * Calling `.set()` with a `name` that is not in the row will push a new field to the row.
+   * @param {Array<CSVNormalizerField>} row A normalized CSV row.
+   * @returns {Pick<Map<string,any>,'get'|'set'|'has'|'size'>} A `Proxy` over a `Map` object with keys set to the `name` of
+   * each field in the row.
+   */
+  static toFieldMap(row) {
+    const map = new Map();
+    for (const field of row) {
+      map.set(field.name, field);
+    }
+    return new Proxy(map, {
+      get(target, prop, receiver) {
+        if (prop === 'get') {
+          return name => target.get(name)?.value;
+        }
+        if (prop === 'set') {
+          return (name, value) => {
+            if (target.has(name)) {
+              target.get(name).value = value;
+            }
+            else {
+              const field = { name, value };
+              row.push(field);
+              target.set(name, field);
+            }
+            return receiver;
+          }
+        }
+        if (prop === 'has' || prop === 'size') {
+          const targetProp = Reflect.get(target, prop);
+          return typeof targetProp === 'function' ? targetProp.bind(target) : targetProp;
+        }
+      }
+    });
+  }
 }
 
 /**
index 4c25b9c6ed01a6e330c79b51db52a92346601b80..cc2700d252630cdca419f1a3abeb04221c081158 100644 (file)
@@ -209,6 +209,48 @@ suite('CSVNormalizer.toObject', { concurrency: true }, () => {
   });
 });
 
+suite('CSVNormalizer.toFieldMap', { concurrency: true }, () => {
+  test('gets value by reference', { concurrency: true }, async () => {
+    const row = [{ name: 'a', value: 1 }];
+    const map = CSVNormalizer.toFieldMap(row);
+    deepStrictEqual(map.get('a'), 1);
+    row[0].value = 999;
+    deepStrictEqual(map.get('a'), 999);
+  });
+  test('sets value by reference', { concurrency: true }, async () => {
+    const row = [{ name: 'a', value: 1 }];
+    const map = CSVNormalizer.toFieldMap(row);
+    deepStrictEqual(row[0].value, 1);
+    map.set('a', 999);
+    deepStrictEqual(row[0].value, 999);
+  });
+  test('can chain set calls', { concurrency: true }, async () => {
+    const row = [{ name: 'a', value: 1 }];
+    const map = CSVNormalizer.toFieldMap(row);
+    deepStrictEqual(row[0].value, 1);
+    map.set('a', 222).set('a', 333).set('a', 444);
+    deepStrictEqual(row[0].value, 444);
+  });
+  test('pushes new fields', { concurrency: true }, async () => {
+    const row = [{ name: 'a', value: 1 }];
+    const map = CSVNormalizer.toFieldMap(row);
+    deepStrictEqual(row.length, 1);
+    map.set('b', 2).set('c', 3).set('d', 4);
+    deepStrictEqual(row.length, 4);
+  });
+  test('passes through .has() method', { concurrency: true }, async () => {
+    const row = [{ name: 'a', value: 1 }];
+    const map = CSVNormalizer.toFieldMap(row);
+    ok(map.has('a'));
+    ok(!map.has('b'));
+  });
+  test('passes through .size property', { concurrency: true }, async () => {
+    const row = [{ name: 'a', value: 1 }];
+    const map = CSVNormalizer.toFieldMap(row);
+    deepStrictEqual(map.size, 1);
+  });
+});
+
 suite('CSVNormalizer', { concurrency: true }, () => {
   test('can remove extra columns in CSV', { concurrency: true }, async () => {
     await createCSVMockStream([
This page took 0.044532 seconds and 4 git commands to generate.