From 7ad43e064ae2eff03064976982fed2853b37f424 Mon Sep 17 00:00:00 2001 From: Ardis Lu Date: Sat, 4 Oct 2025 23:08:02 -0700 Subject: [PATCH] Add `CSVNormalizer.toFieldMap()` --- examples/README.md | 2 ++ examples/data/ex4_4-in.csv | 4 ++++ examples/data/ex4_4-out.csv | 4 ++++ examples/ex4_4.js | 39 ++++++++++++++++++++++++++++++ src/normalization.js | 48 +++++++++++++++++++++++++++++++++++++ test/normalization.test.js | 42 ++++++++++++++++++++++++++++++++ 6 files changed, 139 insertions(+) create mode 100644 examples/data/ex4_4-in.csv create mode 100644 examples/data/ex4_4-out.csv create mode 100644 examples/ex4_4.js diff --git a/examples/README.md b/examples/README.md index 047d2ee..5a7a902 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 index 0000000..3805b7c --- /dev/null +++ b/examples/data/ex4_4-in.csv @@ -0,0 +1,4 @@ +a,b,c +111,222,333 +111,222,333 +111,222,333 diff --git a/examples/data/ex4_4-out.csv b/examples/data/ex4_4-out.csv new file mode 100644 index 0000000..2e903bf --- /dev/null +++ b/examples/data/ex4_4-out.csv @@ -0,0 +1,4 @@ +a,b,c,d +intercepted: 111,intercepted: 222,intercepted: 333,new column: 444 +intercepted: 111,intercepted: 222,intercepted: 333,new column: 444 +intercepted: 111,intercepted: 222,intercepted: 333,new column: 444 diff --git a/examples/ex4_4.js b/examples/ex4_4.js new file mode 100644 index 0000000..28b87bd --- /dev/null +++ b/examples/ex4_4.js @@ -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} */ +const headers = [ + { name: 'a' }, + { name: 'b' }, + { name: 'c' } +]; + +/** + * @param {Array} row + * @returns {Array} + */ +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))); diff --git a/src/normalization.js b/src/normalization.js index 9312c04..70ce087 100644 --- a/src/normalization.js +++ b/src/normalization.js @@ -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} row A normalized CSV row. + * @returns {Pick,'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; + } + } + }); + } } /** diff --git a/test/normalization.test.js b/test/normalization.test.js index 4c25b9c..cc2700d 100644 --- a/test/normalization.test.js +++ b/test/normalization.test.js @@ -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([ -- 2.47.3