Repository: jtablesaw/tablesaw
Pull request #1269 (2025-01-03 09:48:08)

Add test for #1197
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

- Add test for #1197
- Code refactored to call toLowerCase() only once

## Testing

Yes



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/columns/dates/PackedLocalDate.java b/core/src/main/java/tech/tablesaw/columns/dates/PackedLocalDate.java
index 0e736982..db2480d4 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/PackedLocalDate.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/PackedLocalDate.java
@@ -362,7 +362,7 @@ public class PackedLocalDate {
     byte m = getMonthValue(packedDate);
     short y = getYear(packedDate);
 
-    int newYear = YEAR.checkValidIntValue(yearsToAdd + y);
+    int newYear = YEAR.checkValidIntValue((long) yearsToAdd + y);
     return resolvePreviousValid(newYear, m, d);
   }
 
diff --git a/core/src/main/java/tech/tablesaw/io/FileReader.java b/core/src/main/java/tech/tablesaw/io/FileReader.java
index 4c8f96e5..14ceed46 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++) {
@@ -132,12 +137,13 @@ public abstract class FileReader {
     Map<String, Integer> nameCounter = new HashMap<>();
     for (int i = 0; i < headerNames.length; i++) {
       String name = headerNames[i];
-      Integer count = nameCounter.get(name.toLowerCase());
+      String lowerCase = name.toLowerCase();
+      Integer count = nameCounter.get(lowerCase);
       if (count == null) {
-        nameCounter.put(name.toLowerCase(), 1);
+        nameCounter.put(lowerCase, 1);
       } else {
         count++;
-        nameCounter.put(name.toLowerCase(), count);
+        nameCounter.put(lowerCase, count);
         headerNames[i] = name + "-" + count;
       }
     }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
index 30a25873..aa947ff3 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Layout.java
@@ -22,7 +22,7 @@ public class Layout {
   private static final String DEFAULT_DECIMAL_SEPARATOR = ".";
   private static final String DEFAULT_THOUSANDS_SEPARATOR = ",";
   private static final boolean DEFAULT_AUTO_SIZE = false;
-  private static final HoverMode DEFAULT_HOVER_MODE = HoverMode.FALSE;
+  private static final HoverMode DEFAULT_HOVER_MODE = HoverMode.CLOSEST;
   private static final DragMode DEFAULT_DRAG_MODE = DragMode.ZOOM;
   private static final int DEFAULT_HOVER_DISTANCE = 20;
   private static final BarMode DEFAULT_BAR_MODE = BarMode.GROUP;


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..2c431bd6 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -161,6 +161,17 @@ public class CsvReaderTest {
     assertEquals("Col1-2", dupes.columnNames().get(1));
   }
 
+  @Test
+  void allowDuplicateColumnNamesInsensitive() {
+    Reader reader2 =
+        new StringReader(
+            "Col1" + COMMA + "col1" + LINE_END + "first" + COMMA + "second" + LINE_END);
+    Table dupes =
+        Table.read().csv(CsvReadOptions.builder(reader2).allowDuplicateColumnNames(true).build());
+    assertEquals("Col1", dupes.columnNames().get(0));
+    assertEquals("col1-2", dupes.columnNames().get(1));
+  }
+
   @Test
   public void testWithColumnSKIPWithoutHeader() throws IOException {
     // Read the CSV file
@@ -756,7 +767,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());
   }
 
diff --git a/json/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java b/json/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
index 6eeb3427..d2382981 100644
--- a/json/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
+++ b/json/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
@@ -22,10 +22,14 @@ import static tech.tablesaw.api.ColumnType.STRING;
 
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.IntColumn;
 import tech.tablesaw.api.Table;
+import tech.tablesaw.io.Source;
 
 public class JsonReaderTest {
 
@@ -110,4 +114,18 @@ public class JsonReaderTest {
 
     assertArrayEquals(columnTypes, new ColumnType[] {STRING, STRING});
   }
+  
+  @Test
+  void testColumnOrderIsPreserved() {
+      String json =
+          "[{\"日期\":\"1991-04-03\",\"开盘\":49,\"收盘\":49,\"最高\":49,\"最低\":49,\"成交量\":1,\"成交额\":5000,\"振幅\":0,\"涨跌幅\":22.5,\"涨跌额\":9,\"换手率\":0},"
+          + "{\"日期\":\"1991-04-04\",\"开盘\":48.76,\"收盘\":48.76,\"最高\":48.76,\"最低\":48.76,\"成交量\":3,\"成交额\":15000,\"振幅\":0,\"涨跌幅\":-0.49,\"涨跌额\":-0.24,\"换手率\":0}]\n";
+      
+      List<String> header = Arrays.asList("日期", "开盘", "收盘", "最高", "最低", "成交量", "成交额", "振幅", "涨跌幅", "涨跌额", "换手率");
+      
+      Table table = new JsonReader().read(Source.fromString(json));
+
+      assertEquals(header, table.columnNames());
+  }
+
 }