[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))
--- /dev/null
+a,b,c\r
+111,222,333\r
+111,222,333\r
+111,222,333\r
--- /dev/null
+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
--- /dev/null
+// ## 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)));
}
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;
+ }
+ }
+ });
+ }
}
/**
});
});
+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([