Repository: jtablesaw/tablesaw
Pull request #579 (2019-08-08 17:05:20)

Add mapToDoubleColumn on Table
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Change TableSliceGroup to iterate over Table rows.

Add MapTo[x] method to Abstract Relation class so they can be used by TableSlice and Table. 

Helps with #538 

If the implementation looks good I can update this PR to include other column types or send another PR.

## Testing

Yes


Patch:
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/NumberMapFunctions.java b/core/src/main/java/tech/tablesaw/columns/numbers/NumberMapFunctions.java
index fb8b550e..94144085 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/NumberMapFunctions.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/NumberMapFunctions.java
@@ -194,6 +194,31 @@ public interface NumberMapFunctions {
     return newColumn;
   }
 
+  default DoubleColumn power(NumericColumn<?> powerColumn) {
+    DoubleColumn result = DoubleColumn.create(name() + "[pow]", size());
+    for (int i = 0; i < size(); i++) {
+      if (isMissing(i) || (powerColumn.isMissing(i))) {
+        result.setMissing(i);
+      } else {
+        result.set(i, Math.pow(getDouble(i), powerColumn.getDouble(i)));
+      }
+    }
+    return result;
+  }
+
+  /** Returns a NumberColumn with the reciprocal (1/n) for each value n in this column */
+  default DoubleColumn reciprocal() {
+    DoubleColumn result = DoubleColumn.create(name() + "[1/n]", size());
+    for (int i = 0; i < size(); i++) {
+      if (isMissing(i)) {
+        result.setMissing(i);
+      } else {
+        result.set(i, 1 / getDouble(i));
+      }
+    }
+    return result;
+  }
+
   /** Returns a NumberColumn with the square of each value in this column */
   default DoubleColumn square() {
     DoubleColumn newColumn = power(2);
@@ -372,4 +397,6 @@ public interface NumberMapFunctions {
   }
 
   double getDouble(int i);
+
+  boolean isMissing(int rowNumber);
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/temporal/TemporalFilters.java b/core/src/main/java/tech/tablesaw/columns/temporal/TemporalFilters.java
index 00087105..1beaac26 100644
--- a/core/src/main/java/tech/tablesaw/columns/temporal/TemporalFilters.java
+++ b/core/src/main/java/tech/tablesaw/columns/temporal/TemporalFilters.java
@@ -1,9 +1,9 @@
 package tech.tablesaw.columns.temporal;
 
-import static tech.tablesaw.columns.temporal.TemporalPredicates.isMissing;
-import static tech.tablesaw.columns.temporal.TemporalPredicates.isNotMissing;
+import static tech.tablesaw.columns.temporal.TemporalPredicates.*;
 
 import it.unimi.dsi.fastutil.longs.LongArrayList;
+import java.time.Instant;
 import java.time.temporal.Temporal;
 import java.util.function.BiPredicate;
 import java.util.function.LongPredicate;
@@ -79,6 +79,18 @@ public interface TemporalFilters<T extends Temporal> extends Column<T> {
         .and(eval(PackedInstant::isOnOrBefore, highPackedDateTime));
   }
 
+  default Selection isAfter(Instant value) {
+    return eval(isGreaterThan, PackedInstant.pack(value));
+  }
+
+  default Selection isBefore(Instant value) {
+    return eval(isLessThan, PackedInstant.pack(value));
+  }
+
+  default Selection isEqualTo(Instant value) {
+    return eval(isEqualTo, PackedInstant.pack(value));
+  }
+
   @Override
   default Selection isMissing() {
     return eval(isMissing);
diff --git a/core/src/main/java/tech/tablesaw/filtering/DeferredInstantColumn.java b/core/src/main/java/tech/tablesaw/filtering/DeferredInstantColumn.java
index 1acd9497..226ea16b 100644
--- a/core/src/main/java/tech/tablesaw/filtering/DeferredInstantColumn.java
+++ b/core/src/main/java/tech/tablesaw/filtering/DeferredInstantColumn.java
@@ -1,8 +1,33 @@
 package tech.tablesaw.filtering;
 
+import java.time.Instant;
+import java.util.function.Function;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.selection.Selection;
+
 public class DeferredInstantColumn extends DeferredColumn {
 
   public DeferredInstantColumn(String columnName) {
     super(columnName);
   }
+
+  public Function<Table, Selection> isEqualTo(Instant value) {
+    return table -> table.instantColumn(getColumnName()).isEqualTo(value);
+  }
+
+  public Function<Table, Selection> isMissing() {
+    return table -> table.instantColumn(getColumnName()).isMissing();
+  }
+
+  public Function<Table, Selection> isNotMissing() {
+    return table -> table.instantColumn(getColumnName()).isNotMissing();
+  }
+
+  public Function<Table, Selection> isAfter(Instant value) {
+    return table -> table.instantColumn(getColumnName()).isAfter(value);
+  }
+
+  public Function<Table, Selection> isBefore(Instant value) {
+    return table -> table.instantColumn(getColumnName()).isBefore(value);
+  }
 }
diff --git a/core/src/main/java/tech/tablesaw/table/Relation.java b/core/src/main/java/tech/tablesaw/table/Relation.java
index badc4b5a..9b347057 100644
--- a/core/src/main/java/tech/tablesaw/table/Relation.java
+++ b/core/src/main/java/tech/tablesaw/table/Relation.java
@@ -18,7 +18,9 @@ import it.unimi.dsi.fastutil.ints.IntArrays;
 import java.io.ByteArrayOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import tech.tablesaw.api.BooleanColumn;
 import tech.tablesaw.api.CategoricalColumn;
@@ -32,6 +34,7 @@ import tech.tablesaw.api.IntColumn;
 import tech.tablesaw.api.LongColumn;
 import tech.tablesaw.api.NumberColumn;
 import tech.tablesaw.api.NumericColumn;
+import tech.tablesaw.api.Row;
 import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.api.StringColumn;
 import tech.tablesaw.api.Table;
@@ -47,7 +50,7 @@ import tech.tablesaw.sorting.comparators.DescendingIntComparator;
  * A tabular data structure like a table in a relational database, but not formally implementing the
  * relational algebra
  */
-public abstract class Relation {
+public abstract class Relation implements Iterable<Row> {
 
   public abstract Relation addColumns(Column<?>... cols);
 
@@ -529,4 +532,54 @@ public abstract class Relation {
   public boolean containsColumn(Column<?> column) {
     return columns().contains(column);
   }
+
+  /**
+   * Maps the supplied functions across all rows in the table appending the results to a new DoubleColumn.
+   *
+   * If you do not want the supplied function applied to rows with missing values you can supply a
+   * list of columns to check for missing values. For each row, it will check for a missing value in any of the
+   * listed columns. If there is a missing value a missing value will be appended to the results, otherwise
+   * the result of the function will be appended.
+   *
+   *
+   * @param name The name of the new column.
+   * @param function to map a table row to a double.
+   * @param skipIfMissingColNames the column names to check for missing values. If any of the
+   * columns contain a missing value (for a particular row) a missing value will be appended.
+   * @return DoubleColumn with the results appended.
+   */
+  public DoubleColumn mapToDoubleColumn(String name, Function<? super Row, ? extends Double> function, String... skipIfMissingColNames) {
+    return (DoubleColumn) mapToTypeInternal(DoubleColumn.create(name, rowCount()), function, skipIfMissingColNames);
+  }
+
+  private <T> Column<T> mapToTypeInternal(Column<T> destination,
+    Function<? super Row, ? extends T> function, String[] skipIfMissingColNames) {
+    List<Column<?>> checkMissingCols = Arrays.stream(skipIfMissingColNames).map(this::column).collect(Collectors.toList());
+    Iterator<Row> rowIterator = iterator();
+    int i = 0;
+    while(rowIterator.hasNext()) {
+      Row row = rowIterator.next();
+      if(isMissing(checkMissingCols, i)) {
+        destination.setMissing(i);
+      } else {
+        destination.set(i, function.apply(row));
+      }
+      i++;
+    }
+    return destination;
+  }
+
+  /**
+   * Checks every cell for the supplied list of columns and row index.
+   * If any cell contains missing data then return true;
+   */
+  private boolean isMissing(List<Column<?>> cols, int rowIndex) {
+    for (Column<?> col : cols) {
+      if (col.isMissing(rowIndex)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/table/TableSlice.java b/core/src/main/java/tech/tablesaw/table/TableSlice.java
index 5cd0f4dc..118bf112 100644
--- a/core/src/main/java/tech/tablesaw/table/TableSlice.java
+++ b/core/src/main/java/tech/tablesaw/table/TableSlice.java
@@ -17,9 +17,11 @@ package tech.tablesaw.table;
 import it.unimi.dsi.fastutil.ints.IntIterable;
 import it.unimi.dsi.fastutil.ints.IntIterator;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import tech.tablesaw.aggregate.NumericAggregateFunction;
 import tech.tablesaw.api.NumberColumn;
+import tech.tablesaw.api.Row;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.selection.BitmapBackedSelection;
@@ -31,7 +33,7 @@ import tech.tablesaw.selection.Selection;
  *
  * <p>A TableSlice is only good until the structure of the underlying table changes.
  */
-public class TableSlice extends Relation implements IntIterable {
+public class TableSlice extends Relation {
 
   private final Selection selection;
   private String name;
@@ -165,30 +167,23 @@ public class TableSlice extends Relation implements IntIterable {
   }
 
   /**
-   * Returns a 0 based int iterator for use with, for example, get(). When it returns 0 for the
-   * first row, get will transform that to the 0th row in the selection, which may not be the 0th
-   * row in the underlying table.
+   * Iterate of a copy of the table.
    */
   @Override
-  public IntIterator iterator() {
+  public Iterator<Row> iterator() {
 
-    return new IntIterator() {
+    return new Iterator<Row>() {
 
-      private int i = 0;
+      private final Row row = new Row(TableSlice.this.asTable());
 
       @Override
-      public int nextInt() {
-        return i++;
-      }
-
-      @Override
-      public int skip(int k) {
-        return i + k;
+      public Row next() {
+        return row.next();
       }
 
       @Override
       public boolean hasNext() {
-        return i < rowCount();
+        return row.hasNext();
       }
     };
   }


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/InstantColumnTest.java b/core/src/test/java/tech/tablesaw/api/InstantColumnTest.java
new file mode 100644
index 00000000..dbb997c6
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/api/InstantColumnTest.java
@@ -0,0 +1,59 @@
+package tech.tablesaw.api;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Instant;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class InstantColumnTest {
+
+  private final InstantColumn instanceColumn = InstantColumn.create("Test");
+
+  private Instant now = Instant.now();
+
+  private long baseline = now.getEpochSecond();
+  private long before = baseline - 100L;
+  private long after = baseline + 100L;
+
+  private Instant baselineInst = Instant.ofEpochSecond(baseline);
+  private Instant beforeInst = Instant.ofEpochSecond(before);
+  private Instant afterInst = Instant.ofEpochSecond(after);
+
+  @BeforeEach
+  void setUp() {
+
+    instanceColumn.append(beforeInst);
+    instanceColumn.append(baselineInst);
+    instanceColumn.append(afterInst);
+    instanceColumn.appendMissing();
+  }
+
+  @Test
+  void isAfter() {
+    assertEquals(2, instanceColumn.isAfter(baselineInst).get(0));
+  }
+
+  @Test
+  void isBefore() {
+    assertEquals(0, instanceColumn.isBefore(baselineInst).get(0));
+    assertEquals(1, instanceColumn.isBefore(afterInst).get(1));
+  }
+
+  @Test
+  void isEqualTo() {
+    assertEquals(2, instanceColumn.isEqualTo(afterInst).get(0));
+  }
+
+  @Test
+  void isMissing() {
+    assertEquals(3, instanceColumn.isMissing().get(0));
+  }
+
+  @Test
+  void isNotMissing() {
+    assertEquals(0, instanceColumn.isNotMissing().get(0));
+    assertEquals(1, instanceColumn.isNotMissing().get(1));
+    assertEquals(2, instanceColumn.isNotMissing().get(2));
+  }
+}
diff --git a/core/src/test/java/tech/tablesaw/api/TableTest.java b/core/src/test/java/tech/tablesaw/api/TableTest.java
index 313442ce..8f4848e5 100644
--- a/core/src/test/java/tech/tablesaw/api/TableTest.java
+++ b/core/src/test/java/tech/tablesaw/api/TableTest.java
@@ -39,6 +39,8 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.dates.PackedLocalDate;
+import tech.tablesaw.columns.numbers.DoubleColumnType;
+import tech.tablesaw.columns.numbers.IntColumnType;
 import tech.tablesaw.io.csv.CsvReadOptions;
 
 public class TableTest {
@@ -683,4 +685,27 @@ public class TableTest {
       fail("toString shouldn't throw " + e);
     }
   }
+
+  @Test
+  public void testMapToDouble() throws IOException {
+    Table table = Table.read().csv(CsvReadOptions.builder("../data/bush.csv"));
+
+    DoubleColumn doubleColumn = table.mapToDoubleColumn("mappedColumn",
+      r -> r.getInt("approval") * 1.0);
+
+    assertEquals("mappedColumn", doubleColumn.name());
+    assertArrayEquals(table.intColumn("approval").asDoubleArray(), doubleColumn.asDoubleArray());
+  }
+
+  @Test
+  public void testMapToDoubleWithMissingRowsSkipped() throws IOException {
+    IntColumn col11 = IntColumn.create("col1", new int[]{1, 2, 3, IntColumnType.missingValueIndicator()});
+    IntColumn col2 = IntColumn.create("col2", new int[]{1, 1, 1, 1});
+    Table t1 = Table.create("t1", col11, col2);
+
+    DoubleColumn doubleColumn = t1.mapToDoubleColumn("mappedColumn",
+      r -> r.getInt("col1") + r.getInt("col2") * 1.0, "col1", "col2");
+
+    assertArrayEquals(new double[]{2, 3, 4, DoubleColumnType.missingValueIndicator()}, doubleColumn.asDoubleArray());
+  }
 }
diff --git a/core/src/test/java/tech/tablesaw/filters/TimeDependentFilteringTest.java b/core/src/test/java/tech/tablesaw/filters/TimeDependentFilteringTest.java
index e4b0a889..333e27a0 100644
--- a/core/src/test/java/tech/tablesaw/filters/TimeDependentFilteringTest.java
+++ b/core/src/test/java/tech/tablesaw/filters/TimeDependentFilteringTest.java
@@ -33,6 +33,7 @@ import org.apache.commons.lang3.RandomUtils;
 import org.apache.commons.text.RandomStringGenerator;
 import tech.tablesaw.api.DateColumn;
 import tech.tablesaw.api.DoubleColumn;
+import tech.tablesaw.api.Row;
 import tech.tablesaw.api.StringColumn;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.columns.dates.PackedLocalDate;
@@ -113,11 +114,11 @@ public class TimeDependentFilteringTest {
       List<LocalDate> eventDates = new ArrayList<>();
 
       // iterate an individual table and find the rows where concept matches the target concept
-      for (int row : patientTable) {
+      for (Row row : patientTable) {
         StringColumn concepts = patientTable.stringColumn("concept");
         DateColumn dates = patientTable.dateColumn("date");
-        if (concepts.get(row).equals(conceptZ)) {
-          eventDates.add(dates.get(row));
+        if(row.getString("concept").equals(conceptZ)) {
+          eventDates.add(row.getDate("date"));
         }
       }
 
diff --git a/core/src/test/java/tech/tablesaw/selection/SelectionTest.java b/core/src/test/java/tech/tablesaw/selection/SelectionTest.java
index f7da0d40..d9a81193 100644
--- a/core/src/test/java/tech/tablesaw/selection/SelectionTest.java
+++ b/core/src/test/java/tech/tablesaw/selection/SelectionTest.java
@@ -7,10 +7,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.junit.jupiter.api.Test;
 
-public class SelectionTest {
+class SelectionTest {
 
   @Test
-  public void with() {
+  void with() {
     Selection selection = Selection.with(42, 53, 111);
     assertTrue(selection.contains(42));
     assertTrue(selection.contains(53));
@@ -21,7 +21,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void withoutRange() {
+  void withoutRange() {
     Selection selection = Selection.withoutRange(0, 130, 42, 53);
     assertFalse(selection.contains(42));
     assertFalse(selection.contains(43));
@@ -33,7 +33,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void withRange() {
+  void withRange() {
     Selection selection = Selection.withRange(42, 53);
     assertTrue(selection.contains(42));
     assertTrue(selection.contains(43));
@@ -45,14 +45,14 @@ public class SelectionTest {
   }
 
   @Test
-  public void toArray() {
+  void toArray() {
     Selection selection = Selection.with(42, 53, 111);
     int[] expected = {42, 53, 111};
     assertArrayEquals(expected, selection.toArray());
   }
 
   @Test
-  public void add() {
+  void add() {
     Selection selection = Selection.with(42, 53, 111);
     assertTrue(selection.contains(42));
     assertFalse(selection.contains(43));
@@ -60,7 +60,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void addRange() {
+  void addRange() {
     Selection selection = Selection.with(42, 53, 111);
     assertTrue(selection.contains(42));
     assertFalse(selection.contains(43));
@@ -71,13 +71,13 @@ public class SelectionTest {
   }
 
   @Test
-  public void size() {
+  void size() {
     Selection selection = Selection.with(42, 53, 111);
     assertEquals(3, selection.size());
   }
 
   @Test
-  public void and() {
+  void and() {
     Selection selection = Selection.with(42, 53, 111);
     Selection selection2 = Selection.with(11, 133, 53, 112);
     Selection selection3 = selection.and(selection2);
@@ -86,7 +86,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void or() {
+  void or() {
     Selection selection = Selection.with(42, 53, 111);
     Selection selection2 = Selection.with(11, 133, 53, 112);
     Selection selection3 = selection.or(selection2);
@@ -97,7 +97,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void andNot() {
+  void andNot() {
     Selection selection = Selection.with(42, 53, 111);
     Selection selection2 = Selection.with(11, 133, 53, 112);
     Selection selection3 = selection.andNot(selection2);
@@ -108,7 +108,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void isEmpty() {
+  void isEmpty() {
     Selection selection = Selection.with();
     assertTrue(selection.isEmpty());
 
@@ -117,7 +117,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void clear() {
+  void clear() {
     Selection selection1 = Selection.with(42, 53, 111);
     assertFalse(selection1.isEmpty());
 
@@ -126,14 +126,14 @@ public class SelectionTest {
   }
 
   @Test
-  public void get() {
+  void get() {
     Selection selection = Selection.with(42, 53, 111);
     assertEquals(42, selection.get(0));
     assertEquals(53, selection.get(1));
   }
 
   @Test
-  public void remove() {
+  void remove() {
     Selection selection = Selection.with(42, 53, 111);
     assertTrue(selection.contains(53));
 
@@ -143,7 +143,7 @@ public class SelectionTest {
   }
 
   @Test
-  public void flip() {
+  void flip() {
     Selection selection = Selection.with(42, 53, 111);
     assertTrue(selection.contains(53));
     assertTrue(selection.contains(42));