@@ -114,6 +114,36 @@ TEST_CASE("Test Escaped Newline & Empty Last Column", "[read_csv_empty_last_colu
114114 vector<string>({ " 4" , " 5" , " 6" , " " }));
115115}
116116
117+ TEST_CASE (" Test Unquoted Trailing Empty Field" , " [read_csv_empty_last_column]" ) {
118+ auto rows = " A,B,C,D\r\n " // Header row
119+ " 1,2,3,\r\n "
120+ " 4,5,6," _csv;
121+
122+ CSVRow row;
123+ rows.read_row (row);
124+ REQUIRE (vector<string>(row) ==
125+ vector<string>({ " 1" , " 2" , " 3" , " " }));
126+
127+ rows.read_row (row);
128+ REQUIRE (vector<string>(row) ==
129+ vector<string>({ " 4" , " 5" , " 6" , " " }));
130+ }
131+
132+ TEST_CASE (" Test Quoted Empty Single-Column Row" , " [read_csv_empty_last_column]" ) {
133+ auto rows = " A\r\n " // Header row
134+ " \"\"\r\n "
135+ " value" _csv;
136+
137+ CSVRow row;
138+ rows.read_row (row);
139+ REQUIRE (vector<string>(row) ==
140+ vector<string>({ " " }));
141+
142+ rows.read_row (row);
143+ REQUIRE (vector<string>(row) ==
144+ vector<string>({ " value" }));
145+ }
146+
117147TEST_CASE ( " Test Empty Field" , " [read_empty_field]" ) {
118148 // Per RFC 1480, escaped quotes should be doubled up
119149 auto rows = " A,B,C\r\n " // Header row
@@ -183,8 +213,7 @@ TEST_CASE( "Test leading and trailing escaped quote", "[read_csv_quote]" ) {
183213}
184214// ! [Parse Example]
185215
186- // Verify the CSV parser can handle any arbitrary line endings composed of carriage return & newline
187- TEST_CASE (" Cursed Newlines" , " [read_csv_cursed_newline]" ) {
216+ TEST_CASE (" Normal Newlines" , " [read_csv_normal_newline]" ) {
188217 auto row_str = GENERATE (as<std::string> {},
189218 // Windows style
190219 " A,B,C\r\n " // Header row
@@ -198,20 +227,13 @@ TEST_CASE("Cursed Newlines", "[read_csv_cursed_newline]") {
198227 " 1,2,3\n "
199228 " 4,5,6" ,
200229
201- // Eww brother what is that...
202- " A,B,C\r\r\n " // Header row
203- " 123,234,345\r\r\n "
204- " 1,2,3\r\r\n "
205- " 4,5,6" ,
206-
207- // Doubled-up Windows style (ridiculous: but I'm sure it exists somewhere)
208- " A,B,C\r\n\r\n " // Header row
209- " 123,234,345\r\n\r\n "
210- " 1,2,3\r\n\r\n "
230+ // Old Mac Style
231+ " A,B,C\r " // Header row
232+ " 123,234,345\r "
233+ " 1,2,3\r "
211234 " 4,5,6"
212235 );
213236
214- // Set CSVFormat to KEEP all rows, even empty ones (because there shouldn't be any)
215237 CSVFormat format;
216238 format.header_row (0 ).variable_columns (VariableColumnPolicy::KEEP);
217239 auto rows = parse (row_str, format);
@@ -235,6 +257,85 @@ TEST_CASE("Cursed Newlines", "[read_csv_cursed_newline]") {
235257 REQUIRE (rows.n_rows () == 3 );
236258}
237259
260+
261+ TEST_CASE (" Edge-Case Newlines" , " [read_csv_edge_case_newline]" ) {
262+ auto row_str = GENERATE (as<std::string> {},
263+ // Eww brother what is that...
264+ " A,B,C\r\r\n " // Header row
265+ " 123,234,345\r\r\n "
266+ " 1,2,3\r\r\n "
267+ " 4,5,6" ,
268+
269+ // Doubled-up Windows style
270+ " A,B,C\r\n\r\n " // Header row
271+ " 123,234,345\r\n\r\n "
272+ " 1,2,3\r\n\r\n "
273+ " 4,5,6"
274+ );
275+
276+ SECTION (" KEEP policy - all rows including blanks" ) {
277+ CSVFormat format;
278+ format.header_row (0 ).variable_columns (VariableColumnPolicy::KEEP);
279+ auto rows = parse (row_str, format);
280+
281+ CSVRow row;
282+
283+ // Blank line from the trailing \r\n after the header
284+ rows.read_row (row);
285+ REQUIRE (row.empty ());
286+
287+ rows.read_row (row);
288+ vector<string> first_row = { " 123" , " 234" , " 345" };
289+ REQUIRE (vector<string>(row) == first_row);
290+ REQUIRE (row[" A" ] == " 123" );
291+ REQUIRE (row[" B" ] == " 234" );
292+ REQUIRE (row[" C" ] == " 345" );
293+
294+ // Blank line
295+ rows.read_row (row);
296+ REQUIRE (row.empty ());
297+
298+ rows.read_row (row);
299+ vector<string> second_row = { " 1" , " 2" , " 3" };
300+ REQUIRE (vector<string>(row) == second_row);
301+
302+ // Blank line
303+ rows.read_row (row);
304+ REQUIRE (row.empty ());
305+
306+ rows.read_row (row);
307+ vector<string> third_row = { " 4" , " 5" , " 6" };
308+ REQUIRE (vector<string>(row) == third_row);
309+
310+ REQUIRE (rows.n_rows () == 6 );
311+ }
312+
313+ SECTION (" KEEP_NON_EMPTY policy - skip blank rows" ) {
314+ CSVFormat format;
315+ format.header_row (0 ).variable_columns (VariableColumnPolicy::KEEP_NON_EMPTY);
316+ auto rows = parse (row_str, format);
317+
318+ CSVRow row;
319+
320+ rows.read_row (row);
321+ vector<string> first_row = { " 123" , " 234" , " 345" };
322+ REQUIRE (vector<string>(row) == first_row);
323+ REQUIRE (row[" A" ] == " 123" );
324+ REQUIRE (row[" B" ] == " 234" );
325+ REQUIRE (row[" C" ] == " 345" );
326+
327+ rows.read_row (row);
328+ vector<string> second_row = { " 1" , " 2" , " 3" };
329+ REQUIRE (vector<string>(row) == second_row);
330+
331+ rows.read_row (row);
332+ vector<string> third_row = { " 4" , " 5" , " 6" };
333+ REQUIRE (vector<string>(row) == third_row);
334+
335+ REQUIRE (rows.n_rows () == 3 );
336+ }
337+ }
338+
238339TEST_CASE (" Test Whitespace Trimming" , " [read_csv_trim]" ) {
239340 auto row_str = GENERATE (as<std::string> {},
240341 " A,B,C\r\n " // Header row
0 commit comments