Repository: jtablesaw/tablesaw
Pull request #456 (2019-02-01 03:27:08)

Line endings
Thanks for contributing.

- [X] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

@lwhite1 I think this is a new test, but let me know if it's duplicating an existing one



Patch:
diff --git a/aggregate/pom.xml b/aggregate/pom.xml
index 5941e81a..99d0cda4 100644
--- a/aggregate/pom.xml
+++ b/aggregate/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.30.3-SNAPSHOT</version>
+    <version>0.30.4-SNAPSHOT</version>
   </parent>
 
   <!--
@@ -51,17 +51,17 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.30.3-SNAPSHOT</version>
+      <version>0.30.4-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-jsplot</artifactId>
-      <version>0.30.3-SNAPSHOT</version>
+      <version>0.30.4-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-beakerx</artifactId>
-      <version>0.30.3-SNAPSHOT</version>
+      <version>0.30.4-SNAPSHOT</version>
     </dependency>
   </dependencies>
 </project>
diff --git a/beakerx/pom.xml b/beakerx/pom.xml
index 0a7684c6..d5f79f06 100644
--- a/beakerx/pom.xml
+++ b/beakerx/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.30.3-SNAPSHOT</version>
+    <version>0.30.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>tablesaw-beakerx</artifactId>
@@ -63,7 +63,7 @@
     <dependency>
       <groupId>tech.tablesaw</groupId>
       <artifactId>tablesaw-core</artifactId>
-      <version>0.30.3-SNAPSHOT</version>
+      <version>0.30.4-SNAPSHOT</version>
     </dependency>
     <dependency>
       <groupId>com.github.twosigma</groupId>
diff --git a/core/pom.xml b/core/pom.xml
index 721fb870..eb4c6189 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>tech.tablesaw</groupId>
         <artifactId>tablesaw-parent</artifactId>
-        <version>0.30.3-SNAPSHOT</version>
+        <version>0.30.4-SNAPSHOT</version>
     </parent>
 
     <artifactId>tablesaw-core</artifactId>
@@ -90,6 +90,16 @@
             <artifactId>guava</artifactId>
             <version>25.1-jre</version>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.9.8</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.wnameless</groupId>
+            <artifactId>json-flattener</artifactId>
+            <version>0.6.0</version>
+        </dependency>
         <dependency>
             <groupId>com.univocity</groupId>
             <artifactId>univocity-parsers</artifactId>
diff --git a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
index 687ca3f1..301f23ea 100644
--- a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
@@ -326,7 +326,11 @@ public class BooleanColumn extends AbstractColumn<Boolean> implements BooleanMap
 
     @Override
     public String getUnformattedString(int row) {
-        return String.valueOf(get(row));
+        Boolean b = get(row);
+        if (b == null) {
+            return "";
+        }
+        return String.valueOf(b);
     }
 
     @Override
diff --git a/core/src/main/java/tech/tablesaw/api/ColumnType.java b/core/src/main/java/tech/tablesaw/api/ColumnType.java
index b05eea9f..8d671773 100644
--- a/core/src/main/java/tech/tablesaw/api/ColumnType.java
+++ b/core/src/main/java/tech/tablesaw/api/ColumnType.java
@@ -15,7 +15,7 @@ import tech.tablesaw.columns.numbers.ShortColumnType;
 import tech.tablesaw.columns.strings.StringColumnType;
 import tech.tablesaw.columns.strings.TextColumnType;
 import tech.tablesaw.columns.times.TimeColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -64,7 +64,7 @@ public interface ColumnType {
 
     String getPrinterFriendlyName();
 
-    AbstractParser<?> customParser(CsvReadOptions options);
+    AbstractParser<?> customParser(ReadOptions options);
 
     default boolean compare(int rowNumber, Column<?> temp, Column<?> original) {
         return original.get(rowNumber).equals(temp.get(temp.size() - 1));
diff --git a/core/src/main/java/tech/tablesaw/api/Row.java b/core/src/main/java/tech/tablesaw/api/Row.java
index d1a91848..bc9db6fb 100644
--- a/core/src/main/java/tech/tablesaw/api/Row.java
+++ b/core/src/main/java/tech/tablesaw/api/Row.java
@@ -196,11 +196,11 @@ public class Row implements Iterator<Row> {
         return dateTimeColumnMap.get(columnNames[columnIndex]).getLongInternal(rowNumber);
     }
 
-    public boolean getBoolean(String columnName) {
+    public Boolean getBoolean(String columnName) {
         return booleanColumnMap.get(columnName).get(rowNumber);
     }
 
-    public boolean getBoolean(int columnIndex) {
+    public Boolean getBoolean(int columnIndex) {
         return getBoolean(columnNames[columnIndex]);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/api/ShortColumn.java b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
index cc0d1441..ed08ed5c 100644
--- a/core/src/main/java/tech/tablesaw/api/ShortColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
@@ -10,10 +10,9 @@ import it.unimi.dsi.fastutil.shorts.ShortComparator;
 import it.unimi.dsi.fastutil.shorts.ShortListIterator;
 import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
 import it.unimi.dsi.fastutil.shorts.ShortSet;
-import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.AbstractParser;
+import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.numbers.DoubleColumnType;
-import tech.tablesaw.columns.numbers.IntColumnType;
 import tech.tablesaw.columns.numbers.NumberColumnFormatter;
 import tech.tablesaw.columns.numbers.ShortColumnType;
 import tech.tablesaw.selection.Selection;
@@ -355,7 +354,7 @@ public class ShortColumn extends NumberColumn<Short> implements CategoricalColum
     @Override
     public String getUnformattedString(final int row) {
         final int value = getInt(row);
-        if (IntColumnType.isMissingValue(value)) {
+        if (ShortColumnType.isMissingValue(value)) {
             return "";
         }
         return String.valueOf(value);
diff --git a/core/src/main/java/tech/tablesaw/api/TimeColumn.java b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
index 3b26133f..30f1b81a 100644
--- a/core/src/main/java/tech/tablesaw/api/TimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
@@ -39,10 +39,8 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -504,15 +502,6 @@ public class TimeColumn extends AbstractColumn<LocalTime>
         return data.iterator();
     }
 
-    Set<LocalTime> asSet() {
-        Set<LocalTime> times = new HashSet<>();
-        TimeColumn unique = unique();
-        for (LocalTime t : unique) {
-            times.add(t);
-        }
-        return times;
-    }
-
     public boolean contains(LocalTime time) {
         int t = PackedLocalTime.pack(time);
         return data().contains(t);
diff --git a/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java b/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
index 7c8ec504..68e7bf51 100644
--- a/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
@@ -1,6 +1,6 @@
 package tech.tablesaw.columns;
 
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class SkipColumnType extends AbstractColumnType {
 
@@ -22,7 +22,7 @@ public class SkipColumnType extends AbstractColumnType {
     }
 
     @Override
-    public AbstractParser<?> customParser(CsvReadOptions options) {
+    public AbstractParser<?> customParser(ReadOptions options) {
         throw new UnsupportedOperationException("Column type " + name() + " doesn't support parsing");
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
index 173b5575..47c5aaaa 100644
--- a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.booleans;
 import tech.tablesaw.api.BooleanColumn;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class BooleanColumnType extends AbstractColumnType {
 
@@ -36,7 +36,7 @@ public class BooleanColumnType extends AbstractColumnType {
     }
 
     @Override
-    public BooleanParser customParser(CsvReadOptions readOptions) {
+    public BooleanParser customParser(ReadOptions readOptions) {
         return new BooleanParser(this, readOptions);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java
index f4544ba3..4e4aa762 100644
--- a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.booleans;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.util.Arrays;
 import java.util.List;
@@ -34,7 +34,7 @@ public class BooleanParser extends AbstractParser<Boolean> {
         super(columnType);
     }
 
-    public BooleanParser(BooleanColumnType booleanColumnType, CsvReadOptions readOptions) {
+    public BooleanParser(BooleanColumnType booleanColumnType, ReadOptions readOptions) {
         super(booleanColumnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
index a245c02c..975a326b 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
@@ -4,7 +4,7 @@ import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DateColumn;
 import tech.tablesaw.columns.AbstractColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalDate;
 
@@ -32,7 +32,7 @@ public class DateColumnType extends AbstractColumnType {
     }
 
     @Override
-    public AbstractParser<LocalDate> customParser(CsvReadOptions options) {
+    public AbstractParser<LocalDate> customParser(ReadOptions options) {
         return new DateParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java b/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java
index fc25cfcb..ea4b65b1 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.dates;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
@@ -61,7 +61,7 @@ public class DateParser extends AbstractParser<LocalDate> {
     private Locale locale = Locale.getDefault();
     private DateTimeFormatter formatter = DEFAULT_FORMATTER;
 
-    public DateParser(ColumnType type, CsvReadOptions readOptions) {
+    public DateParser(ColumnType type, ReadOptions readOptions) {
         super(type);
         DateTimeFormatter readCsvFormatter = readOptions.dateFormatter();
         if (readCsvFormatter != null) {
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
index c8f8bd4b..0bac2a5d 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.datetimes;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class DateTimeColumnType extends AbstractColumnType {
 
@@ -31,7 +31,7 @@ public class DateTimeColumnType extends AbstractColumnType {
     }
 
     @Override
-    public DateTimeParser customParser(CsvReadOptions options) {
+    public DateTimeParser customParser(ReadOptions options) {
         return new DateTimeParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java
index a986fff3..a9048f86 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java
@@ -5,7 +5,7 @@ import com.google.common.collect.Lists;
 
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -56,7 +56,7 @@ public class DateTimeParser extends AbstractParser<LocalDateTime> {
         super(columnType);
     }
 
-    public DateTimeParser(DateTimeColumnType dateTimeColumnType, CsvReadOptions readOptions) {
+    public DateTimeParser(DateTimeColumnType dateTimeColumnType, ReadOptions readOptions) {
         super(dateTimeColumnType);
         DateTimeFormatter readCsvFormatter = readOptions.dateFormatter();
         if (readCsvFormatter != null) {
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
index b8c2b911..ac368d36 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DoubleColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class DoubleColumnType extends AbstractColumnType {
 
@@ -11,6 +11,9 @@ public class DoubleColumnType extends AbstractColumnType {
 
     public static final DoubleParser DEFAULT_PARSER = new DoubleParser(ColumnType.DOUBLE);
 
+    private static DoubleColumnType INSTANCE =
+            new DoubleColumnType(BYTE_SIZE, "DOUBLE", "Double");
+
     public static DoubleColumnType instance() {
         if (INSTANCE == null) {
             INSTANCE = new DoubleColumnType(BYTE_SIZE, "DOUBLE", "Double");
@@ -18,9 +21,6 @@ public class DoubleColumnType extends AbstractColumnType {
         return INSTANCE;
     }
 
-    private static DoubleColumnType INSTANCE =
-            new DoubleColumnType(BYTE_SIZE, "DOUBLE", "Double");
-
     private DoubleColumnType(int byteSize, String name, String printerFriendlyName) {
         super(byteSize, name, printerFriendlyName);
     }
@@ -31,7 +31,7 @@ public class DoubleColumnType extends AbstractColumnType {
     }
 
     @Override
-    public DoubleParser customParser(CsvReadOptions options) {
+    public DoubleParser customParser(ReadOptions options) {
         return new DoubleParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java
index 18312883..6e004978 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class DoubleParser extends AbstractParser<Double> {
 
@@ -11,7 +11,7 @@ public class DoubleParser extends AbstractParser<Double> {
         super(columnType);
     }
 
-    public DoubleParser(DoubleColumnType doubleColumnType, CsvReadOptions readOptions) {
+    public DoubleParser(DoubleColumnType doubleColumnType, ReadOptions readOptions) {
         super(doubleColumnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java
index 6a29f8aa..9b94053c 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.FloatColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class FloatColumnType extends AbstractColumnType {
 
@@ -30,7 +30,7 @@ public class FloatColumnType extends AbstractColumnType {
     }
 
     @Override
-    public FloatParser customParser(CsvReadOptions options) {
+    public FloatParser customParser(ReadOptions options) {
         return new FloatParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java
index 3609bf92..3edae500 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class FloatParser extends AbstractParser<Float> {
 
@@ -11,7 +11,7 @@ public class FloatParser extends AbstractParser<Float> {
         super(columnType);
     }
 
-    public FloatParser(FloatColumnType columnType, CsvReadOptions readOptions) {
+    public FloatParser(FloatColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java
index d703fbef..60452959 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.IntColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class IntColumnType extends AbstractColumnType {
 
@@ -30,7 +30,7 @@ public class IntColumnType extends AbstractColumnType {
     }
 
     @Override
-    public IntParser customParser(CsvReadOptions options) {
+    public IntParser customParser(ReadOptions options) {
         return new IntParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java
index 2140f86a..77c47d9a 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class IntParser extends AbstractParser<Integer> {
 
@@ -11,7 +11,7 @@ public class IntParser extends AbstractParser<Integer> {
         super(columnType);
     }
 
-    public IntParser(IntColumnType columnType, CsvReadOptions readOptions) {
+    public IntParser(IntColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java
index e833289a..f41c7d6b 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.LongColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class LongColumnType extends AbstractColumnType {
 
@@ -33,7 +33,7 @@ public class LongColumnType extends AbstractColumnType {
     }
 
     @Override
-    public LongParser customParser(CsvReadOptions options) {
+    public LongParser customParser(ReadOptions options) {
         return new LongParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java
index eba352a8..14ecd6c0 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class LongParser extends AbstractParser<Long> {
 
@@ -11,7 +11,7 @@ public class LongParser extends AbstractParser<Long> {
         super(columnType);
     }
 
-    public LongParser(LongColumnType columnType, CsvReadOptions readOptions) {
+    public LongParser(LongColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java
index 53535233..0e287f65 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java
@@ -2,7 +2,7 @@ package tech.tablesaw.columns.numbers;
 
 import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class ShortColumnType extends AbstractColumnType {
 
@@ -29,7 +29,7 @@ public class ShortColumnType extends AbstractColumnType {
     }
 
     @Override
-    public ShortParser customParser(CsvReadOptions options) {
+    public ShortParser customParser(ReadOptions options) {
         return new ShortParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java
index 1a30e4d4..0e5572bd 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java
@@ -2,7 +2,7 @@ package tech.tablesaw.columns.numbers;
 
 import com.google.common.collect.Lists;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class ShortParser extends AbstractParser<Short> {
 
@@ -10,7 +10,7 @@ public class ShortParser extends AbstractParser<Short> {
         super(columnType);
     }
 
-    public ShortParser(ShortColumnType columnType, CsvReadOptions readOptions) {
+    public ShortParser(ShortColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
index b67d8210..f9928cd4 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.strings;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.StringColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class StringColumnType extends AbstractColumnType {
 
@@ -31,7 +31,7 @@ public class StringColumnType extends AbstractColumnType {
     }
 
     @Override
-    public StringParser customParser(CsvReadOptions options) {
+    public StringParser customParser(ReadOptions options) {
         return new StringParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java b/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java
index 274a0f94..0abd62de 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.strings;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class StringParser extends AbstractParser<String> {
 
@@ -11,7 +11,7 @@ public class StringParser extends AbstractParser<String> {
         super(columnType);
     }
 
-    public StringParser(ColumnType columnType, CsvReadOptions readOptions) {
+    public StringParser(ColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java b/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java
index cb7f89b1..3b490aad 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.strings;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.TextColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class TextColumnType extends AbstractColumnType {
 
@@ -29,7 +29,7 @@ public class TextColumnType extends AbstractColumnType {
     }
 
     @Override
-    public StringParser customParser(CsvReadOptions options) {
+    public StringParser customParser(ReadOptions options) {
         return new StringParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
index 48b47f14..ef3dd32d 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
@@ -4,7 +4,7 @@ import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.TimeColumn;
 import tech.tablesaw.columns.AbstractColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalTime;
 
@@ -33,7 +33,7 @@ public class TimeColumnType extends AbstractColumnType {
     }
 
     @Override
-    public AbstractParser<LocalTime> customParser(CsvReadOptions options) {
+    public AbstractParser<LocalTime> customParser(ReadOptions options) {
         return new TimeParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java b/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java
index 4169d6c3..f8272102 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java
@@ -4,7 +4,7 @@ import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
@@ -63,7 +63,7 @@ public class TimeParser extends AbstractParser<LocalTime> {
         super(columnType);
     }
 
-    public TimeParser(ColumnType columnType, CsvReadOptions readOptions) {
+    public TimeParser(ColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         DateTimeFormatter readCsvFormatter = readOptions.timeFormatter();
         if (readCsvFormatter != null) {
diff --git a/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
new file mode 100644
index 00000000..9aa376b2
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
@@ -0,0 +1,210 @@
+package tech.tablesaw.io;
+
+import static tech.tablesaw.api.ColumnType.BOOLEAN;
+import static tech.tablesaw.api.ColumnType.DOUBLE;
+import static tech.tablesaw.api.ColumnType.FLOAT;
+import static tech.tablesaw.api.ColumnType.INTEGER;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE_TIME;
+import static tech.tablesaw.api.ColumnType.LOCAL_TIME;
+import static tech.tablesaw.api.ColumnType.LONG;
+import static tech.tablesaw.api.ColumnType.SHORT;
+import static tech.tablesaw.api.ColumnType.STRING;
+import static tech.tablesaw.api.ColumnType.TEXT;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.google.common.collect.Lists;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.AbstractParser;
+import tech.tablesaw.columns.strings.StringColumnType;
+
+public class ColumnTypeDetector {
+
+    /**
+     * Consider using TextColumn instead of StringColumn for string data after this many rows
+     */
+    private static final int STRING_COLUMN_ROW_COUNT_CUTOFF = 50_000;
+
+    /**
+     * Use a TextColumn if at least this proportion of values are found to be unique in the type detection sample
+     *
+     * Note: This number is based on an assumption that as more records are considered, a smaller proportion of these
+     * new records will be found to be unique
+     *
+     * Sample calculation;
+     * 10 character string = 2 bytes * 10 + 38 extra bytes = 58; rounded up to 64 so it's a multiple of 8
+     *
+     * With dictionary encoding, we have 2*64 + 2*4 = 136 byte per unique value plus 4 bytes for each value
+     * For text columns we have 64 bytes per string
+     *
+     * So, if every value is unique, using dictionary encoding wastes about 70 bytes per value.
+     * If there are only two unique values, dictionary encoding saves about 62 bytes per value.
+     *
+     * Of course, it all depends on the lengths of the strings.
+     */
+    private static final double STRING_COLUMN_CUTOFF = 0.50;
+
+    /**
+     * Types to choose from. When more than one would work, we pick the first of the options. The order these appear in
+     * is critical. The broadest must go last, which is why String is at the end of the list. Any String read from
+     * the input will match string. If it were first on the list, you would get nothing but strings in your table.
+     *
+     * As another example, an integer type, should go before double. Otherwise double would match integers so
+     * the integer test would never be evaluated and all the ints would be read as doubles.
+     */
+    private List<ColumnType> typeArray =
+            Lists.newArrayList(
+                    LOCAL_DATE_TIME,
+                    LOCAL_TIME,
+                    LOCAL_DATE,
+                    BOOLEAN,
+                    SHORT,
+                    INTEGER,
+                    LONG,
+                    FLOAT,
+                    DOUBLE,
+                    STRING,
+                    TEXT);    
+
+    public ColumnTypeDetector() {}
+    
+    public ColumnTypeDetector(List<ColumnType> typeArrayOverrides) {
+	typeArray = typeArrayOverrides;
+    }
+    
+    /**
+     * Estimates and returns the type for each column in the input text
+     * <p>
+     * The type is determined by checking a sample of the data. Because only a sample of the data is
+     * checked, the types may be incorrect. If that is the case a Parse Exception will be thrown.
+     * <p>
+     * The method {@code printColumnTypes()} can be used to print a list of the detected columns that can be
+     * corrected and used to explicitly specify the correct column types.
+     */
+    public ColumnType[] detectColumnTypes(Iterator<String[]> rows, ReadOptions options) {
+        boolean useSampling = options.sample();
+
+        // to hold the results
+        List<ColumnType> columnTypes = new ArrayList<>();
+
+        // to hold the data read from the file
+        List<List<String>> columnData = new ArrayList<>();
+
+        int rowCount = 0; // make sure we don't go over maxRows
+
+        int nextRow = 0;
+        while (rows.hasNext()) {
+            String[] nextLine = rows.next();
+            // initialize the arrays to hold the strings. we don't know how many we need until we read the first row
+            if (rowCount == 0) {
+                for (int i = 0; i < nextLine.length; i++) {
+                    columnData.add(new ArrayList<>());
+                }
+            }
+            int columnNumber = 0;
+            if (rowCount == nextRow) {
+                for (String field : nextLine) {
+                    columnData.get(columnNumber).add(field);
+                    columnNumber++;
+                }
+                if (useSampling) {
+                    nextRow = nextRow(nextRow);
+                } else {
+                    nextRow = nextRowWithoutSampling(nextRow);
+                }
+            }
+            rowCount++;
+        }
+
+        // now detect
+        for (List<String> valuesList : columnData) {
+            ColumnType detectedType = detectType(valuesList, options);
+            if (detectedType.equals(StringColumnType.STRING) && rowCount > STRING_COLUMN_ROW_COUNT_CUTOFF) {
+                HashSet<String> unique = new HashSet<>(valuesList);
+                double uniquePct = unique.size() / (valuesList.size() * 1.0);
+                if (uniquePct > STRING_COLUMN_CUTOFF) {
+                    detectedType = TEXT;
+                }
+            }
+            columnTypes.add(detectedType);
+        }
+        return columnTypes.toArray(new ColumnType[0]);
+    }
+
+    private int nextRowWithoutSampling(int nextRow) {
+        return nextRow + 1;
+    }
+
+    private int nextRow(int nextRow) {
+        if (nextRow < 10_000) {
+            return nextRow + 1;
+        }
+        if (nextRow < 100_000) {
+            return nextRow + 1000;
+        }
+        if (nextRow < 1_000_000) {
+            return nextRow + 10_000;
+        }
+        if (nextRow < 10_000_000) {
+            return nextRow + 100_000;
+        }
+        if (nextRow < 100_000_000) {
+            return nextRow + 1_000_000;
+        }
+        return nextRow + 10_000_000;
+    }    
+
+    /**
+     * Returns a predicted ColumnType derived by analyzing the given list of undifferentiated strings read from a
+     * column in the file and applying the given Locale and options
+     */
+    private ColumnType detectType(List<String> valuesList, ReadOptions options) {
+
+        CopyOnWriteArrayList<AbstractParser<?>> parsers = new CopyOnWriteArrayList<>(getParserList(typeArray, options));
+
+        CopyOnWriteArrayList<ColumnType> typeCandidates = new CopyOnWriteArrayList<>(typeArray);
+
+        for (String s : valuesList) {
+            for (AbstractParser<?> parser : parsers) {
+                if (!parser.canParse(s)) {
+                    typeCandidates.remove(parser.columnType());
+                    parsers.remove(parser);
+                }
+            }
+        }
+        return selectType(typeCandidates);
+    }
+
+    /**
+     * Returns the selected candidate for a column of data, by picking the first value in the given list
+     *
+     * @param typeCandidates a possibly empty list of candidates. This list should be sorted in order of preference
+     */
+    private ColumnType selectType(List<ColumnType> typeCandidates) {
+        return typeCandidates.get(0);
+    }
+
+    /**
+     * Returns the list of parsers to use for type detection
+     *
+     * @param typeArray Array of column types. The order specifies the order the types are applied
+     * @param options CsvReadOptions to use to modify the default parsers for each type
+     * @return  A list of parsers in the order they should be used for type detection
+     */
+    private List<AbstractParser<?>> getParserList(List<ColumnType> typeArray, ReadOptions options) {
+        // Types to choose from. When more than one would work, we pick the first of the options
+
+        List<AbstractParser<?>> parsers = new ArrayList<>();
+        for (ColumnType type : typeArray) {
+            parsers.add(type.customParser(options));
+        }
+        return parsers;
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/DataFrameReader.java b/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
index 56bb003b..b2f2ed01 100644
--- a/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
+++ b/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
@@ -14,19 +14,24 @@
 
 package tech.tablesaw.io;
 
-import tech.tablesaw.api.Table;
-import tech.tablesaw.io.csv.CsvReadOptions;
-import tech.tablesaw.io.csv.CsvReader;
-import tech.tablesaw.io.html.HtmlTableReader;
-import tech.tablesaw.io.jdbc.SqlResultSetReader;
-
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.Scanner;
+
+import tech.tablesaw.api.Table;
+import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.csv.CsvReader;
+import tech.tablesaw.io.html.HtmlTableReader;
+import tech.tablesaw.io.jdbc.SqlResultSetReader;
+import tech.tablesaw.io.json.JsonReader;
 
 public class DataFrameReader {
 
@@ -62,11 +67,22 @@ public class DataFrameReader {
         return new CsvReader().read(options);
     }
 
+    public Table json(String url) throws MalformedURLException, IOException {
+        try (Scanner scanner = new Scanner(new URL(url).openStream(), StandardCharsets.UTF_8.toString())) {
+            scanner.useDelimiter("\\A");
+            return json(new StringReader(scanner.hasNext() ? scanner.next() : ""), url);
+	}
+    }
+
+    public Table json(Reader contents, String tableName) throws IOException {
+	return new JsonReader().read(ReadOptions.builder(contents, tableName).build());
+    }
+
     public Table db(ResultSet resultSet, String tableName) throws SQLException {
         return SqlResultSetReader.read(resultSet, tableName);
     }
 
     public Table html(String url) throws IOException {
-        return csv(new HtmlTableReader().tableToCsv(url), url);
+        return new HtmlTableReader().read(url);
     }
 }
diff --git a/core/src/main/java/tech/tablesaw/io/ReadOptions.java b/core/src/main/java/tech/tablesaw/io/ReadOptions.java
new file mode 100644
index 00000000..b3abb94f
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/ReadOptions.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package tech.tablesaw.io;
+
+import com.google.common.base.Strings;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+public class ReadOptions {
+
+    // we always have one of these (file, reader, or inputStream)
+    protected final File file;
+    protected final Reader reader;
+    protected final InputStream inputStream;
+
+    protected final String tableName;
+    protected final boolean sample;
+    protected final String dateFormat;
+    protected final String dateTimeFormat;
+    protected final String timeFormat;
+    protected final Locale locale;
+    protected final String missingValueIndicator;
+
+    protected ReadOptions(ReadOptions.Builder builder) {
+
+        int sourceCount = 0;
+        if (builder.file != null) sourceCount++;
+        if (builder.reader != null) sourceCount++;
+        if (builder.inputStream != null) sourceCount++;
+
+        if (sourceCount == 0) {
+            throw new IllegalArgumentException("CsvReadOptions Builder configured with no data source");
+        } else if (sourceCount > 1) {
+            throw new IllegalArgumentException("CsvReadOptions Builder configured with more than one data source");
+        }
+
+        file = builder.file;
+        reader = builder.reader;
+        inputStream = builder.inputStream;
+        tableName = builder.tableName;
+        sample = builder.sample;
+        dateFormat = builder.dateFormat;
+        timeFormat = builder.timeFormat;
+        dateTimeFormat = builder.dateTimeFormat;
+        missingValueIndicator = builder.missingValueIndicator;
+
+        if (builder.locale == null) {
+            locale = Locale.getDefault();
+        } else {
+            locale = builder.locale;
+        }
+    }
+
+    public static Builder builder(File file) {
+        return new Builder(file).tableName(file.getName());
+    }
+
+    public static Builder builder(String fileName) {
+        return new Builder(new File(fileName));
+    }
+
+    /**
+     * This method may cause tablesaw to buffer the entire InputStream.
+     * <p>
+     * If you have a large amount of data, you can do one of the following:
+     * 1. Use the method taking a File instead of a stream, or
+     * 2. Provide the array of column types as an option. If you provide the columnType array,
+     * we skip type detection and can avoid reading the entire file
+     */
+    public static Builder builder(InputStream stream, String tableName) {
+        return new Builder(stream).tableName(tableName);
+    }
+
+    /**
+     * This method may cause tablesaw to buffer the entire InputStream.
+     *
+     * <p>
+     * If you have a large amount of data, you can do one of the following:
+     * 1. Use the method taking a File instead of a reader, or
+     * 2. Provide the array of column types as an option. If you provide the columnType array,
+     * we skip type detection and can avoid reading the entire file
+     */
+    public static Builder builder(Reader reader, String tableName) {
+        Builder builder = new Builder(reader);
+        return builder.tableName(tableName);
+    }
+
+    public File file() {
+        return file;
+    }
+
+    public Reader reader() {
+        return reader;
+    }
+
+    public InputStream inputStream() {
+        return inputStream;
+    }
+
+    public String tableName() {
+        return tableName;
+    }
+
+    public boolean sample() {
+        return sample;
+    }
+
+    public String missingValueIndicator() {
+        return missingValueIndicator;
+    }
+
+    public Locale locale() {
+        return locale;
+    }
+
+    public DateTimeFormatter dateTimeFormatter() {
+        if (Strings.isNullOrEmpty(dateTimeFormat)) {
+            return null;
+        }
+        return DateTimeFormatter.ofPattern(dateTimeFormat, locale);
+    }
+
+    public DateTimeFormatter timeFormatter() {
+        if (Strings.isNullOrEmpty(timeFormat)) {
+            return null;
+        }
+        return DateTimeFormatter.ofPattern(timeFormat, locale);
+    }
+
+    public DateTimeFormatter dateFormatter() {
+        if (Strings.isNullOrEmpty(dateFormat)) {
+            return null;
+        }
+        return DateTimeFormatter.ofPattern(dateFormat, locale);
+    }
+
+    public static class Builder {
+
+        protected InputStream inputStream;
+        protected File file;
+        protected Reader reader;
+        protected String tableName = "";
+        protected boolean sample = true;
+        protected String dateFormat;
+        protected String timeFormat;
+        protected String dateTimeFormat;
+        protected Locale locale;
+        protected String missingValueIndicator;
+
+        public Builder(File file) {
+            this.file = file;
+            this.tableName = file.getName();
+        }
+
+        /**
+         * This method may cause tablesaw to buffer the entire InputStream.
+         * <p>
+         * If you have a large amount of data, you can do one of the following:
+         * 1. Use the method taking a File instead of a reader, or
+         * 2. Provide the array of column types as an option. If you provide the columnType array,
+         * we skip type detection and can avoid reading the entire file
+         */
+        public Builder(Reader reader) {
+            this.reader = reader;
+        }
+
+        /**
+         * This method may cause tablesaw to buffer the entire InputStream.
+         * <p>
+         * If you have a large amount of data, you can do one of the following:
+         * 1. Use the method taking a File instead of a stream, or
+         * 2. Provide the array of column types as an option. If you provide the columnType array,
+         * we skip type detection and can avoid reading the entire file
+         */
+        public Builder(InputStream stream) {
+            this.inputStream = stream;
+        }
+
+        public Builder tableName(String tableName) {
+            this.tableName = tableName;
+            return this;
+        }
+
+        public Builder dateFormat(String dateFormat) {
+            this.dateFormat = dateFormat;
+            return this;
+        }
+
+        public Builder timeFormat(String timeFormat) {
+            this.timeFormat = timeFormat;
+            return this;
+        }
+
+        public Builder dateTimeFormat(String dateTimeFormat) {
+            this.dateTimeFormat = dateTimeFormat;
+            return this;
+        }
+
+        public Builder missingValueIndicator(String missingValueIndicator) {
+            this.missingValueIndicator = missingValueIndicator;
+            return this;
+        }
+
+        public Builder sample(boolean sample) {
+            this.sample = sample;
+            return this;
+        }
+
+        public Builder locale(Locale locale) {
+            this.locale = locale;
+            return this;
+        }
+
+        public ReadOptions build() {
+            return new ReadOptions(this);
+        }
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java b/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
new file mode 100644
index 00000000..61f8faba
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
@@ -0,0 +1,52 @@
+package tech.tablesaw.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Iterator;
+import java.util.List;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.Table;
+
+public class TableBuildingUtils {
+
+    public static Reader createReader(ReadOptions options, byte[] cachedBytes) throws IOException {
+	if (cachedBytes != null) {
+	    return new InputStreamReader(new ByteArrayInputStream(cachedBytes));
+	}
+        if (options.inputStream() != null) {
+            return new InputStreamReader(options.inputStream());
+        }
+        if (options.reader() != null) {
+            return options.reader();
+        }
+        return new FileReader(options.file());
+    }
+
+    public static Table build(List<String> columnNames, List<String[]> dataRows, ReadOptions options) {
+        Table table = Table.create(options.tableName());
+
+        if (dataRows.size() == 0) {
+            return table;
+        }
+
+        ColumnTypeDetector detector = new ColumnTypeDetector();
+        Iterator<String[]> iterator = dataRows.iterator();
+        ColumnType[] types = detector.detectColumnTypes(iterator, options);
+        for (int i = 0; i < columnNames.size(); i++) {
+            table.addColumns(types[i].create(columnNames.get(i)));
+        }
+
+        for (int i = 0; i < dataRows.size(); i++) {
+            for (int j = 0; j < table.columnCount(); j++) {
+                table.column(j).appendCell(dataRows.get(i)[j]);        	
+            }
+        }
+
+        return table;
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java b/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
index df13c6af..74c5a64a 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
@@ -15,6 +15,7 @@
 package tech.tablesaw.io.csv;
 
 import java.io.PrintStream;
+import java.util.List;
 
 /**
  * This Exception wraps another Exception thrown while adding a cell to a column.
@@ -42,7 +43,7 @@ public class AddCellToColumnException extends RuntimeException {
     /**
      * The column names stored as an array
      */
-    private final String[] columnNames;
+    private final List<String> columnNames;
 
     /**
      * The original line that caused the Exception
@@ -58,8 +59,8 @@ public class AddCellToColumnException extends RuntimeException {
      * @param columnNames The column names stored as an array
      * @param line        The original line that caused the Exception
      */
-    public AddCellToColumnException(Exception e, int columnIndex, long rowNumber, String[] columnNames, String[] line) {
-        super("Error while adding cell from row " + rowNumber + " and column " + columnNames[columnIndex] + "" +
+    public AddCellToColumnException(Exception e, int columnIndex, long rowNumber, List<String> columnNames, String[] line) {
+        super("Error while adding cell from row " + rowNumber + " and column " + columnNames.get(columnIndex) + "" +
                 "(position:" + columnIndex + "): " + e.getMessage(), e);
         this.columnIndex = columnIndex;
         this.rowNumber = rowNumber;
@@ -84,7 +85,7 @@ public class AddCellToColumnException extends RuntimeException {
     /**
      * Returns the column names array
      */
-    public String[] getColumnNames() {
+    public List<String> getColumnNames() {
         return columnNames;
     }
 
@@ -92,7 +93,7 @@ public class AddCellToColumnException extends RuntimeException {
      * Returns the name of the column that caused the Exception
      */
     public String getColumnName() {
-        return columnNames[columnIndex];
+        return columnNames.get(columnIndex);
     }
 
     /**
@@ -101,11 +102,11 @@ public class AddCellToColumnException extends RuntimeException {
      * @param out The PrintStream to output to
      */
     public void dumpRow(PrintStream out) {
-        for (int i = 0; i < columnNames.length; i++) {
+        for (int i = 0; i < columnNames.size(); i++) {
             out.print("Column ");
             out.print(i);
             out.print(" ");
-            out.print(columnNames[i]);
+            out.print(columnNames.get(columnIndex));
             out.print(" : ");
             try {
                 out.println(line[i]);
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
index feda60a8..5f1cf616 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
@@ -16,6 +16,7 @@ package tech.tablesaw.io.csv;
 
 import com.google.common.base.Strings;
 import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.io.ReadOptions;
 
 import java.io.File;
 import java.io.InputStream;
@@ -23,59 +24,22 @@ import java.io.Reader;
 import java.time.format.DateTimeFormatter;
 import java.util.Locale;
 
-public class CsvReadOptions {
+public class CsvReadOptions extends ReadOptions {
 
-    // we always have one of these (file, reader, or inputStream)
-    private final File file;
-    private final Reader reader;
-    private final InputStream inputStream;
-
-    private final String tableName;
     private final ColumnType[] columnTypes;
     private final boolean header;
     private final Character separator;
     private final String lineEnding;
-    private final boolean sample;
-    private final String dateFormat;
-    private final String dateTimeFormat;
-    private final String timeFormat;
-    private final Locale locale;
-    private final String missingValueIndicator;
     private final Integer maxNumberOfColumns;
 
     private CsvReadOptions(CsvReadOptions.Builder builder) {
+	super(builder);
 
-        int sourceCount = 0;
-        if (builder.file != null) sourceCount++;
-        if (builder.reader != null) sourceCount++;
-        if (builder.inputStream != null) sourceCount++;
-
-        if (sourceCount == 0) {
-            throw new IllegalArgumentException("CsvReadOptions Builder configured with no data source");
-        } else if (sourceCount > 1) {
-            throw new IllegalArgumentException("CsvReadOptions Builder configured with more than one data source");
-        }
-
-        file = builder.file;
-        reader = builder.reader;
-        inputStream = builder.inputStream;
-        tableName = builder.tableName;
         columnTypes = builder.columnTypes;
         header = builder.header;
         separator = builder.separator;
-        sample = builder.sample;
-        dateFormat = builder.dateFormat;
-        timeFormat = builder.timeFormat;
-        dateTimeFormat = builder.dateTimeFormat;
         lineEnding = builder.lineEnding;
-        missingValueIndicator = builder.missingValueIndicator;
         maxNumberOfColumns = builder.maxNumberOfColumns;
-
-        if (builder.locale == null) {
-            locale = Locale.getDefault();
-        } else {
-            locale = builder.locale;
-        }
     }
 
     public static Builder builder(File file) {
@@ -181,27 +145,16 @@ public class CsvReadOptions {
         return maxNumberOfColumns;
     }
 
-    public static class Builder {
+    public static class Builder extends ReadOptions.Builder {
 
-        private InputStream inputStream;
-        private File file;
-        private Reader reader;
-        private String tableName = "";
         private boolean header = true;
         private Character separator = ',';
         private String lineEnding;
-        private boolean sample = true;
         private ColumnType[] columnTypes;
-        private String dateFormat;
-        private String timeFormat;
-        private String dateTimeFormat;
-        private Locale locale;
-        private String missingValueIndicator;
         private Integer maxNumberOfColumns = 10_000;
 
         public Builder(File file) {
-            this.file = file;
-            this.tableName = file.getName();
+            super(file);
         }
 
         /**
@@ -213,7 +166,7 @@ public class CsvReadOptions {
          * we skip type detection and can avoid reading the entire file
          */
         public Builder(Reader reader) {
-            this.reader = reader;
+            super(reader);
         }
 
         /**
@@ -225,7 +178,7 @@ public class CsvReadOptions {
          * we skip type detection and can avoid reading the entire file
          */
         public Builder(InputStream stream) {
-            this.inputStream = stream;
+            super(stream);
         }
 
         public Builder tableName(String tableName) {
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
index c33b5f4d..ac7ec8f2 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -16,7 +16,6 @@ package tech.tablesaw.io.csv;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.common.io.ByteStreams;
 import com.google.common.io.CharStreams;
 import com.univocity.parsers.csv.CsvFormat;
 import com.univocity.parsers.csv.CsvParser;
@@ -25,76 +24,27 @@ import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.columns.AbstractParser;
 import tech.tablesaw.columns.Column;
-import tech.tablesaw.columns.strings.StringColumnType;
+import tech.tablesaw.io.ColumnTypeDetector;
+import tech.tablesaw.io.TableBuildingUtils;
 
 import javax.annotation.concurrent.Immutable;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+
+import org.apache.commons.lang3.tuple.Pair;
+
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 import static tech.tablesaw.api.ColumnType.*;
 
 @Immutable
 public class CsvReader {
 
-    /**
-     * Consider using TextColumn instead of StringColumn for string data after this many rows
-     */
-    private static final int STRING_COLUMN_ROW_COUNT_CUTOFF = 50_000;
-
-    /**
-     * Use a TextColumn if at least this proportion of values are found to be unique in the type detection sample
-     *
-     * Note: This number is based on an assumption that as more records are considered, a smaller proportion of these
-     * new records will be found to be unique
-     *
-     * Sample calculation;
-     * 10 character string = 2 bytes * 10 + 38 extra bytes = 58; rounded up to 64 so it's a multiple of 8
-     *
-     * With dictionary encoding, we have 2*64 + 2*4 = 136 byte per unique value plus 4 bytes for each value
-     * For text columns we have 64 bytes per string
-     *
-     * So, if every value is unique, using dictionary encoding wastes about 70 bytes per value.
-     * If there are only two unique values, dictionary encoding saves about 62 bytes per value.
-     *
-     * Of course, it all depends on the lengths of the strings.
-     */
-    private static final double STRING_COLUMN_CUTOFF = 0.50;
-
-    /**
-     * Types to choose from. When more than one would work, we pick the first of the options. The order these appear in
-     * is critical. The broadest must go last, which is why String is at the end of the list. Any String read from
-     * a CSV will match string. If it were first on the list, you would get nothing but strings in your table.
-     *
-     * As another example, an integer type, should go before double. Otherwise double would match integers so
-     * the integer test would never be evaluated and all the ints would be read as doubles.
-     */
-    private List<ColumnType> typeArray =
-            Lists.newArrayList(
-                    LOCAL_DATE_TIME,
-                    LOCAL_TIME,
-                    LOCAL_DATE,
-                    BOOLEAN,
-                    SHORT,
-                    INTEGER,
-                    LONG,
-                    FLOAT,
-                    DOUBLE,
-                    STRING,
-                    TEXT);
+    private List<ColumnType> typeArrayOverrides = null;
 
     /**
      * Constructs a CsvReader
@@ -107,43 +57,50 @@ public class CsvReader {
      * These are the only types that the CsvReader can detect and parse
      */
     public CsvReader(List<ColumnType> typeDetectionList) {
-        this.typeArray = typeDetectionList;
+        this.typeArrayOverrides = typeDetectionList;
     }
 
-    public Table read(CsvReadOptions options) throws IOException {
-
+    /**
+     * Determines column types if not provided by the user
+     * Reads all input into memory unless File was provided
+     */
+    private Pair<Reader, ColumnType[]> getReaderAndColumnTypes(CsvReadOptions options) throws IOException {
         ColumnType[] types = options.columnTypes();
-        byte[] bytes = null;
+        byte[] bytesCache = null;
 
         if (types == null) {
-            if (options.reader() != null) {
-                bytes = CharStreams.toString(options.reader()).getBytes();
+            Reader reader = TableBuildingUtils.createReader(options, bytesCache);
+            if (options.file() == null) {
+        	bytesCache = CharStreams.toString(reader).getBytes();
+        	// create a new reader since we just exhausted the existing one
+        	reader = TableBuildingUtils.createReader(options, bytesCache);
             }
-            if (options.inputStream() != null) {
-                bytes = ByteStreams.toByteArray(options.inputStream());
-            }
-            types = getColumnTypes(options, bytes);
+            types = detectColumnTypes(reader, options);
         }
 
-        Reader reader = getReader(options, bytes);
+        return Pair.of(TableBuildingUtils.createReader(options, bytesCache), types);
+    }
+
+    public Table read(CsvReadOptions options) throws IOException {
+        return read(options, false);
+    }
+
+    private Table read(CsvReadOptions options, boolean headerOnly) throws IOException {
+	Pair<Reader, ColumnType[]> pair = getReaderAndColumnTypes(options);
+	Reader reader = pair.getLeft();
+	ColumnType[] types = pair.getRight();
+
         CsvParser parser = csvParser(options);
 
         try {
             parser.beginParsing(reader);
             Table table = Table.create(options.tableName());
 
-            String[] headerNames = getHeaderNames(options, types, parser);
-            if (headerNames == null) {
-                return table;
-            }
-            List<String> headerRow = Lists.newArrayList(headerNames);
-
-            String[] columnNames = selectColumnNames(headerRow, types);
+            List<String> headerRow = Lists.newArrayList(getHeaderNames(options, types, parser));
 
-            cleanNames(headerRow);
             for (int x = 0; x < types.length; x++) {
                 if (types[x] != SKIP) {
-                    String columnName = headerRow.get(x);
+                    String columnName = cleanName(headerRow.get(x));
                     if (Strings.isNullOrEmpty(columnName)) {
                         columnName = "Column " + table.columnCount();
                     }
@@ -151,13 +108,17 @@ public class CsvReader {
                     table.addColumns(newColumn);
                 }
             }
-            int[] columnIndexes = new int[columnNames.length];
-            for (int i = 0; i < columnIndexes.length; i++) {
-                // get the index in the original table, which includes skipped fields
-                columnIndexes[i] = headerRow.indexOf(columnNames[i]);
+
+            if (!headerOnly) {
+                String[] columnNames = selectColumnNames(headerRow, types);
+                int[] columnIndexes = new int[columnNames.length];
+                for (int i = 0; i < columnIndexes.length; i++) {
+                    // get the index in the original table, which includes skipped fields
+                    columnIndexes[i] = headerRow.indexOf(columnNames[i]);
+                }
+                addRows(options, types, parser, table, columnIndexes);
             }
 
-            addRows(options, types, parser, table, columnNames, columnIndexes);
             return table;
         } finally {
             if (options.reader() == null) {
@@ -170,86 +131,62 @@ public class CsvReader {
     }
 
     private String[] getHeaderNames(CsvReadOptions options, ColumnType[] types, CsvParser parser) {
-        String[] headerNames;
         if (options.header()) {
-            headerNames = parser.parseNext();
+            String[] headerNames = parser.parseNext();
             // work around issue where Univocity returns null if a column has no header.
             for (int i = 0; i < headerNames.length; i++) {
                 if (headerNames[i] == null) {
                     headerNames[i] = "C" + i;
                 }
             }
+            return headerNames;
         } else {
-            headerNames = makeColumnNames(types);
-        }
-        return headerNames;
-    }
-
-    private Reader getReader(CsvReadOptions options, byte[] bytes)
-            throws FileNotFoundException {
-
-        if (bytes != null) {
-            return new InputStreamReader(new ByteArrayInputStream(bytes));
-        }
-        if (options.inputStream() != null) {
-            return new InputStreamReader(options.inputStream());
-        }
-        if (options.reader() != null) {
-            return options.reader();
-        }
-        return new InputStreamReader(new FileInputStream(options.file()));
-    }
-
-    /**
-     * Returns column types for this table as an array, the types are either provided in read options, or calculated by
-     * scanning the data
-     */
-    private ColumnType[] getColumnTypes(CsvReadOptions options, byte[] bytes) throws IOException {
-        ColumnType[] types;
-        try(InputStream detectTypesStream = options.file() != null
-                ? new FileInputStream(options.file())
-                : new ByteArrayInputStream(bytes)) {
-            types = detectColumnTypes(detectTypesStream, options);
+            // Placeholder column names for when the file read has no header
+            String[] headerNames = new String[types.length];
+            for (int i = 0; i < types.length; i++) {
+        	headerNames[i] = "C" + i;
+            }
+            return headerNames;
         }
-        return types;
     }
 
-    private void addRows(CsvReadOptions options, ColumnType[] types, CsvParser reader, Table table, String[] columnNames, int[] columnIndexes) {
-        long rowNumber = options.header() ? 1L : 0L;
+    private void addRows(CsvReadOptions options, ColumnType[] types, CsvParser reader, Table table, int[] columnIndexes) {
         String[] nextLine;
 
         Map<String, AbstractParser<?>> parserMap = getParserMap(options, table);
 
         // Add the rows
-        while ((nextLine = reader.parseNext()) != null) {
-
+        for (long rowNumber = options.header() ? 1L : 0L; (nextLine = reader.parseNext()) != null; rowNumber++) {
+            // validation
             if (nextLine.length < types.length) {
                 if (nextLine.length == 1 && Strings.isNullOrEmpty(nextLine[0])) {
                     System.err.println("Warning: Invalid CSV file. Row "
                             + rowNumber
                             + " is empty. Continuing.");
+                    continue;
                 } else {
-                    Exception e = new IndexOutOfBoundsException("Row number " + rowNumber + " is too short.");
-                    throw new AddCellToColumnException(e, 0, rowNumber, columnNames, nextLine);
+                    Exception e = new IndexOutOfBoundsException("Row number " + rowNumber + " contains " + nextLine.length + " columns. "
+                    	+ types.length + " expected.");
+                    throw new AddCellToColumnException(e, 0, rowNumber, table.columnNames(), nextLine);
                 }
             } else if (nextLine.length > types.length) {
-                throw new IllegalArgumentException("Row number " + rowNumber + " is too long.");
-            } else {
-                // for each column that we're including (not skipping)
-                int cellIndex = 0;
-                for (int columnIndex : columnIndexes) {
-                    Column<?> column = table.column(cellIndex);
-                    AbstractParser<?> parser = parserMap.get(column.name());
-                    try {
-                        String value = nextLine[columnIndex];
-                        column.appendCell(value, parser);
-                    } catch (Exception e) {
-                        throw new AddCellToColumnException(e, columnIndex, rowNumber, columnNames, nextLine);
-                    }
-                    cellIndex++;
+                throw new IllegalArgumentException("Row number " + rowNumber + " contains " + nextLine.length + " columns. "
+                	+ types.length + " expected.");
+            }
+
+            // append each column that we're including (not skipping)
+            int cellIndex = 0;
+            for (int columnIndex : columnIndexes) {
+                Column<?> column = table.column(cellIndex);
+                AbstractParser<?> parser = parserMap.get(column.name());
+                try {
+                    String value = nextLine[columnIndex];
+                    column.appendCell(value, parser);
+                } catch (Exception e) {
+                    throw new AddCellToColumnException(e, columnIndex, rowNumber, table.columnNames(), nextLine);
                 }
+                cellIndex++;
             }
-            rowNumber++;
         }
     }
 
@@ -262,94 +199,8 @@ public class CsvReader {
         return parserMap;
     }
 
-    private void cleanNames(List<String> headerRow) {
-        for (int i = 0; i < headerRow.size(); i++) {
-            headerRow.set(i, headerRow.get(i).trim());
-        }
-    }
-
-    /**
-     * Returns a Table constructed from a CSV File with the given file name
-     * <p>
-     * The @code{fileName} is used as the initial table name for the new table
-     *
-     * @param types           An array of the types of columns in the file, in the order they appear
-     * @param header          Is the first row in the file a header?
-     * @param options         Sets the format for and instructions for parsing
-     * @param file            The fully specified file name. It is used to provide a default name for the table
-     * @return                A Relation containing the data in the csv file.
-     * @throws IOException    if file cannot be read
-     */
-    public Table headerOnly(ColumnType[] types, boolean header, CsvReadOptions options, File file)
-            throws IOException {
-
-        FileInputStream fis = new FileInputStream(file);
-
-        Reader reader = new InputStreamReader(fis);
-
-        Table table;
-
-        CsvParser csvParser = csvParser(options);
-        try (BufferedReader streamReader = new BufferedReader(reader)) {
-            csvParser.beginParsing(streamReader);
-
-            String[] columnNames;
-            List<String> headerRow;
-
-            String[] headerNames;
-            if (header) {
-                headerNames = csvParser.parseNext();
-                // work around issue where Univocity returns null if a column has no header.
-                for (int i = 0; i < headerNames.length; i++) {
-                    if (headerNames[i] == null) {
-                        headerNames[i] = "C" + i;
-                    }
-                }
-            } else {
-                headerNames = makeColumnNames(types);
-            }
-
-            headerRow = Lists.newArrayList(headerNames);
-            columnNames = selectColumnNames(headerRow, types);
-
-            table = Table.create(file.getName());
-            for (int x = 0; x < types.length; x++) {
-                if (types[x] != SKIP) {
-                    Column<?> newColumn = types[x].create(headerRow.get(x).trim());
-                    table.addColumns(newColumn);
-                }
-            }
-            int[] columnIndexes = new int[columnNames.length];
-            for (int i = 0; i < columnIndexes.length; i++) {
-                // get the index in the original table, which includes skipped fields
-                columnIndexes[i] = headerRow.indexOf(columnNames[i]);
-            }
-        } finally {
-            // the stream is already closed
-            csvParser.stopParsing();
-        }
-        return table;
-    }
-
-    /**
-     * Returns the structure of the table given by {@code csvFileName} as detected by analysis of a sample of the data
-     *
-     * @throws IOException if file cannot be read
-     */
-    private Table detectedColumnTypes(String csvFileName, boolean header, char delimiter, Locale locale) throws IOException {
-        File file = new File(csvFileName);
-        try (InputStream stream = new FileInputStream(file)) {
-
-            CsvReadOptions options = CsvReadOptions.builder(stream, "")
-                    .separator(delimiter)
-                    .header(header)
-                    .locale(locale)
-                    .sample(true)
-                    .build();
-            ColumnType[] types = detectColumnTypes(stream, options);
-            Table t = headerOnly(types, header, options, file);
-            return t.structure();
-        }
+    private String cleanName(String name) {
+	return name.trim();
     }
 
     /**
@@ -371,9 +222,9 @@ public class CsvReader {
      *
      * @throws IOException if file cannot be read
      */
-    public String printColumnTypes(String csvFileName, boolean header, char delimiter, Locale locale) throws IOException {
+    public String printColumnTypes(CsvReadOptions options) throws IOException {
 
-        Table structure = detectedColumnTypes(csvFileName, header, delimiter, locale);
+        Table structure = read(options, true).structure();
 
         StringBuilder buf = new StringBuilder();
         buf.append("ColumnType[] columnTypes = {");
@@ -429,17 +280,6 @@ public class CsvReader {
         return header.toArray(result);
     }
 
-    /**
-     * Provides placeholder column names for when the file read has no header
-     */
-    private String[] makeColumnNames(ColumnType[] types) {
-        String[] header = new String[types.length];
-        for (int i = 0; i < types.length; i++) {
-            header[i] = "C" + i;
-        }
-        return header;
-    }
-
     /**
      * Estimates and returns the type for each column in the delimited text file {@code file}
      * <p>
@@ -451,141 +291,43 @@ public class CsvReader {
      * corrected and
      * used to explicitly specify the correct column types.
      */
-    public ColumnType[] detectColumnTypes(InputStream stream, CsvReadOptions options) {
+    protected ColumnType[] detectColumnTypes(Reader reader, CsvReadOptions options) {
 
         boolean header = options.header();
-        boolean useSampling = options.sample();
-
         int linesToSkip = header ? 1 : 0;
 
-        // to hold the results
-        List<ColumnType> columnTypes = new ArrayList<>();
-
-        // to hold the data read from the file
-        List<List<String>> columnData = new ArrayList<>();
-
-        int rowCount = 0; // make sure we don't go over maxRows
-
         CsvParser csvParser = csvParser(options);
 
         try {
-            csvParser.beginParsing(new InputStreamReader(stream));
+            csvParser.beginParsing(reader);
 
             for (int i = 0; i < linesToSkip; i++) {
                 csvParser.parseNext();
             }
 
-            String[] nextLine;
-            int nextRow = 0;
-            while ((nextLine = csvParser.parseNext()) != null) {
-                // initialize the arrays to hold the strings. we don't know how many we need until we read the first row
-                if (rowCount == 0) {
-                    for (int i = 0; i < nextLine.length; i++) {
-                        columnData.add(new ArrayList<>());
-                    }
-                }
-                int columnNumber = 0;
-                if (rowCount == nextRow) {
-                    for (String field : nextLine) {
-                        columnData.get(columnNumber).add(field);
-                        columnNumber++;
-                    }
-                    if (useSampling) {
-                        nextRow = nextRow(nextRow);
-                    } else {
-                        nextRow = nextRowWithoutSampling(nextRow);
-                    }
-                }
-                rowCount++;
-            }
+            ColumnTypeDetector detector = typeArrayOverrides == null
+        	    ? new ColumnTypeDetector() : new ColumnTypeDetector(typeArrayOverrides);
+            return detector.detectColumnTypes(new Iterator<String[]>() {
+
+        	String[] nextRow = csvParser.parseNext();
+
+		@Override
+		public boolean hasNext() {
+		    return nextRow != null;
+		}
+
+		@Override
+		public String[] next() {
+		    String[] tmp = nextRow;
+		    nextRow = csvParser.parseNext();
+		    return tmp;
+		}
+        	
+            }, options);
         } finally {
             csvParser.stopParsing();
             // we don't close the reader since we didn't create it
         }
-
-        // now detect
-        for (List<String> valuesList : columnData) {
-            ColumnType detectedType = detectType(valuesList, options);
-            if (detectedType.equals(StringColumnType.STRING) && rowCount > STRING_COLUMN_ROW_COUNT_CUTOFF) {
-                HashSet<String> unique = new HashSet<>(valuesList);
-                double uniquePct = unique.size() / (valuesList.size() * 1.0);
-                if (uniquePct > STRING_COLUMN_CUTOFF) {
-                    detectedType = TEXT;
-                }
-            }
-            columnTypes.add(detectedType);
-        }
-        return columnTypes.toArray(new ColumnType[0]);
-    }
-
-    private int nextRowWithoutSampling(int nextRow) {
-        return nextRow + 1;
-    }
-
-    private int nextRow(int nextRow) {
-        if (nextRow < 10_000) {
-            return nextRow + 1;
-        }
-        if (nextRow < 100_000) {
-            return nextRow + 1000;
-        }
-        if (nextRow < 1_000_000) {
-            return nextRow + 10_000;
-        }
-        if (nextRow < 10_000_000) {
-            return nextRow + 100_000;
-        }
-        if (nextRow < 100_000_000) {
-            return nextRow + 1_000_000;
-        }
-        return nextRow + 10_000_000;
-    }
-
-    /**
-     * Returns a predicted ColumnType derived by analyzing the given list of undifferentiated strings read from a
-     * column in the file and applying the given Locale and options
-     */
-    private ColumnType detectType(List<String> valuesList, CsvReadOptions options) {
-
-        CopyOnWriteArrayList<AbstractParser<?>> parsers = new CopyOnWriteArrayList<>(getParserList(typeArray, options));
-
-        CopyOnWriteArrayList<ColumnType> typeCandidates = new CopyOnWriteArrayList<>(typeArray);
-
-        for (String s : valuesList) {
-            for (AbstractParser<?> parser : parsers) {
-                if (!parser.canParse(s)) {
-                    typeCandidates.remove(parser.columnType());
-                    parsers.remove(parser);
-                }
-            }
-        }
-        return selectType(typeCandidates);
-    }
-
-    /**
-     * Returns the selected candidate for a column of data, by picking the first value in the given list
-     *
-     * @param typeCandidates a possibly empty list of candidates. This list should be sorted in order of preference
-     */
-    private ColumnType selectType(List<ColumnType> typeCandidates) {
-        return typeCandidates.get(0);
-    }
-
-    /**
-     * Returns the list of parsers to use for type detection
-     *
-     * @param typeArray Array of column types. The order specifies the order the types are applied
-     * @param options CsvReadOptions to use to modify the default parsers for each type
-     * @return  A list of parsers in the order they should be used for type detection
-     */
-    private List<AbstractParser<?>> getParserList(List<ColumnType> typeArray, CsvReadOptions options) {
-        // Types to choose from. When more than one would work, we pick the first of the options
-
-        List<AbstractParser<?>> parsers = new ArrayList<>();
-        for (ColumnType type : typeArray) {
-            parsers.add(type.customParser(options));
-        }
-        return parsers;
     }
 
     private CsvParser csvParser(CsvReadOptions options) {
@@ -608,10 +350,4 @@ public class CsvReader {
         return format;
     }
 
-    /**
-     * Returns the list of types that specifies the order in which types are tested in the detection algorithm.
-     */
-    public List<ColumnType> getTypeArray() {
-        return typeArray;
-    }
 }
diff --git a/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java b/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
index 9f1e9962..63b592a6 100644
--- a/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
+++ b/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
@@ -1,19 +1,23 @@
 package tech.tablesaw.io.html;
 
-import com.univocity.parsers.csv.CsvWriter;
-import com.univocity.parsers.csv.CsvWriterSettings;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.io.ReadOptions;
+import tech.tablesaw.io.TableBuildingUtils;
+
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
 
 import java.io.IOException;
-import java.io.StringWriter;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.stream.Stream;
 
 public class HtmlTableReader {
 
-    public String tableToCsv(String url) throws IOException {
+    public Table read(String url) throws IOException { // TODO: take ReaderOptions. Add a test using a File
         Document doc = Jsoup.connect(url).get();
         Elements tables = doc.select("table");
         if (tables.size() != 1) {
@@ -22,18 +26,31 @@ public class HtmlTableReader {
                             + " The URL you passed has " + tables.size()
                             + ". You may file a feature request with the URL if you'd like your pagae to be supported");
         }
-        Element table = tables.get(0);
-        CsvWriterSettings settings = new CsvWriterSettings();
-        StringWriter stringWriter = new StringWriter();
-        CsvWriter csvWriter = new CsvWriter(stringWriter, settings);
+        Element htmlTable = tables.get(0);
 
-        for (Element row : table.select("tr")) {
+        List<String[]> rows = new ArrayList<>();
+        for (Element row : htmlTable.select("tr")) {
             Elements headerCells = row.getElementsByTag("th");
             Elements cells = row.getElementsByTag("td");
             String[] nextLine = Stream.concat(headerCells.stream(), cells.stream())
                     .map(Element::text).toArray(String[]::new);
-            csvWriter.writeRow(nextLine);
+            rows.add(nextLine);
+        }
+
+        Table table = Table.create(url);
+
+        if (rows.size() == 0) {
+            return table;
         }
-        return stringWriter.toString();
+
+        ReadOptions options = ReadOptions.builder(new StringReader(""), url).build(); // TODO: this should be passed in
+        String[] headerRow = rows.get(0);
+        List<String> columnNames = new ArrayList<>();
+        for (int i = 0; i < headerRow.length; i++) {
+            columnNames.add(headerRow[i]); // TODO: cleansing and fallback name
+        }
+
+        return TableBuildingUtils.build(columnNames, rows, options);
     }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/io/json/JsonReader.java b/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
new file mode 100644
index 00000000..dc7f594d
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
@@ -0,0 +1,98 @@
+package tech.tablesaw.io.json;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.wnameless.json.flattener.JsonFlattener;
+
+import tech.tablesaw.api.Table;
+import tech.tablesaw.io.ReadOptions;
+import tech.tablesaw.io.TableBuildingUtils;
+
+public class JsonReader {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    public Table read(ReadOptions options) throws IOException {
+        JsonNode jsonObj = mapper.readTree(TableBuildingUtils.createReader(options, null));
+        if (!jsonObj.isArray()) {
+            throw new IllegalStateException(
+                    "Only reading a json array or arrays or objects is currently supported");
+        }
+        if (jsonObj.size() == 0) {
+            return Table.create(options.tableName());
+        }
+
+        JsonNode firstNode = jsonObj.get(0);
+        if (firstNode.isArray()) {
+            return convertArrayOfArrays(jsonObj, options);
+        }
+        return convertArrayOfObjects(jsonObj, options);
+    }
+
+    private Table convertArrayOfArrays(JsonNode jsonObj, ReadOptions options) {
+        JsonNode firstNode = jsonObj.get(0);
+	boolean firstRowAllStrings = true;
+        List<String> columnNames = new ArrayList<>();
+        for (JsonNode n : firstNode) {
+    	if (!n.isTextual()) {
+    	    firstRowAllStrings = false;
+    	}
+        }
+        boolean hasHeader = firstRowAllStrings;
+        for (int i = 0; i < firstNode.size(); i++) {
+    	columnNames.add(hasHeader ? firstNode.get(i).textValue() : "Column " + i);
+        }
+        List<String[]> dataRows = new ArrayList<>();
+        for (int i = hasHeader ? 1 : 0; i < jsonObj.size(); i++) {
+    	JsonNode arr = jsonObj.get(i);
+            String[] row = new String[arr.size()];
+            for (int j = 0; j < arr.size(); j++) {
+                row[j] = arr.get(j).asText();
+            }
+            dataRows.add(row);
+        }
+        return TableBuildingUtils.build(columnNames, dataRows, options);
+    }
+
+    private Table convertArrayOfObjects(JsonNode jsonObj, ReadOptions options) throws IOException {
+        // flatten each object inside the array
+        StringBuilder result = new StringBuilder("[");
+        for (int i = 0; i < jsonObj.size(); i++) {
+            JsonNode rowObj = jsonObj.get(i);
+            String flattenedRow = JsonFlattener.flatten(mapper.writeValueAsString(rowObj));
+            if (i != 0) {
+                result.append(",");
+            }
+            result.append(flattenedRow);
+        }
+        String flattenedJsonString = result.append("]").toString();
+        JsonNode flattenedJsonObj = mapper.readTree(flattenedJsonString);
+
+        Set<String> colNames = new HashSet<>();
+        for (JsonNode row : flattenedJsonObj) {
+            Iterator<String> fieldNames = row.fieldNames();
+            while (fieldNames.hasNext()) {
+        	colNames.add(fieldNames.next());
+            }
+        }
+
+        List<String> columnNames = new ArrayList<>(colNames);
+        List<String[]> dataRows = new ArrayList<>();
+        for (JsonNode node : flattenedJsonObj) {
+            String[] arr = new String[columnNames.size()];
+            for (int i = 0; i < columnNames.size(); i++) {
+        	arr[i] = node.get(columnNames.get(i)).asText();
+            }
+            dataRows.add(arr);
+        }
+
+        return TableBuildingUtils.build(columnNames, dataRows, options);
+    }
+}
diff --git a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
index eff58deb..422ca410 100644
--- a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
+++ b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
@@ -67,11 +67,11 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         this.splitColumnNames = groupColumnNames;
     }
 
-    String[] getSplitColumnNames() {
+    protected String[] getSplitColumnNames() {
         return splitColumnNames;
     }
 
-    int getByteSize(List<Column<?>> columns) {
+    protected int getByteSize(List<Column<?>> columns) {
         int byteSize = 0;
         for (Column<?> c : columns) {
             byteSize += c.byteSize();
@@ -79,7 +79,7 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         return byteSize;
     }
 
-    void addSlice(TableSlice slice) {
+    protected void addSlice(TableSlice slice) {
         subTables.add(slice);
     }
 
@@ -211,7 +211,7 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         return tableList;
     }
 
-    void setSourceTable(Table sourceTable) {
+    protected void setSourceTable(Table sourceTable) {
         this.sourceTable = sourceTable;
     }
 }
diff --git a/docs/contributing.md b/docs/contributing.md
index dd734061..099a14dd 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -47,14 +47,10 @@ To summarize:
 ### "Help Wanted" labels
 Among the open bugs and feature requests, we've tagged some as "Help Wanted". This indicates that a fix should be relatively straightforward to address. We would, of course, LOVE help on some of the more challenging issues. Help Wanted simply means that this is a good place for a developer with limited knowlege of the library and/or limited experience with Java to jump in. 
 
-### Javascript visualisation libraries
+### Javascript visualization libraries
 Implement an interface for a Javascript plotting library (big job, we know), but the best Javascript libraries are awesome.
 Email larry@tablesaw.tech if you have any questions, or use the comments for the issue.
 
-## Working on the code in Eclipse
-
-* Install e(fx)clipse. Choose the update site that came with your installation of Eclipse. E.g. "Oxygen - http://download.eclipse.org/releases/oxygen". Then install "e(fx)clipse" under "General Purpose Tools"
-
 ## Become a Maintainer/Committer
 
 We are actively seeking new maintainers for the project. The best way to get involved is to familiarize yourself with the project through use, and make a few minor changes/fixes. After that, introduce yourself by emailing info@tablesaw.tech and we’ll get in touch.
diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md
index 15b4b0fc..147c8408 100644
--- a/docs/gettingstarted.md
+++ b/docs/gettingstarted.md
@@ -35,9 +35,9 @@ To create a column you can use one of its static *create()* methods:
 ```java
 double[] numbers = {1, 2, 3, 4};
 DoubleColumn nc = DoubleColumn.create("Test", numbers);
-out(nc.print());
+System.out.println(nc.print());
 ```
-which produces: [^1]
+which produces:
 ```java
 Column: Test
 1.0
@@ -57,8 +57,9 @@ Tablesaw makes columns easy to work with. Operations that work on numbers in sta
 
 ```java
 DoubleColumn nc2 = nc.multiply(4);
+System.out.println(nc2.print());
 ```
-producing: [^1] 
+producing:
 
 ```java
 Column: Test * 4.0
@@ -123,7 +124,7 @@ If you have several columns of the same length as you would in a table of data,
 DoubleColumn result = firstColumn.where(someOtherColumn.startsWith("foo"));
 ```
 
-> **Key point:** Note the methods *startsWith(aString)*, *isLessThan(aNumber)*, and *isOdd()*. These were predefined for your use. There are many such methods that can be used in building queries. For StringColumn, they're defined in the [tech.tablesaw.columns.strings.StringFilters interface](http://www.javadoc.io/page/tech.tablesaw/tablesaw-core/latest/tech/tablesaw/columns/strings/StringFilters.html). It also includes *endsWith()*, *isEmpty()*, *isAlpha()*, *containsString()*[^2], etc. Each column has a similar set of filter operations. They can all be found in the filter interfaces located in sub-folders of tech.tablesaw.columns (e.g. tech.tablesaw.columns.dates.DateFilters). 
+> **Key point:** Note the methods *startsWith(aString)*, *isLessThan(aNumber)*, and *isOdd()*. These were predefined for your use. There are many such methods that can be used in building queries. For StringColumn, they're defined in the [tech.tablesaw.columns.strings.StringFilters interface](http://www.javadoc.io/page/tech.tablesaw/tablesaw-core/latest/tech/tablesaw/columns/strings/StringFilters.html). It also includes *endsWith()*, *isEmpty()*, *isAlpha()*, *containsString()*[^1], etc. Each column has a similar set of filter operations. They can all be found in the filter interfaces located in sub-folders of tech.tablesaw.columns (e.g. tech.tablesaw.columns.dates.DateFilters). 
 
 #### Map functions
 
@@ -463,10 +464,4 @@ See the section on [Cross Tabs](https://jtablesaw.github.io/tablesaw/userguide/c
 
 We've covered a lot of ground. To learn more, please take a look at the [User Guide](https://jtablesaw.github.io/tablesaw/userguide/toc) or API documentation ([Java Docs](http://www.javadoc.io/page/tech.tablesaw/tablesaw-core/latest/index)).
 
-[^1]: The method shown does not actually "produce" any output For that you would call *System.out.println()*. For brevity, this "faux" output will be shown indented by one tab beneath the code that produced it.
-[^2]: Note that containsString(String subString) is different from contains(). The first method looks at each string in the column to see if it conains the substring. The second method looks at every row in the column and returns true if any matches the entire string. In other words, contains is like contains as defined on List<String>. , etc.
-
-
-
-
-
+[^1]: Note that containsString(String subString) is different from contains(). The first method looks at each string in the column to see if it conains the substring. The second method looks at every row in the column and returns true if any matches the entire string. In other words, contains is like contains as defined on List<String>. , etc.
diff --git a/docs/userguide/BarsAndPies.md b/docs/userguide/BarsAndPies.md
index 16d989d8..60c47c8b 100644
--- a/docs/userguide/BarsAndPies.md
+++ b/docs/userguide/BarsAndPies.md
@@ -14,10 +14,10 @@ Here we focus on some common plot types for working with univariate data:
 
 When you're exploring data, you need plot creation to be as easy as possible. With Tablesaw's simple plot API you can usually create and display new charts in a line or two of code. 
 
-First we load the Tornado dataset: 
+First we load the Tornado dataset:
 
 ```java
-Table tornadoes = Table.read().csv("Tornadoes.csv");
+Table tornadoes = Table.read().csv("data/tornadoes_1950-2014.csv");
 ```
 
 ### Univariate data: counts and distributions
diff --git a/docs/userguide/plotting.md b/docs/userguide/plotting.md
index 9bd64190..0332a424 100644
--- a/docs/userguide/plotting.md
+++ b/docs/userguide/plotting.md
@@ -11,7 +11,7 @@ for every US tornado between 1950 and 2014. The code below loads the data, filte
 and renders the plot:
 
 ```java
-Table tornado = Table.createFromCsv("data/tornadoes_1950-2014.csv");
+Table tornado = Table.read().csv("data/tornadoes_1950-2014.csv");
 tornado = tornado.selectWhere(
         both(column("Start Lat").isGreaterThan(0),
              column("Scale").isGreaterThanOrEqualTo(0)));
@@ -20,7 +20,7 @@ Scatter.show("US Tornados 1950-2014",
         tornado.numberColumn("Start Lon"),
         tornado.numberColumn("Start Lat"));
 ```
-These plots provide instant visual feedback to the analyst while she’s working. They’re for discovery, rather than for presentation, and so ease of use is stressed over beauty. Behind the scenes, the scatter plots are created with Tim Molter’s awesome XChart library: https://github.com/timmolter/XChart.
+These plots provide instant visual feedback to the analyst while she’s working. They’re for discovery, rather than for presentation, and so ease of use is stressed over beauty.
 
 The following chart is taken from a baseball data set. It shows how to split a table on the values of one or more columns, 
 producing a series for each group. In this case, we color the mark differently if the team made the playoffs. 
@@ -30,7 +30,7 @@ winsByYear
 Here’s the code:
 
 ```java
-    Table baseball = Table.createFromCsv("data/baseball.csv");
+    Table baseball = Table.read().csv("data/baseball.csv");
     Scatter.show("Regular season wins by year",
         baseball.numericColumn("W"),
         baseball.numericColumn("Year"),
diff --git a/jsplot/pom.xml b/jsplot/pom.xml
index ecf2a2eb..ba30b8c7 100644
--- a/jsplot/pom.xml
+++ b/jsplot/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>tech.tablesaw</groupId>
         <artifactId>tablesaw-parent</artifactId>
-        <version>0.30.3-SNAPSHOT</version>
+        <version>0.30.4-SNAPSHOT</version>
     </parent>
 
     <artifactId>tablesaw-jsplot</artifactId>
@@ -59,7 +59,7 @@
         <dependency>
             <groupId>tech.tablesaw</groupId>
             <artifactId>tablesaw-core</artifactId>
-            <version>0.30.3-SNAPSHOT</version>
+            <version>0.30.4-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>junit</groupId>
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
index f95d40f7..4c6e7ff8 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
@@ -72,7 +72,7 @@ public abstract class TraceBuilder {
         return this;
     }
 
-    static String[] columnToStringArray(Column<?> column) {
+    protected static String[] columnToStringArray(Column<?> column) {
         String[] x = new String[column.size()];
         for (int i = 0; i < column.size(); i++) {
             x[i] = column.getString(i);
diff --git a/pom.xml b/pom.xml
index 61eece17..0a390c67 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
     <groupId>tech.tablesaw</groupId>
     <artifactId>tablesaw-parent</artifactId>
-    <version>0.30.3-SNAPSHOT</version>
+    <version>0.30.4-SNAPSHOT</version>
     <name>Tablesaw</name>
     <description>High-performance Java Dataframe</description>
     <url>https://github.com/jtablesaw/tablesaw</url>


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java b/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java
index 23261966..3791d32e 100644
--- a/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java
@@ -25,7 +25,6 @@ import java.nio.ByteBuffer;
 import java.time.LocalTime;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
-import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -147,18 +146,6 @@ public class TimeColumnTest {
         assertNotEquals(LocalTime.NOON, column1.get(3));
     }
 
-    @Test
-    public void testAsSet() {
-        column1.appendCell("10:15:30");
-        column1.appendCell("11:30:00");
-        column1.appendCell("14:00:00");
-        column1.appendCell("18:15:30");
-        column1.appendInternal(of(12,0, 0));
-        column1.appendInternal(of(12,0, 0));
-        Set<LocalTime> set = column1.asSet();
-        assertEquals(column1.size(), set.size() + 1);
-    }
-
     @Test
     public void testAppendCell2() {
         column1.appendCell("12:18:03 AM");
diff --git a/core/src/test/java/tech/tablesaw/columns/ColumnTest.java b/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
index 9cc33bc7..59b54ee3 100644
--- a/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
@@ -200,7 +200,7 @@ public class ColumnTest {
         assertContentEquals(filtered, 0.0, 1.0);
     }
 
-    static String getSeason(LocalDate date) {
+    private static String getSeason(LocalDate date) {
         String season = "";
         int month = date.getMonthValue();
         int day = date.getDayOfMonth();
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 5b9ffcd0..f27e3888 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -25,12 +25,12 @@ import tech.tablesaw.api.LongColumn;
 import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.api.Table;
 
-import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileReader;
 import java.io.InputStream;
 import java.io.Reader;
+import java.io.StringReader;
 import java.nio.file.Paths;
 import java.time.ZoneOffset;
 import java.util.Collections;
@@ -124,15 +124,15 @@ public class CsvReaderTest {
 
     @Test
     public void testDataTypeDetection() throws Exception {
-        InputStream stream = new FileInputStream(new File("../data/bus_stop_test.csv"));
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        Reader reader = new FileReader("../data/bus_stop_test.csv");
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(true)
                 .separator(',')
                 .sample(false)
                 .locale(Locale.getDefault())
                 .build();
 
-        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(stream, options);
+        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(reader, options);
         assertArrayEquals(bus_types, columnTypes);
     }
 
@@ -147,27 +147,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateDetectionEnglish() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
                 "Date" + LINE_END
             + "\"Nov 1, 2017\"" + LINE_END
             + "\"Oct 1, 2017\"" + LINE_END
             + "\"Sep 1, 2017\"" + LINE_END
             + "\"Aug 1, 2017\"" + LINE_END
             + "\"Jul 1, 2017\"" + LINE_END
-            + "\"Jun 1, 2017\"" + LINE_END).getBytes());
+            + "\"Jun 1, 2017\"" + LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.ENGLISH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE))));
     }
@@ -175,27 +175,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateTimeDetectionEnglish() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
               "Date" + LINE_END
             + "09-Nov-2014 13:03" + LINE_END
             + "09-Oct-2014 13:03" + LINE_END
             + "09-Sep-2014 13:03" + LINE_END
             + "09-Aug-2014 13:03" + LINE_END
             + "09-Jul-2014 13:03" + LINE_END
-            + "09-Jun-2014 13:03" + LINE_END).getBytes());
+            + "09-Jun-2014 13:03" + LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.ENGLISH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE_TIME))));
 
@@ -204,27 +204,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateDetectionFrench() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
                 "Date" + LINE_END
             + "\"nov. 1, 2017\"" + LINE_END
             + "\"oct. 1, 2017\"" + LINE_END
             + "\"sept. 1, 2017\"" + LINE_END
             + "\"août 1, 2017\"" + LINE_END
             + "\"juil. 1, 2017\"" + LINE_END
-            + "\"juin 1, 2017\""+ LINE_END).getBytes());
+            + "\"juin 1, 2017\""+ LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.FRENCH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE))));
     }
@@ -232,27 +232,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateTimeDetectionFrench() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
               "Date" + LINE_END
             + "09-nov.-2014 13:03" + LINE_END
             + "09-oct.-2014 13:03" + LINE_END
             + "09-sept.-2014 13:03" + LINE_END
             + "09-août-2014 13:03" + LINE_END
             + "09-juil.-2014 13:03" + LINE_END
-            + "09-juin-2014 13:03" + LINE_END).getBytes());
+            + "09-juin-2014 13:03" + LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.FRENCH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE_TIME))));
     }
@@ -273,8 +273,23 @@ public class CsvReaderTest {
     }
 
     @Test
-    public void testLineEndings() throws Exception {
+    public void testWindowsAndLinuxLineEndings() throws Exception {
+        Reader reader = new StringReader(
+              "TestCol\n"
+            + "foobar1\n"
+            + "foobar2\n"
+            + "foobar3\n"
+            + "foobar4\r\n"
+            + "foobar5\r\n"
+            + "foobar6\r\n");
 
+        Table t = Table.read().csv(reader, "test table");
+        assertEquals(1, t.columnCount());
+        assertEquals(6, t.rowCount());
+    }
+
+    @Test
+    public void testCustomLineEndings() throws Exception {
         CsvReadOptions options = CsvReadOptions.builder("../data/alt_line_endings.csv")
                 .lineEnding("~")
                 .header(true)
@@ -313,20 +328,25 @@ public class CsvReaderTest {
                         "STRING,     // 2     who         " + LINE_END +
                         "}" + LINE_END;
         assertEquals(output, new CsvReader()
-                .printColumnTypes("../data/bush.csv", true, ',', Locale.getDefault()));
+                .printColumnTypes(CsvReadOptions.builder("../data/bush.csv")
+                	.header(true)
+                	.separator(',')
+                	.locale(Locale.getDefault())
+                	.sample(true)
+                	.build()));
     }
 
     @Test
     public void testDataTypeDetection2() throws Exception {
-        InputStream stream = new FileInputStream(new File("../data/bush.csv"));
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        Reader reader = new FileReader("../data/bush.csv");
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(true)
                 .separator(',')
                 .sample(false)
                 .locale(Locale.getDefault())
                 .build();
 
-        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(stream, options);
+        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(reader, options);
         assertEquals(LOCAL_DATE, columnTypes[0]);
         assertEquals(SHORT, columnTypes[1]);
         assertEquals(STRING, columnTypes[2]);
@@ -473,11 +493,4 @@ public class CsvReaderTest {
         assertEquals("1 rows X 10001 cols", table1.shape());
     }
 
-    @Test
-    public void getTypeArray() {
-        List<ColumnType> types = new CsvReader().getTypeArray();
-        for (ColumnType type : types) {
-            assertNotNull(type);
-        }
-    }
 }
diff --git a/core/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java b/core/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
new file mode 100644
index 00000000..55b2c50b
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package tech.tablesaw.io.json;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import org.junit.Test;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.Table;
+
+public class JsonReaderTest {
+
+    @Test
+    public void arrayOfArraysWithHeader() throws IOException {
+	String json = "[[\"Date\",\"Value\"],[1453438800000,-2.1448117025014],[1454043600000,-2.9763153817574],[1454648400000,-2.9545283436391]]";
+	Table table = Table.read().json(new StringReader(json), "jsonTable");
+        assertEquals(2, table.columnCount());
+        assertEquals(3, table.rowCount());
+        assertEquals("Date", table.column(0).name());
+        assertEquals("Value", table.column(1).name());
+        assertEquals(ColumnType.LONG, table.columnTypes()[0]);
+    }
+
+    @Test
+    public void arrayOfArraysNoHeader() throws IOException {
+	String json = "[[1453438800000,-2.1448117025014],[1454043600000,-2.9763153817574],[1454648400000,-2.9545283436391]]";
+	Table table = Table.read().json(new StringReader(json), "jsonTable");
+        assertEquals(2, table.columnCount());
+        assertEquals(3, table.rowCount());
+        assertEquals(ColumnType.LONG, table.columnTypes()[0]);
+    }
+
+    @Test
+    public void arrayOfNestedObjects() throws IOException {
+	String json = "[{\"a\":1453438800000,\"b\":{\"c\":-2.1448117025014}},{\"a\":1454043600000,\"b\":{\"c\":-2.9763153817574}},{\"a\":1454648400000,\"b\":{\"c\":-2.9545283436391}}]";
+	Table table = Table.read().json(new StringReader(json), "jsonTable");
+        assertEquals(2, table.columnCount());
+        assertEquals(3, table.rowCount());
+        assertEquals("a", table.column(0).name());
+        assertEquals("b.c", table.column(1).name());
+        assertEquals(ColumnType.LONG, table.columnTypes()[0]);
+    }
+
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java
index 57c6aa21..305aed51 100644
--- a/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java
+++ b/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java
@@ -17,6 +17,30 @@ import java.util.List;
 
 public class MultiPlotExample {
 
+    private static final String pageTop = "<html>" + System.lineSeparator() +
+        "<head>" + System.lineSeparator() +
+        "    <title>Multi-plot test</title>" + System.lineSeparator() +
+        "    <script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>" + System.lineSeparator() +
+        "</head>" + System.lineSeparator() +
+        "<body>" + System.lineSeparator() +
+        "<div id='plot1'>" + System.lineSeparator() +
+        "<div id='plot2'>" + System.lineSeparator();
+
+    private static final String pageBottom =
+        "</body>" + System.lineSeparator() + "</html>";
+
+    private static String makePage(Figure figure1, Figure figure2, String divName1, String divName2) {
+        return new StringBuilder()
+                .append(pageTop)
+                .append(System.lineSeparator())
+                .append(figure1.asJavascript(divName1))
+                .append(System.lineSeparator())
+                .append(figure2.asJavascript(divName2))
+                .append(System.lineSeparator())
+                .append(pageBottom)
+                .toString();
+    }
+
     public static void main(String[] args) throws Exception {
 
         /*
@@ -86,29 +110,4 @@ public class MultiPlotExample {
         new Browser().browse(outputFile);
     }
 
-    private static String makePage(Figure figure1, Figure figure2, String divName1, String divName2) {
-
-        return new StringBuilder()
-                .append(pageTop)
-                .append(System.lineSeparator())
-                .append(figure1.asJavascript(divName1))
-                .append(System.lineSeparator())
-                .append(figure2.asJavascript(divName2))
-                .append(System.lineSeparator())
-                .append(pageBottom)
-                .toString();
-    }
-
-    private static final String pageTop = "<html>" + System.lineSeparator() +
-            "<head>" + System.lineSeparator() +
-            "    <title>Multi-plot test</title>" + System.lineSeparator() +
-            "    <script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>" + System.lineSeparator() +
-            "</head>" + System.lineSeparator() +
-            "<body>" + System.lineSeparator() +
-            "<div id='plot1'>" + System.lineSeparator() +
-            "<div id='plot2'>" + System.lineSeparator();
-
-    private static final String pageBottom =
-            "</body>" + System.lineSeparator() +
-                    "</html>";
 }