Repository: jtablesaw/tablesaw (java)

Issue:
Create Better Column Not Found Error Message for Row
Ran into this today while using the library.

`row.getLong("colDoesNotExist")` throws an NPE. We should create a better error message. Possibly create a custom ColumnNotFoundException (unchecked) and use it project wide.

Current error message:

```
Exception in thread "main" java.lang.NullPointerException
	at tech.tablesaw.api.Row.getLong(Row.java:153)
	at com.ryancerf.tgo.steps.CreateInvoicesStep.createInvoices(CreateInvoicesStep.java:23)
	at com.ryancerf.tgo.Main.main(Main.java:40)
```

This error also occurs when the column name exists on the table, but the type is wrong. It would be nice if we searched the possible column names and gave the user the hint that they probably got the wrong `row.get` method.

If no one has grabbed this I should have time to work on it in a few days.

Pull request description:
Provide more helpful error messages when a column cannot be found on Row
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Fixes #617

New Error Msgs look like:

For missing column:
```
Column col2 is not present in table myTable
```

For using wrong type.
```
Column col1 is of type LOCAL_DATE and cannot be cast to LOCAL_TIME. Use the method for LOCAL_DATE.
```


## Testing

Yes. Added tests.

Patch:
diff --git a/core/src/main/java/tech/tablesaw/api/Row.java b/core/src/main/java/tech/tablesaw/api/Row.java
index 10b75749..0f6d61b8 100644
--- a/core/src/main/java/tech/tablesaw/api/Row.java
+++ b/core/src/main/java/tech/tablesaw/api/Row.java
@@ -4,39 +4,90 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.table.TableSlice;
 
 public class Row implements Iterator<Row> {
 
+  /**
+   * Wrap Map of column name to Column map and provide helpful error messages to the user when a
+   * column name cannot be found.
+   */
+  private class ColumnMap<T> {
+    private final Map<String, T> columnMap = new HashMap<>();
+    private final ColumnType columnType;
+
+    public ColumnMap(ColumnType columnType) {
+      this.columnType = columnType;
+    }
+
+    ColumnMap() {
+      this.columnType = null;
+    }
+
+    T get(String columnName) {
+      T column = columnMap.get(columnName.toLowerCase());
+      if (column == null) {
+        throwWrongTypeError(columnName);
+        throwColumnNotPresentError(columnName);
+      }
+      return column;
+    }
+
+    void put(String columnName, T column) {
+      columnMap.put(columnName.toLowerCase(), column);
+    }
+
+    /**
+     * Will get thrown when column name is correct, but used the wrong method get/set is called. E.G. the user called
+     * .getLong on an IntColumn.
+     */
+    private void throwWrongTypeError(String columnName) {
+      for (int i = 0; i < columnNames.length; i++) {
+        if (columnName.equals(columnNames[i])) {
+          String actualType = tableSlice.getTable().columns().get(i).type().name();
+          String proposedType = columnTypeName();
+          throw new IllegalArgumentException(
+              String.format(
+                  "Column %s is of type %s and cannot be cast to %s. Use the method for %s.",
+                  columnName, actualType, proposedType, actualType));
+        }
+      }
+    }
+
+    private void throwColumnNotPresentError(String columnName) {
+      throw new IllegalStateException(
+          String.format(
+              "Column %s is not present in table %s", columnName, tableSlice.getTable().name()));
+    }
+
+    private String columnTypeName() {
+      if (this.columnType != null) {
+        return this.columnType.name();
+      }
+      return "Object";
+    }
+  }
+
   private final TableSlice tableSlice;
   private final String[] columnNames;
-  private final Map<String, DateColumn> dateColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, DoubleColumn> doubleColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, IntColumn> intColumnMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, LongColumn> longColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, ShortColumn> shortColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, FloatColumn> floatColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, Column<String>> stringColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, BooleanColumn> booleanColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, DateTimeColumn> dateTimeColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, InstantColumn> instantColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, TimeColumn> timeColumnMap =
-      new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-  private final Map<String, Column<?>> columnMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+  private final ColumnMap<DateColumn> dateColumnMap = new ColumnMap<>();
+  private final ColumnMap<DoubleColumn> doubleColumnMap = new ColumnMap<>(ColumnType.DOUBLE);
+  private final ColumnMap<IntColumn> intColumnMap = new ColumnMap<>(ColumnType.INTEGER);
+  private final ColumnMap<LongColumn> longColumnMap = new ColumnMap<>(ColumnType.LONG);
+  private final ColumnMap<ShortColumn> shortColumnMap = new ColumnMap<>(ColumnType.SHORT);
+  private final ColumnMap<FloatColumn> floatColumnMap = new ColumnMap<>(ColumnType.FLOAT);
+  private final ColumnMap<Column<String>> stringColumnMap = new ColumnMap<>(ColumnType.STRING);
+  private final ColumnMap<BooleanColumn> booleanColumnMap = new ColumnMap<>(ColumnType.BOOLEAN);
+  private final ColumnMap<DateTimeColumn> dateTimeColumnMap =
+      new ColumnMap<>(ColumnType.LOCAL_DATE_TIME);
+  private final ColumnMap<InstantColumn> instantColumnMap = new ColumnMap<>(ColumnType.INSTANT);
+  private final ColumnMap<TimeColumn> timeColumnMap = new ColumnMap<>(ColumnType.LOCAL_TIME);
+  private final ColumnMap<Column<?>> columnMap = new ColumnMap<>();
   private int rowNumber;
 
   public Row(Table table) {


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/RowTest.java b/core/src/test/java/tech/tablesaw/api/RowTest.java
index ac8e4197..76c5ec6c 100644
--- a/core/src/test/java/tech/tablesaw/api/RowTest.java
+++ b/core/src/test/java/tech/tablesaw/api/RowTest.java
@@ -2,6 +2,7 @@ package tech.tablesaw.api;
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static tech.tablesaw.api.ColumnType.BOOLEAN;
 import static tech.tablesaw.api.ColumnType.DOUBLE;
 import static tech.tablesaw.api.ColumnType.FLOAT;
@@ -627,4 +628,28 @@ public class RowTest {
 
     assertArrayEquals(expected, actual);
   }
+
+  @Test
+  public void columnDoesNotExistOnRow() {
+    Table table = Table.create("myTable", IntColumn.create("col1", new int[] {1}));
+
+    Throwable thrown =
+        assertThrows(IllegalStateException.class, () -> table.forEach(r -> r.getInt("col2")));
+
+    assertEquals("Column col2 is not present in table myTable", thrown.getMessage());
+  }
+
+  @Test
+  public void columnExistsButWrongType() {
+    Table table = Table.create("myTale", DateColumn.create("col1", new LocalDate[] {null}));
+
+    Throwable thrown =
+        assertThrows(
+            IllegalArgumentException.class, () -> table.forEach(r -> r.setTime("col1", null)));
+
+    assertEquals(
+        "Column col1 is of type LOCAL_DATE and cannot be cast to LOCAL_TIME."
+            + " Use the method for LOCAL_DATE.",
+        thrown.getMessage());
+  }
 }