Repository: jtablesaw/tablesaw
Pull request #922 (2021-05-02 08:27:38)

Fix rightOuter joining  on multiple tables.
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Fix bug of multi-table rightOuter joining (issue #907). The solution is recomputing `joinColumnIndexes` after each iteration. 

BTW, I checked `fullOuter` joining as well and added some tests on it. It works well so far.

## Testing

Added 5 new tests and modified 1 incorrect test.


Patch:
diff --git a/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java b/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java
index 107f738b..f1bb9f7b 100644
--- a/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java
+++ b/core/src/main/java/tech/tablesaw/joining/DataFrameJoiner.java
@@ -543,7 +543,11 @@ public class DataFrameJoiner {
   public Table rightOuter(boolean allowDuplicateColumnNames, Table... tables) {
     Table joined = table;
     for (Table table2 : tables) {
-      joined = rightOuter(table2, allowDuplicateColumnNames, joinColumnNames);
+      joined =
+          joinInternal(
+              joined, table2, JoinType.RIGHT_OUTER, allowDuplicateColumnNames, joinColumnNames);
+      joinColumnIndexes.clear();
+      joinColumnIndexes.addAll(getJoinIndexes(joined, joinColumnNames));
     }
     return joined;
   }


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java b/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java
index f2ab42d2..80ce265b 100644
--- a/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java
+++ b/core/src/test/java/tech/tablesaw/joining/DataFrameJoinerTest.java
@@ -93,6 +93,13 @@ public class DataFrameJoinerTest {
                   .join("ID,Dog Name", "1.1,Spot", "3.0,Fido", "4.0,Sasha", "5.0,King"),
               "Dogs");
 
+  private static final Table DOUBLE_INDEXED_DOGS_REVERSE =
+      Table.read()
+          .csv(
+              Joiner.on(System.lineSeparator())
+                  .join("Dog Name,ID", "Spot,1.1", "Fido,3.0", "Sasha,4.0", "King,5.0"),
+              "DogsReverse");
+
   private static final Table DOUBLE_INDEXED_CATS =
       Table.read()
           .csv(
@@ -603,12 +610,42 @@ public class DataFrameJoinerTest {
     assertEquals(4, joined.column("ID").size());
   }
 
+  @Test
+  public void rightOuterJoinWithDoubles2Reverse() {
+    Table joined = DOUBLE_INDEXED_PEOPLE.joinOn("ID").rightOuter(DOUBLE_INDEXED_DOGS_REVERSE);
+    assertEquals(3, joined.columnCount());
+    assertEquals(4, joined.rowCount());
+    assertEquals(4, joined.column("ID").size());
+  }
+
   @Test
   public void rightOuterJoinWithDoubles3() {
     Table joined =
         DOUBLE_INDEXED_PEOPLE.joinOn("ID").rightOuter(DOUBLE_INDEXED_DOGS, DOUBLE_INDEXED_CATS);
-    assertEquals(3, joined.columnCount());
+    assertTrue(
+        joined.columnNames().containsAll(Arrays.asList("ID", "Name", "Dog Name", "Cat Name")));
     assertEquals(4, joined.rowCount());
+    assertEquals(4, joined.column("ID").size());
+    assertEquals(0, joined.column("ID").countMissing());
+    assertEquals(4, joined.column("Name").size());
+    assertEquals(3, joined.column("Name").countMissing());
+    assertEquals(4, joined.column("Dog Name").size());
+    assertEquals(3, joined.column("Dog Name").countMissing());
+    assertEquals(4, joined.column("Cat Name").size());
+    assertEquals(0, joined.column("Cat Name").countMissing());
+  }
+
+  @Test
+  public void rightOuterJoinWithDoubles4() {
+    Table joined = DOUBLE_INDEXED_PEOPLE.joinOn("ID").rightOuter(DOUBLE_INDEXED_DOGS);
+    assertTrue(joined.columnNames().containsAll(Arrays.asList("ID", "Name", "Dog Name")));
+    assertEquals(4, joined.rowCount());
+    assertEquals(4, joined.column("ID").size());
+    assertEquals(0, joined.column("ID").countMissing());
+    assertEquals(4, joined.column("Dog Name").size());
+    assertEquals(0, joined.column("Dog Name").countMissing());
+    assertEquals(4, joined.column("Name").size());
+    assertEquals(1, joined.column("Name").countMissing());
   }
 
   @Test
@@ -782,6 +819,84 @@ public class DataFrameJoinerTest {
     assertEquals(2, joined.column("Feed").countMissing());
   }
 
+  @Test
+  public void fullOuterJoinMultiTable1() {
+    Table table1 = createGOODS1();
+    Table table2 = createGOODS2();
+    Table table3 = createGOODS3();
+    Table joined = table1.joinOn("Name").fullOuter(true, table2, table3);
+    assertEquals(7, joined.columnCount());
+    assertTrue(
+        joined
+            .columnNames()
+            .containsAll(
+                Arrays.asList("ID", "Name", "Price", "T2.ID", "T2.Price", "T3.ID", "T3.Price")));
+    assertEquals(6, joined.rowCount());
+    assertEquals(0, joined.column("ID").countMissing());
+    assertEquals(0, joined.column("Name").countMissing());
+    assertEquals(0, joined.column("Price").countMissing());
+    assertEquals(6, joined.column("T2.ID").size());
+    assertEquals(2, joined.column("T2.ID").countMissing());
+    assertEquals(6, joined.column("T2.Price").size());
+    assertEquals(2, joined.column("T2.Price").countMissing());
+    assertEquals(6, joined.column("T3.ID").size());
+    assertEquals(3, joined.column("T3.ID").countMissing());
+    assertEquals(6, joined.column("T3.Price").size());
+    assertEquals(3, joined.column("T3.Price").countMissing());
+  }
+
+  @Test
+  public void fullOuterJoinMultiTable2() {
+    Table table3 = createGOODS3();
+    Table table2 = createGOODS2();
+    Table table1 = createGOODS1();
+    Table joined = table3.joinOn("Name").fullOuter(true, table2, table1);
+    assertEquals(7, joined.columnCount());
+    assertTrue(
+        joined
+            .columnNames()
+            .containsAll(
+                Arrays.asList("ID", "Name", "Price", "T2.ID", "T2.Price", "T3.ID", "T3.Price")));
+    assertEquals(6, joined.rowCount());
+    assertEquals(3, joined.column("ID").countMissing());
+    assertEquals(0, joined.column("Name").countMissing());
+    assertEquals(3, joined.column("Price").countMissing());
+    assertEquals(6, joined.column("T2.ID").size());
+    assertEquals(2, joined.column("T2.ID").countMissing());
+    assertEquals(6, joined.column("T2.Price").size());
+    assertEquals(2, joined.column("T2.Price").countMissing());
+    assertEquals(6, joined.column("T3.ID").size());
+    assertEquals(0, joined.column("T3.ID").countMissing());
+    assertEquals(6, joined.column("T3.Price").size());
+    assertEquals(0, joined.column("T3.Price").countMissing());
+  }
+
+  @Test
+  public void fullOuterJoinMultiTable3() {
+    Table table2 = createGOODS2();
+    Table table3 = createGOODS3();
+    Table table1 = createGOODS1();
+    Table joined = table2.joinOn("Name").fullOuter(true, table3, table1);
+    assertEquals(7, joined.columnCount());
+    assertTrue(
+        joined
+            .columnNames()
+            .containsAll(
+                Arrays.asList("ID", "Name", "Price", "T2.ID", "T2.Price", "T3.ID", "T3.Price")));
+    assertEquals(6, joined.rowCount());
+    assertEquals(2, joined.column("ID").countMissing());
+    assertEquals(0, joined.column("Name").countMissing());
+    assertEquals(2, joined.column("Price").countMissing());
+    assertEquals(6, joined.column("T2.ID").size());
+    assertEquals(3, joined.column("T2.ID").countMissing());
+    assertEquals(6, joined.column("T2.Price").size());
+    assertEquals(3, joined.column("T2.Price").countMissing());
+    assertEquals(6, joined.column("T3.ID").size());
+    assertEquals(0, joined.column("T3.ID").countMissing());
+    assertEquals(6, joined.column("T3.Price").size());
+    assertEquals(0, joined.column("T3.Price").countMissing());
+  }
+
   @Test
   public void innerJoinStudentInstructorOnAge() {
     Table table1 = createSTUDENT();