Repository: jtablesaw/tablesaw
Pull request #1274 (2025-01-03 10:44:45)

Fix empty csv test
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

- Fix `CSVReaderTest.testEmptyFileHeaderEnabled` by enabling header - disabled header tested in `testEmptyFileHeaderDisabled`
- Fix `FileReader.getColumnNames` to not throw NPE when header is enabled and file is empty

## Testing

Fixed one



Patch:
diff --git a/docs-src/README.md b/CONTRIBUTING.md
similarity index 68%
rename from docs-src/README.md
rename to CONTRIBUTING.md
index 928b37ab..515f181c 100644
--- a/docs-src/README.md
+++ b/CONTRIBUTING.md
@@ -1,11 +1,12 @@
-## Compiled Docs
+# Contributing
+
+## Documentation
 
 There are several pieces that go into the docs.
 
 Note: Paths are all relative to the root of the project.
 
-1) Content files (markdown) files in `docs-src/main/`. These contain the content and links to code snippets defined in
-java src files.
+1) Content files (markdown) files in `docs-src/main/`. These contain the content and links to code snippets defined in java src files.
 
 2) src files (java) that hold the compiled code (can be anywhere in the project, but are generally in `docs-src/src/`)
 
@@ -13,11 +14,9 @@ java src files.
 
 4) The built docs that have not necessarily released are in `docs-src/dist/`
 
-5) The released/deployed docs are in `docs/` at the root of the project. (Github automatically serves whatever is
-in the `docs/` directory in the master branch)
-
+5) The released/deployed docs are in `docs/` at the root of the project. (Github automatically serves whatever is in the `docs/` directory in the master branch)
 
-### To update the Docs
+### Rebuilding and deploying the docs
 
 1) Navigate to the  `./docs-src` sub directory (Where POM is located).
 
@@ -32,10 +31,8 @@ This will inject snippets into content files and copy everything the content to
 4) To deploy the docs run the shell script `./deploy_docs.sh`.
 
 * This will copy the `docs-src/dist/` directory to `docs/`
-* Having a staging location for the built docs allows us to choose when to release them.
-  We can now work on the docs for a new feature and then release them at the same time the
-  feature is released.
+* Having a staging location for the built docs allows us to choose when to release them. We can now work on the docs for a new feature and then release them at the same time the feature is released.
     
- ### Notes
- For more background on injecting snippets see:
- https://github.com/ryancerf/choss-maven-plugin
+### Notes
+
+For more background on injecting snippets see [`ryancerf/choss-maven-plugin`](https://github.com/ryancerf/choss-maven-plugin).
diff --git a/core/src/main/java/tech/tablesaw/api/Row.java b/core/src/main/java/tech/tablesaw/api/Row.java
index 5cabeaa6..07c5f462 100644
--- a/core/src/main/java/tech/tablesaw/api/Row.java
+++ b/core/src/main/java/tech/tablesaw/api/Row.java
@@ -809,8 +809,29 @@ public class Row implements Iterator<Row> {
     return tableSlice.column(columnIndex);
   }
 
+  /** Returns true if every value is equal to the corresponding value in the given row */
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == null || obj.getClass() != this.getClass()) {
+        return false;
+    }
+
+    Row other = (Row) obj;
+    if (columnCount() != other.columnCount()) {
+      return false;
+    }
+
+    for (int columnIndex = 0; columnIndex < columnCount(); columnIndex++) {
+      if (!column(columnIndex).equals(getRowNumber(), other.getRowNumber())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   /** Returns a hash computed on the values in the backing table at this row */
-  public int rowHash() {
+  @Override
+  public int hashCode() {
     int[] values = new int[columnCount()];
     for (int i = 0; i < columnCount(); i++) {
       Column<?> column = tableSlice.column(i);
diff --git a/core/src/main/java/tech/tablesaw/api/Table.java b/core/src/main/java/tech/tablesaw/api/Table.java
index f099adc0..e1706f62 100644
--- a/core/src/main/java/tech/tablesaw/api/Table.java
+++ b/core/src/main/java/tech/tablesaw/api/Table.java
@@ -545,25 +545,6 @@ public class Table extends Relation implements Iterable<Row> {
     return true;
   }
 
-  /**
-   * Returns true if every value in row1 is equal to the same value in row2, where row1 and row2 are
-   * both rows from this table
-   */
-  private boolean duplicateRows(Row row1, Row row2) {
-    if (row1.columnCount() != row2.columnCount()) {
-      return false;
-    }
-    boolean result;
-    for (int columnIndex = 0; columnIndex < row1.columnCount(); columnIndex++) {
-      Column<?> c = column(columnIndex);
-      result = c.equals(row1.getRowNumber(), row2.getRowNumber());
-      if (!result) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   public Table[] sampleSplit(double table1Proportion) {
     Table[] tables = new Table[2];
     int table1Count = (int) Math.round(rowCount() * table1Proportion);
@@ -931,52 +912,17 @@ public class Table extends Relation implements Iterable<Row> {
    * this table, appears only once in the returned table.
    */
   public Table dropDuplicateRows() {
-
     Table temp = emptyCopy();
-    Int2ObjectMap<IntArrayList> uniqueHashes = new Int2ObjectOpenHashMap<>();
-    // ListMultimap<Integer, Integer> uniqueHashes = ArrayListMultimap.create();
+    Set uniqueRows = new HashSet<>();
     for (Row row : this) {
-      if (!isDuplicate(row, uniqueHashes)) {
+      if (!uniqueRows.contains(row)) {
+        uniqueRows.add(row);
         temp.append(row);
       }
     }
     return temp;
   }
 
-  /**
-   * Returns true if all the values in row are identical to those in another row previously seen and
-   * recorded in the list.
-   *
-   * @param row the row to evaluate
-   * @param uniqueHashes a map of row hashes to the id of an exemplar row that produces that hash.
-   *     If two different rows produce the same hash, then the row number for each is placed in the
-   *     list, so that there are exemplars for both
-   * @return true if the row's values exactly match a row that was previously seen
-   */
-  private boolean isDuplicate(Row row, Int2ObjectMap<IntArrayList> uniqueHashes) {
-    int hash = row.rowHash();
-    if (!uniqueHashes.containsKey(hash)) {
-      IntArrayList rowNumbers = new IntArrayList();
-      rowNumbers.add(row.getRowNumber());
-      uniqueHashes.put(hash, rowNumbers);
-      return false;
-    }
-
-    // the hashmap contains the hash, make sure the actual row values match
-    IntArrayList matchingKeys = uniqueHashes.get(hash);
-
-    for (int key : matchingKeys) {
-      Row oldRow = this.row(key);
-      if (duplicateRows(row, oldRow)) {
-        return true;
-      } else {
-        uniqueHashes.get(hash).add(row.getRowNumber());
-        return false;
-      }
-    }
-    return true;
-  }
-
   /** Returns only those records in this table that have no columns with missing values */
   public Table dropRowsWithMissingValues() {
 
@@ -1658,14 +1604,14 @@ public class Table extends Relation implements Iterable<Row> {
    * <p>This kind of structure often makes for a good intermediate format for performing subsequent
    * transformations. It is especially useful when combined with the {@link #cast()} operation
    *
-   * @param idVariables A list of column names intended to be used as identifiers. In he example,
+   * @param idVariables A list of column names intended to be used as identifiers. In the example,
    *     only patient_id would be an identifier
    * @param measuredVariables A list of columns intended to be used as measured variables. All
    *     columns must have the same type
    * @param dropMissing drop any row where the value is missing
    */
   public Table melt(
-      List<String> idVariables, List<NumericColumn<?>> measuredVariables, Boolean dropMissing) {
+      List<String> idVariables, List<NumericColumn<?>> measuredVariables, boolean dropMissing) {
 
     Table result = Table.create(name);
     for (String idColName : idVariables) {
diff --git a/core/src/main/java/tech/tablesaw/io/FileReader.java b/core/src/main/java/tech/tablesaw/io/FileReader.java
index 4c8f96e5..b3185049 100644
--- a/core/src/main/java/tech/tablesaw/io/FileReader.java
+++ b/core/src/main/java/tech/tablesaw/io/FileReader.java
@@ -94,6 +94,11 @@ public abstract class FileReader {
     if (options.header()) {
 
       String[] headerNames = parser.parseNext();
+      
+      if (headerNames == null) {
+        // no header because file is empty
+        return new String[] {};
+      }
 
       // work around issue where Univocity returns null if a column has no header.
       for (int i = 0; i < headerNames.length; i++) {


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
index e9b418c6..87ef7464 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -756,7 +756,7 @@ public class CsvReaderTest {
 
   @Test
   public void testEmptyFileHeaderEnabled() throws IOException {
-    Table table1 = Table.read().csv(CsvReadOptions.builder("../data/empty_file.csv").header(false));
+    Table table1 = Table.read().csv(CsvReadOptions.builder("../data/empty_file.csv").header(true));
     assertEquals("empty_file.csv: 0 rows X 0 cols", table1.shape());
   }