Repository: mholt/PapaParse
Pull request #990 (2023-03-28 16:32:15)

feat: renamed headers meta
Following #988 , the PR is ready for review.

This PR includes:

Added `Parser.meta` field called `renamedHeaders`.

- The value is `null` if the config is headless, or no renamed headers.
- The value is `Record<string, string>`, in which the keys are the renamed headers and the corresponding values are their original headers, or transformed headers.

The relevant unit tests are updated. I didn't assert all `meta` in existing unit tests by using `assert.deepEqual`. Instead, I used `assert.deepNestedInclude` to assert the values `renamedHeaders` are correct to avoid a monster PR.

FallingCeilingS: @mholt @pokoli - can you help merge this PR if everything looks okay? I realised I don't have the permission to do so.
mholt: LGTM -- sure. Thanks!

Patch:
diff --git a/docs/docs.html b/docs/docs.html
index cf198a5..49095b1 100644
--- a/docs/docs.html
+++ b/docs/docs.html
@@ -502,7 +502,8 @@ var csv = Papa.unparse({
 									<code>header</code>
 								</td>
 								<td>
-									If true, the first row of parsed data will be interpreted as field names. An array of field names will be returned in <a href="#meta">meta</a>, and each row of data will be an object of values keyed by field name instead of a simple array. Rows with a different number of fields from the header row will produce an error. Warning: Duplicate field names will overwrite values in previous fields having the same name.
+									If true, the first row of parsed data will be interpreted as field names. An array of field names will be returned in <a href="#meta">meta</a>, and each row of data will be an object of values keyed by field name instead of a simple array. Rows with a different number of fields from the header row will produce an error. 
+									Warning: Duplicated field names will be automatically renamed to avoid values in previous fields having the same name to be overwritten. Renamed fields with original (or transformed by <code>transformHeader</code>) are stored in <code>ParseResult.meta.renamedHeaders</code>
 								</td>
 							</tr>
 							<tr>
@@ -759,11 +760,13 @@ var csv = Papa.unparse({
 [
 	{
 		"Column 1": "foo",
-		"Column 2": "bar"
+		"Column 2": "bar",
+		"Column 1": "foo1",
 	},
 	{
 		"Column 1": "abc",
-		"Column 2": "def"
+		"Column 2": "def",
+		"Column 1": "abc1",
 	}
 ]</code></pre>
 					</div>
@@ -811,6 +814,7 @@ var csv = Papa.unparse({
 	aborted:   // Whether process was aborted
 	fields:    // Array of field names
 	truncated: // Whether preview consumed all input
+	renamedHeaders: // Headers that are automatically renamed by the library to avoid duplication. {Column 1_1: 'Column 1' // the later header 'Column 1' was renamed to 'Column 1_1'}
 }</code></pre>
 					</div>
 					<div class="grid-50">
diff --git a/papaparse.js b/papaparse.js
index 6495e7d..60997b0 100755
--- a/papaparse.js
+++ b/papaparse.js
@@ -1413,6 +1413,8 @@ License: MIT
 		var preview = config.preview;
 		var fastMode = config.fastMode;
 		var quoteChar;
+		var renamedHeaders = null;
+
 		if (config.quoteChar === undefined || config.quoteChar === null) {
 			quoteChar = '"';
 		} else {
@@ -1486,6 +1488,10 @@ License: MIT
 					if (count > 0) {
 						duplicateHeaders = true;
 						headerName = header + separator + count;
+						// Initialise the variable if it hasn't been.
+						if (renamedHeaders === null) {
+							renamedHeaders = {};
+						}
 					}
 					headerCount[header] = count + 1;
 					// In case it already exists, we add more separators
@@ -1493,6 +1499,9 @@ License: MIT
 						headerName = headerName + separator + count;
 					}
 					headerMap.add(headerName);
+					if (count > 0) {
+						renamedHeaders[headerName] = header;
+					}
 				}
 				if (duplicateHeaders) {
 					var editedInput = input.split(newline);
@@ -1769,7 +1778,8 @@ License: MIT
 						linebreak: newline,
 						aborted: aborted,
 						truncated: !!stopped,
-						cursor: lastCursor + (baseIndex || 0)
+						cursor: lastCursor + (baseIndex || 0),
+						renamedHeaders: renamedHeaders
 					}
 				};
 			}


Test patch:
diff --git a/tests/node-tests.js b/tests/node-tests.js
index 6d012ec..cad8058 100644
--- a/tests/node-tests.js
+++ b/tests/node-tests.js
@@ -32,6 +32,7 @@ function assertLongSampleParsedCorrectly(parsedCsv) {
 		"linebreak": "\n",
 		"aborted": false,
 		"truncated": false,
+		renamedHeaders: null,
 		"cursor": 1209
 	});
 	assert.equal(parsedCsv.errors.length, 0);
diff --git a/tests/test-cases.js b/tests/test-cases.js
index 0243a2c..1bccea6 100644
--- a/tests/test-cases.js
+++ b/tests/test-cases.js
@@ -29,7 +29,8 @@ var CORE_PARSER_TESTS = [
 		input: 'A,b,c',
 		expected: {
 			data: [['A', 'b', 'c']],
-			errors: []
+			errors: [],
+			meta: {delimiter: ',', renamedHeaders: null}
 		}
 	},
 	{
@@ -587,30 +588,33 @@ var CORE_PARSER_TESTS = [
 		}
 	},
 	{
-		description: "Simple duplicate header names",
+		description: "Simple duplicated header names",
 		input: 'A,A,A,A\n1,2,3,4',
 		config: { header: true },
 		expected: {
 			data: [['A', 'A_1', 'A_2', 'A_3'], ['1', '2', '3', '4']],
-			errors: []
+			errors: [],
+			meta: {renamedHeaders: {A_1: 'A', A_2: 'A', A_3: 'A'}}
 		}
 	},
 	{
-		description: "Duplicate header names with headerTransform",
+		description: "Duplicated header names with headerTransform",
 		input: 'A,A,A,A\n1,2,3,4',
 		config: { header: true, transformHeader: function(header) { return header.toLowerCase(); } },
 		expected: {
 			data: [['a', 'a_1', 'a_2', 'a_3'], ['1', '2', '3', '4']],
-			errors: []
+			errors: [],
+			meta: {renamedHeaders: {a_1: 'a', a_2: 'a', a_3: 'a'}}
 		}
 	},
 	{
-		description: "Duplicate header names existing column",
+		description: "Duplicated header names existing column",
 		input: 'c,c,c,c_1\n1,2,3,4',
 		config: { header: true },
 		expected: {
 			data: [['c', 'c_1', 'c_2', 'c_1_0'], ['1', '2', '3', '4']],
-			errors: []
+			errors: [],
+			meta: {renamedHeaders: {c_1: 'c', c_2: 'c'}}
 		}
 	},
 ];
@@ -621,6 +625,7 @@ describe('Core Parser Tests', function() {
 			var actual = new Papa.Parser(test.config).parse(test.input);
 			assert.deepEqual(actual.errors, test.expected.errors);
 			assert.deepEqual(actual.data, test.expected.data);
+			assert.deepNestedInclude(actual.meta, test.expected.meta || {});
 		});
 	}
 
@@ -1401,7 +1406,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 23,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1417,7 +1423,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 19,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1433,7 +1440,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 28,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1449,7 +1457,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 27,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1465,7 +1474,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 29,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1481,7 +1491,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 24,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1497,7 +1508,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 27,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},
@@ -1513,7 +1525,8 @@ var PARSE_TESTS = [
 				delimiter: ',',
 				cursor: 27,
 				aborted: false,
-				truncated: false
+				truncated: false,
+				renamedHeaders: null
 			}
 		}
 	},