From dcdf69ea2d29ae3055a22e55d6072d5c71de6088 Mon Sep 17 00:00:00 2001 From: AryanRG Date: Sun, 18 Oct 2026 02:37:22 +0530 Subject: [PATCH] [CSV-330] Preserve full header width for record consistency Keep the number of parsed header columns separate from the name-to-index map. Duplicate and empty names can collapse in that map, but records with all header positions must still be considered consistent. Cover both cases and add the Jira-linked changes entry. --- src/changes/changes.xml | 1 + .../org/apache/commons/csv/CSVParser.java | 21 +++++++++++-- .../org/apache/commons/csv/CSVRecord.java | 5 +-- .../org/apache/commons/csv/CSVRecordTest.java | 31 +++++++++++++++++-- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index d9b3963b8..59a1ae9b2 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -42,6 +42,7 @@ + CSVRecord.isConsistent() compares against the number of header columns when duplicate or empty header names are allowed. Remove Spotbugs dependency and use exclude-filter instead #564. Remove broken website link #577. Fix Apache RAT plugin console warnings. diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index eb4c0d329..06d3a6d6f 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -308,9 +308,15 @@ private static final class Headers { */ final List headerNames; - Headers(final Map headerMap, final List headerNames) { + /** + * Number of header columns, including duplicate and null names that do not add a distinct entry to {@link #headerMap}. + */ + final int headerCount; + + Headers(final Map headerMap, final List headerNames, final int headerCount) { this.headerMap = headerMap; this.headerNames = headerNames; + this.headerCount = headerCount; } } @@ -628,6 +634,7 @@ private Map createEmptyHeaderMap() { private Headers createHeaders() throws IOException { Map headerMap = null; List headerNames = null; + int headerCount = 0; final String[] formatHeader = format.getHeader(); if (formatHeader != null) { headerMap = createEmptyHeaderMap(); @@ -650,6 +657,7 @@ private Headers createHeaders() throws IOException { } // build the name to index mappings if (headerRecord != null) { + headerCount = headerRecord.length; // Track an occurrence of a null, empty or blank header. boolean observedMissing = false; for (int i = 0; i < headerRecord.length; i++) { @@ -679,7 +687,7 @@ private Headers createHeaders() throws IOException { } } // Make header names Collection immutable - return new Headers(headerMap, headerNames == null ? Collections.emptyList() : Collections.unmodifiableList(headerNames)); + return new Headers(headerMap, headerNames == null ? Collections.emptyList() : Collections.unmodifiableList(headerNames), headerCount); } /** @@ -747,6 +755,15 @@ Map getHeaderMapRaw() { return headers.headerMap; } + /** + * Gets the number of header columns, which can exceed the header map size when duplicate or null names are allowed. + * + * @return The number of header columns. + */ + int getHeaderCount() { + return headers.headerCount; + } + /** * Gets a read-only list of header names that iterates in column order as defined in the CSVFormat's header. *

diff --git a/src/main/java/org/apache/commons/csv/CSVRecord.java b/src/main/java/org/apache/commons/csv/CSVRecord.java index 9e8071379..2de2fe315 100644 --- a/src/main/java/org/apache/commons/csv/CSVRecord.java +++ b/src/main/java/org/apache/commons/csv/CSVRecord.java @@ -235,8 +235,9 @@ public boolean hasComment() { * @return true of this record is valid, false if not. */ public boolean isConsistent() { - final Map headerMap = getHeaderMapRaw(); - return headerMap == null || headerMap.size() == values.length; + // Compare with the header column count, not the header map size: duplicate or null header names allowed by + // DuplicateHeaderMode or allowMissingColumnNames share or lack a map entry. + return getHeaderMapRaw() == null || parser.getHeaderCount() == values.length; } /** diff --git a/src/test/java/org/apache/commons/csv/CSVRecordTest.java b/src/test/java/org/apache/commons/csv/CSVRecordTest.java index 821532975..344ac14c5 100644 --- a/src/test/java/org/apache/commons/csv/CSVRecordTest.java +++ b/src/test/java/org/apache/commons/csv/CSVRecordTest.java @@ -194,14 +194,39 @@ void testIsConsistent() { assertTrue(recordWithHeader.isConsistent()); } + /** + * A record whose size matches the header column count is consistent even when duplicate header names share a + * single header map entry. + */ + @Test + void testIsConsistentWithDuplicateHeaderNames() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_ALL).get(); + try (CSVParser parser = format.parse(new StringReader("a,b,a\n1,2,3\n1,2\n"))) { + final List records = parser.getRecords(); + assertTrue(records.get(0).isConsistent()); + assertFalse(records.get(1).isConsistent()); + } + } + + /** + * A record whose size matches the header column count is consistent even when empty header names share a single + * header map entry. + */ + @Test + void testIsConsistentWithDuplicateEmptyHeaderNames() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setAllowMissingColumnNames(true) + .setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_EMPTY).get(); + try (CSVParser parser = format.parse(new StringReader("a,,b,\n1,2,3,4\n"))) { + assertTrue(parser.nextRecord().isConsistent()); + } + } + @Test void testIsInconsistent() throws IOException { - final String[] headers = { "first", "second", "third" }; + final String[] headers = { "first", "second", "third", "fourth" }; final String rowData = StringUtils.join(values, ','); try (CSVParser parser = CSVFormat.DEFAULT.withHeader(headers).parse(new StringReader(rowData))) { - final Map map = parser.getHeaderMapRaw(); final CSVRecord record1 = parser.iterator().next(); - map.put("fourth", Integer.valueOf(4)); assertFalse(record1.isConsistent()); } }