Repository: jtablesaw/tablesaw
Pull request #322 (2018-07-31 20:29:05)

Issue 310
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

What was changed
More work on making column type creation accessible to library clients.

## Testing

Did you add a unit test?
No, but all the existing tests pass. 



Patch:
diff --git a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
index c67b614b..09e81248 100644
--- a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
@@ -14,7 +14,6 @@
 
 package tech.tablesaw.api;
 
-import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.booleans.BooleanIterable;
 import it.unimi.dsi.fastutil.booleans.BooleanIterator;
 import it.unimi.dsi.fastutil.booleans.BooleanOpenHashSet;
@@ -31,12 +30,13 @@ import it.unimi.dsi.fastutil.bytes.ByteSet;
 import it.unimi.dsi.fastutil.ints.IntComparator;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.columns.booleans.BooleanColumnType;
 import tech.tablesaw.columns.booleans.BooleanColumnUtils;
 import tech.tablesaw.columns.booleans.BooleanFillers;
 import tech.tablesaw.columns.booleans.BooleanFormatter;
 import tech.tablesaw.columns.booleans.BooleanMapUtils;
 import tech.tablesaw.filtering.predicates.BytePredicate;
-import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.selection.BitmapBackedSelection;
 import tech.tablesaw.selection.Selection;
 
@@ -59,8 +59,8 @@ public class BooleanColumn extends AbstractColumn implements BooleanMapUtils, Ca
 
     public static final byte MISSING_VALUE = (Byte) BOOLEAN.getMissingValue();
 
-    private static final byte BYTE_TRUE = 1;
-    private static final byte BYTE_FALSE = 0;
+    public static final byte BYTE_TRUE = 1;
+    public static final byte BYTE_FALSE = 0;
 
     private final ByteComparator descendingByteComparator = (o1, o2) -> Byte.compare(o2, o1);
 
@@ -144,21 +144,6 @@ public class BooleanColumn extends AbstractColumn implements BooleanMapUtils, Ca
         return formatter;
     }
 
-    public static byte convert(String stringValue) {
-
-        if (Strings.isNullOrEmpty(stringValue) || TypeUtils.MISSING_INDICATORS.contains(stringValue)) {
-            return MISSING_VALUE;
-
-        } else if (TypeUtils.TRUE_STRINGS.contains(stringValue)) {
-            return BYTE_TRUE;
-        } else if (TypeUtils.FALSE_STRINGS.contains(stringValue)) {
-            return BYTE_FALSE;
-        } else {
-            throw new IllegalArgumentException("Attempting to convert non-boolean value " +
-                    stringValue + " to Boolean");
-        }
-    }
-
     public int size() {
         return data.size();
     }
@@ -294,7 +279,13 @@ public class BooleanColumn extends AbstractColumn implements BooleanMapUtils, Ca
     }
 
     public BooleanColumn appendCell(String object) {
-        append(convert(object));
+        append(BooleanColumnType.DEFAULT_PARSER.parseByte(object));
+        return this;
+    }
+
+    @Override
+    public BooleanColumn appendCell(String object, StringParser parser) {
+        append(parser.parseByte(object));
         return this;
     }
 
diff --git a/core/src/main/java/tech/tablesaw/api/ColumnType.java b/core/src/main/java/tech/tablesaw/api/ColumnType.java
index c10a20a5..e45b1102 100644
--- a/core/src/main/java/tech/tablesaw/api/ColumnType.java
+++ b/core/src/main/java/tech/tablesaw/api/ColumnType.java
@@ -1,6 +1,7 @@
 package tech.tablesaw.api;
 
 import com.google.common.base.Preconditions;
+import tech.tablesaw.columns.StringParser;
 import tech.tablesaw.columns.booleans.BooleanColumnType;
 import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.dates.DateColumnType;
@@ -9,6 +10,7 @@ import tech.tablesaw.columns.numbers.DoubleColumnType;
 import tech.tablesaw.columns.SkipColumnType;
 import tech.tablesaw.columns.strings.StringColumnType;
 import tech.tablesaw.columns.times.TimeColumnType;
+import tech.tablesaw.io.csv.CsvReadOptions;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -53,4 +55,8 @@ public interface ColumnType {
     int byteSize();
 
     String getPrinterFriendlyName();
+
+    StringParser defaultParser();
+
+    StringParser customParser(CsvReadOptions options);
 }
diff --git a/core/src/main/java/tech/tablesaw/api/DateColumn.java b/core/src/main/java/tech/tablesaw/api/DateColumn.java
index 63a84b0f..619862eb 100644
--- a/core/src/main/java/tech/tablesaw/api/DateColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateColumn.java
@@ -15,7 +15,6 @@
 package tech.tablesaw.api;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntArrays;
 import it.unimi.dsi.fastutil.ints.IntComparator;
@@ -24,13 +23,14 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
 import tech.tablesaw.columns.dates.DateColumnFormatter;
+import tech.tablesaw.columns.dates.DateColumnType;
 import tech.tablesaw.columns.dates.DateFillers;
 import tech.tablesaw.columns.dates.DateFilters;
 import tech.tablesaw.columns.dates.DateMapFunctions;
 import tech.tablesaw.columns.dates.PackedLocalDate;
 import tech.tablesaw.filtering.predicates.IntBiPredicate;
-import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.selection.BitmapBackedSelection;
 import tech.tablesaw.selection.Selection;
 import tech.tablesaw.sorting.comparators.DescendingIntComparator;
@@ -38,13 +38,11 @@ import tech.tablesaw.sorting.comparators.DescendingIntComparator;
 import java.nio.ByteBuffer;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
@@ -60,11 +58,6 @@ DateMapFunctions, CategoricalColumn, Iterable<LocalDate> {
 
     public static final int MISSING_VALUE = (Integer) ColumnType.LOCAL_DATE.getMissingValue();
 
-    /**
-     * locale for formatter
-     */
-    private final Locale locale;
-
     private final IntComparator reverseIntComparator = DescendingIntComparator.instance();
 
     private IntArrayList data;
@@ -77,33 +70,16 @@ DateMapFunctions, CategoricalColumn, Iterable<LocalDate> {
 
     private DateColumnFormatter printFormatter = new DateColumnFormatter();
 
-    /**
-     * The formatter chosen to parse dates for this particular column
-     */
-    private DateTimeFormatter selectedFormatter;
-
-    public void setFormatter(DateTimeFormatter formatter) {
-        this.selectedFormatter = formatter;
-    }
-
     public static DateColumn create(String name) {
-        return create(name, DEFAULT_ARRAY_SIZE, Locale.getDefault());
-    }
-
-    public static DateColumn create(String name, Locale locale) {
-        return create(name, DEFAULT_ARRAY_SIZE, locale);
-    }
-
-    public static DateColumn create(String name, int initialSize, Locale locale) {
-        return new DateColumn(name, new IntArrayList(initialSize), locale);
+        return create(name, DEFAULT_ARRAY_SIZE);
     }
 
     public static DateColumn create(String name, int initialSize) {
-        return create(name, initialSize, Locale.getDefault());
+        return new DateColumn(name, new IntArrayList(initialSize));
     }
 
     public static DateColumn create(String name, List<LocalDate> data) {
-        DateColumn column = new DateColumn(name, new IntArrayList(data.size()), Locale.getDefault());
+        DateColumn column = new DateColumn(name, new IntArrayList(data.size()));
         for (LocalDate date : data) {
             column.append(date);
         }
@@ -111,17 +87,16 @@ DateMapFunctions, CategoricalColumn, Iterable<LocalDate> {
     }
 
     public static DateColumn create(String name, LocalDate[] data) {
-        DateColumn column = new DateColumn(name, new IntArrayList(data.length), Locale.getDefault());
+        DateColumn column = new DateColumn(name, new IntArrayList(data.length));
         for (LocalDate date : data) {
             column.append(date);
         }
         return column;
     }
 
-    private DateColumn(String name, IntArrayList data, Locale locale) {
+    private DateColumn(String name, IntArrayList data) {
         super(ColumnType.LOCAL_DATE, name);
         this.data = data;
-        this.locale = locale;
     }
 
     @Override
@@ -182,9 +157,8 @@ DateMapFunctions, CategoricalColumn, Iterable<LocalDate> {
 
     @Override
     public DateColumn emptyCopy(int rowSize) {
-        DateColumn copy = create(name(), rowSize, locale);
+        DateColumn copy = create(name(), rowSize);
         copy.printFormatter = printFormatter;
-        copy.selectedFormatter = selectedFormatter;
         return copy;
     }
 
@@ -340,45 +314,15 @@ DateMapFunctions, CategoricalColumn, Iterable<LocalDate> {
         return comparator;
     }
 
-    /**
-     * Returns a PackedDate as converted from the given string
-     *
-     * @param value A string representation of a date
-     * @throws DateTimeParseException if no parser can be found for the date format
-     */
-    public int convert(String value) {
-        if (Strings.isNullOrEmpty(value) || TypeUtils.MISSING_INDICATORS.contains(value) || value.equals("-1")) {
-            return (Integer) ColumnType.LOCAL_DATE.getMissingValue();
-        }
-        String paddedValue = Strings.padStart(value, 4, '0');
-
-        if (selectedFormatter == null) {
-            selectedFormatter = TypeUtils.getDateFormatter(paddedValue).withLocale(locale);
-        }
-        LocalDate date = parseDate(paddedValue);
-        return PackedLocalDate.pack(date);
-    }
-
-    private LocalDate parseDate(String value) {
-        if (selectedFormatter == null) {
-            setFormatter(TypeUtils.getDateFormatter(value));
-        }
-        if (! TypeUtils.canParse(selectedFormatter, value)) {
-            setFormatter(TypeUtils.getDateFormatter(value));
-        }
-        LocalDate date;
-        try {
-            date = LocalDate.parse(value, selectedFormatter);
-        } catch (DateTimeParseException e) {
-            selectedFormatter = TypeUtils.DATE_FORMATTER.withLocale(locale);
-            date = LocalDate.parse(value, selectedFormatter);
-        }
-        return date;
+    @Override
+    public DateColumn appendCell(String string) {
+        appendInternal(PackedLocalDate.pack(DateColumnType.DEFAULT_PARSER.parse(string)));
+        return this;
     }
 
     @Override
-    public DateColumn appendCell(String string) {
-        appendInternal(convert(string));
+    public DateColumn appendCell(String string, StringParser parser) {
+        appendInternal(PackedLocalDate.pack((LocalDate) parser.parse(string)));
         return this;
     }
 
diff --git a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
index 7f38e2df..963af182 100644
--- a/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateTimeColumn.java
@@ -15,7 +15,6 @@
 package tech.tablesaw.api;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.IntComparator;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.longs.LongArrays;
@@ -25,12 +24,13 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 import it.unimi.dsi.fastutil.longs.LongSet;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
 import tech.tablesaw.columns.datetimes.DateTimeColumnFormatter;
+import tech.tablesaw.columns.datetimes.DateTimeColumnType;
 import tech.tablesaw.columns.datetimes.DateTimeFillers;
 import tech.tablesaw.columns.datetimes.DateTimeFilters;
 import tech.tablesaw.columns.datetimes.DateTimeMapFunctions;
 import tech.tablesaw.columns.datetimes.PackedLocalDateTime;
-import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.selection.Selection;
 import tech.tablesaw.sorting.comparators.DescendingLongComparator;
 
@@ -38,13 +38,11 @@ import java.nio.ByteBuffer;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -55,7 +53,7 @@ import static tech.tablesaw.api.ColumnType.LOCAL_DATE_TIME;
  * A column in a table that contains long-integer encoded (packed) local date-time values
  */
 public class DateTimeColumn extends AbstractColumn
-implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn>, Iterable<LocalDateTime> {
+    implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn>, Iterable<LocalDateTime> {
 
     public static final long MISSING_VALUE = (Long) ColumnType.LOCAL_DATE_TIME.getMissingValue();
 
@@ -69,19 +67,8 @@ implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn
         return Long.compare(f1, f2);
     };
 
-    /**
-     * The formatter chosen to parse date-time strings for this particular column
-     */
-    private DateTimeFormatter selectedFormatter;
-
-    public void setFormatter(DateTimeFormatter formatter) {
-        this.selectedFormatter = formatter;
-    }
-
     private DateTimeColumnFormatter printFormatter = new DateTimeColumnFormatter();
 
-    private Locale locale;
-
     public static boolean valueIsMissing(long value) {
         return MISSING_VALUE == value;
     }
@@ -96,15 +83,11 @@ implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn
     }
 
     public static DateTimeColumn create(String name, int initialSize) {
-        return create(name, initialSize, Locale.getDefault());
-    }
-
-    public static DateTimeColumn create(String name, int initialSize, Locale locale) {
-        return new DateTimeColumn(name, new LongArrayList(initialSize), locale);
+        return new DateTimeColumn(name, new LongArrayList(initialSize));
     }
 
     public static DateTimeColumn create(String name, List<LocalDateTime> data) {
-        DateTimeColumn column = new DateTimeColumn(name, new LongArrayList(data.size()), Locale.getDefault());
+        DateTimeColumn column = new DateTimeColumn(name, new LongArrayList(data.size()));
         for (LocalDateTime date : data) {
             column.append(date);
         }
@@ -112,17 +95,16 @@ implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn
     }
 
     public static DateTimeColumn create(String name, LocalDateTime[] data) {
-        DateTimeColumn column = new DateTimeColumn(name, new LongArrayList(data.length), Locale.getDefault());
+        DateTimeColumn column = new DateTimeColumn(name, new LongArrayList(data.length));
         for (LocalDateTime date : data) {
             column.append(date);
         }
         return column;
     }
 
-    private DateTimeColumn(String name, LongArrayList data, Locale locale) {
+    private DateTimeColumn(String name, LongArrayList data) {
         super(LOCAL_DATE_TIME, name);
         this.data = data;
-        this.locale = locale;
     }
 
     @Override
@@ -184,12 +166,13 @@ implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn
 
     @Override
     public DateTimeColumn appendCell(String stringValue) {
-        if (stringValue == null) {
-            appendInternal(MISSING_VALUE);
-        } else {
-            long dateTime = convert(stringValue);
-            appendInternal(dateTime);
-        }
+        appendInternal(PackedLocalDateTime.pack(DateTimeColumnType.DEFAULT_PARSER.parse(stringValue)));
+        return this;
+    }
+
+    @Override
+    public DateTimeColumn appendCell(String stringValue, StringParser parser) {
+        appendInternal(PackedLocalDateTime.pack((LocalDateTime) parser.parse(stringValue)));
         return this;
     }
 
@@ -203,33 +186,6 @@ implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn
         return this;
     }
 
-    /**
-     * Returns a PackedDateTime as converted from the given string
-     *
-     * @param value A string representation of a time
-     * @throws DateTimeParseException if no parser can be found for the time format used
-     */
-    public long convert(String value) {
-        if (Strings.isNullOrEmpty(value)
-                || TypeUtils.MISSING_INDICATORS.contains(value)
-                || value.equals("-1")) {
-            return MISSING_VALUE;
-        }
-        value = Strings.padStart(value, 4, '0');
-        LocalDateTime dateTime = parseDateTime(value);
-        return PackedLocalDateTime.pack(dateTime);
-    }
-
-    private LocalDateTime parseDateTime(String value) {
-        if (selectedFormatter == null) {
-            setFormatter(TypeUtils.getDateTimeFormatter(value));
-        }
-        if (! TypeUtils.canParse(selectedFormatter, value)) {
-            setFormatter(TypeUtils.getDateTimeFormatter(value));
-        }
-        return LocalDateTime.parse(value, selectedFormatter);
-    }
-
     public int size() {
         return data.size();
     }
@@ -260,8 +216,7 @@ implements DateTimeMapFunctions, DateTimeFilters, DateTimeFillers<DateTimeColumn
 
     @Override
     public DateTimeColumn emptyCopy(int rowSize) {
-        DateTimeColumn column = create(name(), rowSize, locale);
-        column.setFormatter(selectedFormatter);
+        DateTimeColumn column = create(name(), rowSize);
         column.setPrintFormatter(printFormatter);
         return column;
     }
diff --git a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
index c579587b..600b888c 100644
--- a/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DoubleColumn.java
@@ -15,7 +15,6 @@
 package tech.tablesaw.api;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
 import it.unimi.dsi.fastutil.doubles.DoubleArrays;
 import it.unimi.dsi.fastutil.doubles.DoubleComparator;
@@ -30,11 +29,12 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.columns.numbers.DoubleColumnType;
 import tech.tablesaw.columns.numbers.NumberColumnFormatter;
 import tech.tablesaw.columns.numbers.Stats;
 import tech.tablesaw.filtering.predicates.DoubleBiPredicate;
 import tech.tablesaw.filtering.predicates.DoubleRangePredicate;
-import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.selection.BitmapBackedSelection;
 import tech.tablesaw.selection.Selection;
 
@@ -50,8 +50,6 @@ import java.util.function.BiPredicate;
 import java.util.function.DoubleConsumer;
 import java.util.function.DoublePredicate;
 import java.util.function.DoubleSupplier;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import static tech.tablesaw.api.ColumnType.NUMBER;
 
@@ -60,7 +58,6 @@ import static tech.tablesaw.api.ColumnType.NUMBER;
  */
 public class DoubleColumn extends AbstractColumn implements NumberColumn {
 
-    private static final Pattern COMMA_PATTERN = Pattern.compile(",");
     /**
      * Compares two doubles, such that a sort based on this comparator would sort in descending order
      */
@@ -130,22 +127,6 @@ public class DoubleColumn extends AbstractColumn implements NumberColumn {
         return new DoubleColumn(name, new DoubleArrayList(doubles));
     }
 
-    /**
-     * Returns a double that is parsed from the given String
-     * <p>
-     * We remove any commas before parsing
-     */
-    public static double convert(final String stringValue) {
-        if (stringValue == null) {
-            return MISSING_VALUE;
-        }
-        if (Strings.isNullOrEmpty(stringValue.trim()) || TypeUtils.MISSING_INDICATORS.contains(stringValue)) {
-            return MISSING_VALUE;
-        }
-        final Matcher matcher = DoubleColumn.COMMA_PATTERN.matcher(stringValue);
-        return Double.parseDouble(matcher.replaceAll(""));
-    }
-
     @Override
     public DoubleColumn removeMissing() {
         final DoubleColumn noMissing = (DoubleColumn) emptyCopy();
@@ -385,7 +366,17 @@ public class DoubleColumn extends AbstractColumn implements NumberColumn {
     @Override
     public DoubleColumn appendCell(final String object) {
         try {
-            append(convert(object));
+            append(DoubleColumnType.DEFAULT_PARSER.parseDouble(object));
+        } catch (final NumberFormatException e) {
+            throw new NumberFormatException(name() + ": " + e.getMessage());
+        }
+        return this;
+    }
+
+    @Override
+    public DoubleColumn appendCell(final String object, StringParser parser) {
+        try {
+            append(parser.parseDouble(object));
         } catch (final NumberFormatException e) {
             throw new NumberFormatException(name() + ": " + e.getMessage());
         }
diff --git a/core/src/main/java/tech/tablesaw/api/StringColumn.java b/core/src/main/java/tech/tablesaw/api/StringColumn.java
index 96ed7c77..f6e1ba05 100644
--- a/core/src/main/java/tech/tablesaw/api/StringColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/StringColumn.java
@@ -15,7 +15,6 @@
 package tech.tablesaw.api;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@@ -29,11 +28,12 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
 import tech.tablesaw.columns.strings.StringColumnFormatter;
+import tech.tablesaw.columns.strings.StringColumnType;
 import tech.tablesaw.columns.strings.StringFilters;
 import tech.tablesaw.columns.strings.StringMapFunctions;
 import tech.tablesaw.columns.strings.StringReduceUtils;
-import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.selection.BitmapBackedSelection;
 import tech.tablesaw.selection.Selection;
 
@@ -128,13 +128,6 @@ public class StringColumn extends AbstractColumn
         }
     }
 
-    public static String convert(String stringValue) {
-        if (Strings.isNullOrEmpty(stringValue) || TypeUtils.MISSING_INDICATORS.contains(stringValue)) {
-            return MISSING_VALUE;
-        }
-        return stringValue;
-    }
-
     @Override
     public boolean isMissing(int rowNumber) {
         return get(rowNumber).equals(MISSING_VALUE);
@@ -423,7 +416,13 @@ public class StringColumn extends AbstractColumn
 
     @Override
     public StringColumn appendCell(String object) {
-        addValue(convert(object));
+        addValue(StringColumnType.DEFAULT_PARSER.parse(object));
+        return this;
+    }
+
+    @Override
+    public StringColumn appendCell(String object, StringParser parser) {
+        addValue((String) parser.parse(object));
         return this;
     }
 
diff --git a/core/src/main/java/tech/tablesaw/api/TimeColumn.java b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
index 8906b8c2..7233c031 100644
--- a/core/src/main/java/tech/tablesaw/api/TimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
@@ -15,7 +15,6 @@
 package tech.tablesaw.api;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntArrays;
 import it.unimi.dsi.fastutil.ints.IntComparator;
@@ -24,25 +23,24 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
 import tech.tablesaw.columns.times.PackedLocalTime;
 import tech.tablesaw.columns.times.TimeColumnFormatter;
+import tech.tablesaw.columns.times.TimeColumnType;
 import tech.tablesaw.columns.times.TimeFillers;
 import tech.tablesaw.columns.times.TimeFilters;
 import tech.tablesaw.columns.times.TimeMapFunctions;
-import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.selection.Selection;
 import tech.tablesaw.sorting.comparators.DescendingIntComparator;
 
 import java.nio.ByteBuffer;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -57,20 +55,7 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
 
     public static final int MISSING_VALUE = (Integer) LOCAL_TIME.getMissingValue();
 
-    /**
-     * locale for formatter
-     */
-    private Locale locale;
-
     private final IntComparator descendingIntComparator = DescendingIntComparator.instance();
-    /**
-     * The formatter chosen to parse times for this particular column
-     */
-    private DateTimeFormatter selectedFormatter;
-
-    public void setFormatter(DateTimeFormatter formatter) {
-        this.selectedFormatter = formatter;
-    }
 
     private TimeColumnFormatter printFormatter = new TimeColumnFormatter();
 
@@ -82,10 +67,9 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
         return Integer.compare(f1, f2);
     };
 
-    private TimeColumn(String name, IntArrayList times, Locale locale) {
+    private TimeColumn(String name, IntArrayList times) {
         super(LOCAL_TIME, name);
         data = times;
-        this.locale = locale;
     }
 
     public static boolean valueIsMissing(int i) {
@@ -97,7 +81,7 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
     }
 
     public static TimeColumn create(String name, List<LocalTime> data) {
-        TimeColumn column = new TimeColumn(name, new IntArrayList(data.size()), Locale.getDefault());
+        TimeColumn column = new TimeColumn(name, new IntArrayList(data.size()));
         for (LocalTime time : data) {
             column.append(time);
         }
@@ -105,7 +89,7 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
     }
 
     public static TimeColumn create(String name, LocalTime[] data) {
-        TimeColumn column = new TimeColumn(name, new IntArrayList(data.length), Locale.getDefault());
+        TimeColumn column = new TimeColumn(name, new IntArrayList(data.length));
         for (LocalTime time : data) {
             column.append(time);
         }
@@ -113,11 +97,7 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
     }
 
     public static TimeColumn create(String name, int initialSize) {
-        return create(name, initialSize, Locale.getDefault());
-    }
-
-    public static TimeColumn create(String name, int initialSize, Locale locale) {
-        return new TimeColumn(name, new IntArrayList(initialSize), locale);
+        return new TimeColumn(name, new IntArrayList(initialSize));
     }
 
     @Override
@@ -210,9 +190,8 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
 
     @Override
     public TimeColumn emptyCopy(int rowSize) {
-        TimeColumn column = TimeColumn.create(name(), rowSize, locale);
+        TimeColumn column = TimeColumn.create(name(), rowSize);
         column.printFormatter = printFormatter;
-        column.selectedFormatter = selectedFormatter;
         return column;
     }
 
@@ -346,47 +325,15 @@ public class TimeColumn extends AbstractColumn implements CategoricalColumn, Ite
         return data.isEmpty();
     }
 
-    /**
-     * Returns a PackedTime as converted from the given string
-     *
-     * @param value A string representation of a time
-     * @throws DateTimeParseException if no parser can be found for the time format used
-     */
-    public int convert(String value) {
-        if (Strings.isNullOrEmpty(value)
-                || TypeUtils.MISSING_INDICATORS.contains(value)
-                || value.equals("-1")) {
-            return MISSING_VALUE;
-        }
-        value = Strings.padStart(value, 4, '0');
-        if (selectedFormatter == null) {
-            selectedFormatter = TypeUtils.getTimeFormatter(value);
-        }
-        LocalTime time = parseTime(value);
-        return PackedLocalTime.pack(time);
-    }
-
-    private LocalTime parseTime(String value) {
-        if (selectedFormatter == null) {
-            setFormatter(TypeUtils.getTimeFormatter(value));
-        }
-        if (! TypeUtils.canParse(selectedFormatter, value)) {
-            setFormatter(TypeUtils.getTimeFormatter(value));
-        }
-        LocalTime time;
-        try {
-            time = LocalTime.parse(value, selectedFormatter);
-        } catch (DateTimeParseException e) {
-            selectedFormatter = TypeUtils.TIME_FORMATTER.withLocale(locale);
-            time = LocalTime.parse(value, selectedFormatter);
-        }
-        return time;
+    @Override
+    public TimeColumn appendCell(String object) {
+        appendInternal(PackedLocalTime.pack(TimeColumnType.DEFAULT_PARSER.parse(object)));
+        return this;
     }
 
-
     @Override
-    public TimeColumn appendCell(String object) {
-        appendInternal(convert(object));
+    public TimeColumn appendCell(String object, StringParser parser) {
+        appendInternal(PackedLocalTime.pack((LocalTime) parser.parse(object)));
         return this;
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/Column.java b/core/src/main/java/tech/tablesaw/columns/Column.java
index 63c36ec9..9cc32918 100644
--- a/core/src/main/java/tech/tablesaw/columns/Column.java
+++ b/core/src/main/java/tech/tablesaw/columns/Column.java
@@ -169,6 +169,8 @@ public interface Column {
 
     Column appendCell(String stringValue);
 
+    Column appendCell(String stringValue, StringParser parser);
+
     IntComparator rowComparator();
 
     void append(Column column);
diff --git a/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java b/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
index e58f7d1b..814c1287 100644
--- a/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
@@ -1,5 +1,7 @@
 package tech.tablesaw.columns;
 
+import tech.tablesaw.io.csv.CsvReadOptions;
+
 public class SkipColumnType extends AbstractColumnType {
 
     public SkipColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
@@ -10,4 +12,14 @@ public class SkipColumnType extends AbstractColumnType {
     public Column create(String name) {
         throw new UnsupportedOperationException("Column type " + name() + " doesn't support column creation");
     }
+
+    @Override
+    public StringParser defaultParser() {
+        throw new UnsupportedOperationException("Column type " + name() + " doesn't support parsing");
+    }
+
+    @Override
+    public StringParser customParser(CsvReadOptions options) {
+        throw new UnsupportedOperationException("Column type " + name() + " doesn't support parsing");
+    }
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/StringParser.java b/core/src/main/java/tech/tablesaw/columns/StringParser.java
new file mode 100644
index 00000000..85da4f60
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/StringParser.java
@@ -0,0 +1,50 @@
+package tech.tablesaw.columns;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.io.TypeUtils;
+
+import java.util.List;
+
+/**
+ * A parser for turning strings into objects that can be inserted into a column
+ *
+ * It serves two purposes, to determine if a string can be parsed into the desired object type, and to actually
+ * parse the string.
+ *
+ * Implementations may take additional parameters such as a locale or DateTimeFormatter.
+ *
+ * @param <T>   The Class of object to be inserted: String for StringColumn, LocalDate for DateColumn, etc.
+ */
+public abstract class StringParser<T> {
+
+    private ColumnType columnType;
+
+    protected List<String> missingValueStrings = TypeUtils.MISSING_INDICATORS;
+
+    public StringParser(ColumnType columnType) {
+        this.columnType = columnType;
+    }
+
+    public abstract boolean canParse(String s);
+
+    public abstract T parse(String s);
+
+    public ColumnType columnType() {
+        return columnType;
+    }
+
+    protected boolean isMissing(String s) {
+        if (s == null) {
+            return true;
+        }
+        return s.isEmpty() || missingValueStrings.contains(s);
+    }
+
+    public byte parseByte(String s) {
+        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " doesn't support parsing to booleans");
+    }
+
+    public double parseDouble(String s) {
+        throw new UnsupportedOperationException(this.getClass().getSimpleName() + " doesn't support parsing to booleans");
+    }
+}
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 b61bf8bb..ffb9315e 100644
--- a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
@@ -1,11 +1,15 @@
 package tech.tablesaw.columns.booleans;
 
 import tech.tablesaw.api.BooleanColumn;
+import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractColumnType;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.io.csv.CsvReadOptions;
 
 public class BooleanColumnType extends AbstractColumnType {
 
+    public static BooleanStringParser DEFAULT_PARSER = new BooleanStringParser(ColumnType.BOOLEAN);
+
     public BooleanColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
         super(missingValue, byteSize, name, printerFriendlyName);
     }
@@ -14,4 +18,14 @@ public class BooleanColumnType extends AbstractColumnType {
     public Column create(String name) {
         return BooleanColumn.create(name);
     }
+
+    @Override
+    public BooleanStringParser defaultParser() {
+        return DEFAULT_PARSER;
+    }
+
+    @Override
+    public BooleanStringParser customParser(CsvReadOptions readOptions) {
+        return new BooleanStringParser(this, readOptions);
+    }
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanStringParser.java b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanStringParser.java
new file mode 100644
index 00000000..068cf6ec
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanStringParser.java
@@ -0,0 +1,62 @@
+package tech.tablesaw.columns.booleans;
+
+import com.google.common.collect.Lists;
+import tech.tablesaw.api.BooleanColumn;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.TypeUtils;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import static tech.tablesaw.api.BooleanColumn.*;
+
+public class BooleanStringParser extends StringParser<Boolean> {
+
+    public BooleanStringParser(ColumnType columnType) {
+        super(columnType);
+    }
+
+    public BooleanStringParser(BooleanColumnType booleanColumnType, CsvReadOptions readOptions) {
+        super(booleanColumnType);
+        if (readOptions.missingValueIndicator() != null) {
+            missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
+        }
+    }
+
+    @Override
+    public boolean canParse(String s) {
+        if (isMissing(s)) {
+            return true;
+        }
+        return TypeUtils.TRUE_STRINGS_FOR_DETECTION.contains(s)
+                || TypeUtils.FALSE_STRINGS_FOR_DETECTION.contains(s);
+    }
+
+    @Override
+    public Boolean parse(String s) {
+        if (isMissing(s)) {
+            return null;
+        } else if (TypeUtils.TRUE_STRINGS.contains(s)) {
+            return true;
+        } else if (TypeUtils.FALSE_STRINGS.contains(s)) {
+            return false;
+        } else {
+            throw new IllegalArgumentException("Attempting to convert non-boolean value " +
+                    s + " to Boolean");
+        }
+    }
+
+    @Override
+    public byte parseByte(String s) {
+        if (isMissing(s)) {
+            return MISSING_VALUE;
+        } else if (TypeUtils.TRUE_STRINGS.contains(s)) {
+            return BooleanColumn.BYTE_TRUE;
+        } else if (TypeUtils.FALSE_STRINGS.contains(s)) {
+            return BYTE_FALSE;
+        } else {
+            throw new IllegalArgumentException("Attempting to convert non-boolean value " +
+                    s + " to Boolean");
+        }
+    }
+}
+
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 644189a1..bbaa94d0 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
@@ -1,10 +1,17 @@
 package tech.tablesaw.columns.dates;
 
+import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DateColumn;
 import tech.tablesaw.columns.AbstractColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.time.LocalDate;
 
 public class DateColumnType extends AbstractColumnType {
 
+    public static final DateStringParser DEFAULT_PARSER = new DateStringParser(ColumnType.LOCAL_DATE);
+
     public DateColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
         super(missingValue, byteSize, name, printerFriendlyName);
     }
@@ -13,4 +20,15 @@ public class DateColumnType extends AbstractColumnType {
     public DateColumn create(String name) {
         return DateColumn.create(name);
     }
+
+    @Override
+    public StringParser<LocalDate> defaultParser() {
+        return new DateStringParser(this);
+    }
+
+    @Override
+    public StringParser<LocalDate> customParser(CsvReadOptions options) {
+        return new DateStringParser(this, options);
+    }
+
 }
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 598426cb..6fca1f06 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
@@ -1,10 +1,17 @@
 package tech.tablesaw.columns.datetimes;
 
+import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.columns.AbstractColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.time.LocalDateTime;
 
 public class DateTimeColumnType extends AbstractColumnType {
 
+    public static final DateTimeStringParser DEFAULT_PARSER = new DateTimeStringParser(ColumnType.LOCAL_DATE_TIME);
+
     public DateTimeColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
         super(missingValue, byteSize, name, printerFriendlyName);
     }
@@ -13,4 +20,15 @@ public class DateTimeColumnType extends AbstractColumnType {
     public DateTimeColumn create(String name) {
         return DateTimeColumn.create(name);
     }
+
+    @Override
+    public StringParser<LocalDateTime> defaultParser() {
+        return new DateTimeStringParser(this);
+    }
+
+    @Override
+    public DateTimeStringParser customParser(CsvReadOptions options) {
+        return new DateTimeStringParser(this, options);
+    }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeStringParser.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeStringParser.java
new file mode 100644
index 00000000..7f314c51
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeStringParser.java
@@ -0,0 +1,62 @@
+package tech.tablesaw.columns.datetimes;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Locale;
+
+import static tech.tablesaw.io.TypeUtils.DATE_TIME_FORMATTER;
+
+public class DateTimeStringParser extends StringParser<LocalDateTime> {
+
+    private static final DateTimeFormatter DEFAULT_FORMATTER = DATE_TIME_FORMATTER;
+
+    private Locale locale = Locale.getDefault();
+    private DateTimeFormatter formatter = DEFAULT_FORMATTER;
+
+    public DateTimeStringParser(ColumnType columnType) {
+        super(columnType);
+    }
+
+    public DateTimeStringParser(DateTimeColumnType dateTimeColumnType, CsvReadOptions readOptions) {
+        super(dateTimeColumnType);
+        if (readOptions.dateFormatter() != null) {
+            formatter = readOptions.dateFormatter();
+        }
+        if (readOptions.locale() != null) {
+            locale = readOptions.locale();
+        }
+        if (readOptions.missingValueIndicator() != null) {
+            missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
+        }
+    }
+
+    @Override
+    public boolean canParse(String s) {
+        if (isMissing(s)) {
+            return true;
+        }
+        try {
+            LocalDateTime.parse(s, formatter.withLocale(locale));
+            return true;
+        } catch (DateTimeParseException e) {
+            // it's all part of the plan
+            return false;
+        }
+    }
+
+    @Override
+    public LocalDateTime parse(String value) {
+        if (isMissing(value)) {
+            return null;
+        }
+        value = Strings.padStart(value, 4, '0');
+        return LocalDateTime.parse(value, formatter);
+    }
+}
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 5eb0fbb6..10fbb346 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
@@ -1,10 +1,14 @@
 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;
 
 public class DoubleColumnType extends AbstractColumnType {
 
+    public static final DoubleStringParser DEFAULT_PARSER = new DoubleStringParser(ColumnType.NUMBER);
+
     public DoubleColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
         super(missingValue, byteSize, name, printerFriendlyName);
     }
@@ -13,4 +17,15 @@ public class DoubleColumnType extends AbstractColumnType {
     public DoubleColumn create(String name) {
         return DoubleColumn.create(name);
     }
+
+    @Override
+    public DoubleStringParser defaultParser() {
+        return DEFAULT_PARSER;
+    }
+
+    @Override
+    public DoubleStringParser customParser(CsvReadOptions options) {
+        return new DoubleStringParser(this, options);
+    }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleStringParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleStringParser.java
new file mode 100644
index 00000000..6b5fad68
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleStringParser.java
@@ -0,0 +1,58 @@
+package tech.tablesaw.columns.numbers;
+
+import com.google.common.collect.Lists;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.DoubleColumn;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DoubleStringParser extends StringParser<Double> {
+
+    private static final Pattern COMMA_PATTERN = Pattern.compile(",");
+
+    public DoubleStringParser(ColumnType columnType) {
+        super(columnType);
+    }
+
+    public DoubleStringParser(DoubleColumnType doubleColumnType, CsvReadOptions readOptions) {
+        super(doubleColumnType);
+        if (readOptions.missingValueIndicator() != null) {
+            missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
+        }
+    }
+
+    @Override
+    public boolean canParse(String s) {
+        if (isMissing(s)) {
+            return true;
+        }
+        try {
+            Double.parseDouble(s);
+            return true;
+        } catch (NumberFormatException e) {
+            // it's all part of the plan
+            return false;
+        }
+    }
+
+    @Override
+    public Double parse(String s) {
+        if (isMissing(s)) {
+            return DoubleColumn.MISSING_VALUE;
+        }
+        final Matcher matcher = COMMA_PATTERN.matcher(s);
+        return Double.parseDouble(matcher.replaceAll(""));
+    }
+
+    @Override
+    public double parseDouble(String s) {
+        if (isMissing(s)) {
+            return DoubleColumn.MISSING_VALUE;
+        }
+        final Matcher matcher = COMMA_PATTERN.matcher(s);
+        return Double.parseDouble(matcher.replaceAll(""));
+    }
+}
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 23dfa66f..200979d5 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
@@ -1,10 +1,14 @@
 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;
 
 public class StringColumnType extends AbstractColumnType {
 
+    public static final StringStringParser DEFAULT_PARSER = new StringStringParser(ColumnType.STRING);
+
     public StringColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
         super(missingValue, byteSize, name, printerFriendlyName);
     }
@@ -13,4 +17,15 @@ public class StringColumnType extends AbstractColumnType {
     public StringColumn create(String name) {
         return StringColumn.create(name);
     }
+
+    @Override
+    public StringStringParser defaultParser() {
+        return new StringStringParser(this);
+    }
+
+    @Override
+    public StringStringParser customParser(CsvReadOptions options) {
+        return new StringStringParser(this, options);
+    }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/StringStringParser.java b/core/src/main/java/tech/tablesaw/columns/strings/StringStringParser.java
new file mode 100644
index 00000000..e2a77cf7
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringStringParser.java
@@ -0,0 +1,42 @@
+package tech.tablesaw.columns.strings;
+
+import com.google.common.collect.Lists;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.util.Locale;
+
+import static tech.tablesaw.api.StringColumn.MISSING_VALUE;
+
+public class StringStringParser extends StringParser<String> {
+
+    private Locale locale = Locale.getDefault();
+
+    public StringStringParser(ColumnType columnType) {
+        super(columnType);
+    }
+
+    public StringStringParser(ColumnType columnType, CsvReadOptions readOptions) {
+        super(columnType);
+        if (readOptions.locale() != null) {
+            locale = readOptions.locale();
+        }
+        if (readOptions.missingValueIndicator() != null) {
+            missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
+        }
+    }
+
+    @Override
+    public boolean canParse(String s) {
+        return true;
+    }
+
+    @Override
+    public String parse(String s) {
+        if (isMissing(s)) {
+            return MISSING_VALUE;
+        }
+        return s;
+    }
+}
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 c25a3461..e4f90f27 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
@@ -1,10 +1,17 @@
 package tech.tablesaw.columns.times;
 
+import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.TimeColumn;
 import tech.tablesaw.columns.AbstractColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.time.LocalTime;
 
 public class TimeColumnType extends AbstractColumnType {
 
+    public static final TimeStringParser DEFAULT_PARSER = new TimeStringParser(ColumnType.LOCAL_TIME);
+
     public TimeColumnType(Comparable<?> missingValue, int byteSize, String name, String printerFriendlyName) {
         super(missingValue, byteSize, name, printerFriendlyName);
     }
@@ -13,4 +20,15 @@ public class TimeColumnType extends AbstractColumnType {
     public TimeColumn create(String name) {
         return TimeColumn.create(name);
     }
+
+    @Override
+    public StringParser<LocalTime> defaultParser() {
+        return new TimeStringParser(this);
+    }
+
+    @Override
+    public StringParser<LocalTime> customParser(CsvReadOptions options) {
+        return new TimeStringParser(this, options);
+    }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeStringParser.java b/core/src/main/java/tech/tablesaw/columns/times/TimeStringParser.java
new file mode 100644
index 00000000..b87dbf15
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeStringParser.java
@@ -0,0 +1,63 @@
+package tech.tablesaw.columns.times;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Locale;
+
+import static tech.tablesaw.io.TypeUtils.TIME_DETECTION_FORMATTER;
+
+public class TimeStringParser extends StringParser<LocalTime> {
+
+    private static final DateTimeFormatter DEFAULT_FORMATTER = TIME_DETECTION_FORMATTER;
+
+    private Locale locale = Locale.getDefault();
+    private DateTimeFormatter formatter = DEFAULT_FORMATTER;
+
+    public TimeStringParser(ColumnType columnType) {
+        super(columnType);
+    }
+
+    public TimeStringParser(ColumnType columnType, CsvReadOptions readOptions) {
+        super(columnType);
+        if (readOptions.dateFormatter() != null) {
+            if (readOptions.dateFormatter().getLocale() != null ) {} // TODO HERE AND OTHER TIME COLS, MAKE SURE WE DON'T REPLACE A LOCALE SET IN FORMATTER IF ANY
+            formatter = readOptions.dateFormatter();
+        }
+        if (readOptions.locale() != null) {
+            locale = readOptions.locale();
+        }
+        if (readOptions.missingValueIndicator() != null) {
+            missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
+        }
+    }
+
+    @Override
+    public boolean canParse(String s) {
+        if (isMissing(s)) {
+            return true;
+        }
+        try {
+            LocalTime.parse(s, formatter.withLocale(locale));
+            return true;
+        } catch (DateTimeParseException e) {
+            // it's all part of the plan
+            return false;
+        }
+    }
+
+    @Override
+    public LocalTime parse(String value) {
+        if (isMissing(value)) {
+            return null;
+        }
+        value = Strings.padStart(value, 4, '0');
+        return LocalTime.parse(value, formatter);
+    }
+}
diff --git a/core/src/main/java/tech/tablesaw/io/TypeUtils.java b/core/src/main/java/tech/tablesaw/io/TypeUtils.java
index 740206a6..d1d8df9f 100644
--- a/core/src/main/java/tech/tablesaw/io/TypeUtils.java
+++ b/core/src/main/java/tech/tablesaw/io/TypeUtils.java
@@ -17,13 +17,7 @@ package tech.tablesaw.io;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import tech.tablesaw.api.BooleanColumn;
 import tech.tablesaw.api.ColumnType;
-import tech.tablesaw.api.DateColumn;
-import tech.tablesaw.api.DateTimeColumn;
-import tech.tablesaw.api.DoubleColumn;
-import tech.tablesaw.api.StringColumn;
-import tech.tablesaw.api.TimeColumn;
 import tech.tablesaw.columns.Column;
 
 import javax.annotation.Nonnull;
@@ -224,14 +218,12 @@ public final class TypeUtils {
     /**
      * Private constructor to prevent instantiation
      */
-    private TypeUtils() {
-    }
+    private TypeUtils() {}
 
     /**
      * Constructs and returns a column for the given {@code name} and {@code type}
      */
-    public static Column newColumn(@Nonnull String name,
-                                   @Nonnull ColumnType type) {
+    public static Column newColumn(@Nonnull String name, @Nonnull ColumnType type) {
 
         Preconditions.checkArgument(!Strings.isNullOrEmpty(name),
                 "There must be a valid name for a new column");
@@ -239,29 +231,13 @@ public final class TypeUtils {
         Preconditions.checkArgument(type != ColumnType.SKIP,
                 "SKIP-ped columns should be handled outside of this method.");
 
-        final String columnTypeName = type.name();
-        switch (columnTypeName) {
-            case "LOCAL_DATE":
-                return DateColumn.create(name);
-            case "LOCAL_TIME":
-                return TimeColumn.create(name);
-            case "LOCAL_DATE_TIME":
-                return DateTimeColumn.create(name);
-            case "NUMBER":
-                return DoubleColumn.create(name);
-            case "BOOLEAN":
-                return BooleanColumn.create(name);
-            case "STRING":
-                return StringColumn.create(name);
-            default:
-                throw new IllegalArgumentException("Unknown ColumnType: " + type);
-        }
+        return type.create(name);
     }
 
     /**
      * Returns the first DateTimeFormatter to parse the string, which represents a DATE
      * <p>
-     * It's intended to be called at the start of a large formatting job so that it picks the write format and is not
+     * It's intended to be called at the start of a large formatting job so that it picks the right format and is not
      * called again. This is an optimization, because the older version, which will try multiple formatters was too
      * slow for large data sets.
      */
@@ -327,35 +303,4 @@ public final class TypeUtils {
         }
         return DATE_FORMATTER;
     }
-
-/*
-/**
-     * Handles converting formatted strings and timestamps.
-     * Assumes timestamps are milliseconds since epoch (midnight, January 1, 1970 UTC).
-     * This is the timestamp format most commonly used in Java.
-     * Unix uses seconds since epoch instead of Java's millis since epoch.
-     * Unix timestamps are not currently supported.
-     *//*
-
-    public static class DateTimeConverter {
-        private final boolean isTimestamp;
-        private final DateTimeFormatter dtFormatter;
-
-        public DateTimeConverter() {
-            this.dtFormatter = null;
-            this.isTimestamp = true;
-        }
-
-        public DateTimeConverter(DateTimeFormatter dtFormatter) {
-            this.dtFormatter = dtFormatter;
-            this.isTimestamp = false;
-        }
-
-        public LocalDateTime convert(String dateTime) {
-            return isTimestamp
-                    ? Instant.ofEpochMilli(Long.parseLong(dateTime)).atZone(ZoneOffset.UTC).toLocalDateTime()
-                    : LocalDateTime.parse(dateTime, dtFormatter);
-        }
-    }
-*/
 }
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 3f6afc83..145d604d 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -23,11 +23,9 @@ import com.opencsv.CSVReader;
 import com.opencsv.CSVReaderBuilder;
 import org.apache.commons.lang3.StringUtils;
 import tech.tablesaw.api.ColumnType;
-import tech.tablesaw.api.DateColumn;
-import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.api.Table;
-import tech.tablesaw.api.TimeColumn;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.columns.StringParser;
 import tech.tablesaw.io.TypeUtils;
 import tech.tablesaw.io.UnicodeBOMInputStream;
 
@@ -40,111 +38,16 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.function.BiPredicate;
-import java.util.function.Predicate;
 
 import static tech.tablesaw.api.ColumnType.*;
 
 @Immutable
 public class CsvReader {
 
-    private static final Predicate<String> isBoolean = s
-            -> TypeUtils.TRUE_STRINGS_FOR_DETECTION.contains(s) || TypeUtils.FALSE_STRINGS_FOR_DETECTION.contains(s);
-
-    private static final Predicate<String> isDouble = s -> {
-        try {
-            Double.parseDouble(s);
-            return true;
-        } catch (NumberFormatException e) {
-            // it's all part of the plan
-            return false;
-        }
-    };
-
-    private static final BiPredicate<String, Locale> isLocalDate = (s, locale) -> {
-        try {
-            LocalDate.parse(s, TypeUtils.DATE_FORMATTER.withLocale(locale));
-            return true;
-        } catch (DateTimeParseException e) {
-            // it's all part of the plan
-            return false;
-        }
-    };
-
-    private static boolean isLocalDate(String s, DateTimeFormatter dateTimeFormatter) {
-        try {
-            if (dateTimeFormatter == null) {
-                LocalDate.parse(s, TypeUtils.DATE_FORMATTER);
-                return true;
-            } else {
-                LocalDate.parse(s, dateTimeFormatter);
-                return true;
-            }
-        } catch (DateTimeParseException e) {
-            // it's all part of the plan
-            return false;
-        }
-    }
-
-    private static final BiPredicate<String, Locale> isLocalTime = (s, locale) -> {
-        try {
-            LocalTime.parse(s, TypeUtils.TIME_DETECTION_FORMATTER.withLocale(locale));
-            return true;
-        } catch (DateTimeParseException e) {
-            // it's all part of the plan
-            return false;
-        }
-    };
-
-    private static boolean isLocalTime(String s, DateTimeFormatter formatter) {
-        try {
-            if (formatter == null) {
-                LocalTime.parse(s, TypeUtils.TIME_DETECTION_FORMATTER);
-                return true;
-            } else {
-                LocalDate.parse(s, formatter);
-                return true;
-            }
-        } catch (DateTimeParseException e) {
-            // it's all part of the plan
-            return false;
-        }
-    };
-
-    private static final BiPredicate<String, Locale> isLocalDateTime = (s, locale) -> {
-        try {
-            LocalDateTime.parse(s, TypeUtils.DATE_TIME_FORMATTER.withLocale(locale));
-            return true;
-        } catch (DateTimeParseException e) {
-            // it's all part of the plan
-            return false;
-        }
-    };
-
-    private static boolean isLocalDateTime(String s, DateTimeFormatter formatter) {
-        try {
-            if (formatter == null) {
-                LocalDateTime.parse(s, TypeUtils.DATE_TIME_FORMATTER);
-                return true;
-            } else {
-                LocalDate.parse(s, formatter);
-                return true;
-            }
-        } catch (DateTimeParseException e) {
-            // it's all part of the plan
-            return false;
-        }
-    };
-
     /**
      * Private constructor to prevent instantiation
      */
@@ -202,7 +105,6 @@ public class CsvReader {
                         columnName = "Column " + table.columnCount();
                     }
                     Column newColumn = TypeUtils.newColumn(columnName, types[x]);
-                    addFormatter(newColumn, options);
                     table.addColumns(newColumn);
                 }
             }
@@ -234,13 +136,10 @@ public class CsvReader {
                     int cellIndex = 0;
                     for (int columnIndex : columnIndexes) {
                         Column column = table.column(cellIndex);
+                        StringParser parser = column.type().customParser(options);
                         try {
                             String value = nextLine[columnIndex];
-                            if (value.equals(options.missingValueIndicator())) {
-                                column.appendCell("");
-                            } else {
-                                column.appendCell(value);
-                            }
+                            column.appendCell(value, parser);
                         } catch (Exception e) {
                             throw new AddCellToColumnException(e, columnIndex, rowNumber, columnNames, nextLine);
                         }
@@ -253,22 +152,6 @@ public class CsvReader {
         }
     }
 
-    private static void addFormatter(Column newColumn, CsvReadOptions options) {
-        final String columnTypeName = newColumn.type().name();
-        switch (columnTypeName) {
-            case "LOCAL_DATE_TIME" :
-                ((DateTimeColumn) newColumn).setFormatter(options.dateTimeFormatter());
-                return;
-            case "LOCAL_DATE":
-                ((DateColumn) newColumn).setFormatter(options.dateFormatter());
-                return;
-            case "LOCAL_TIME" :
-                ((TimeColumn) newColumn).setFormatter(options.timeFormatter());
-                return;
-            default:
-        }
-    }
-
     private static void cleanNames(List<String> headerRow) {
         for (int i = 0; i < headerRow.size(); i++) {
             headerRow.set(i, headerRow.get(i).trim());
@@ -548,75 +431,25 @@ public class CsvReader {
      */
     private static ColumnType detectType(List<String> valuesList, CsvReadOptions options) {
 
-        Locale locale = options.locale();
-        DateTimeFormatter dateFormatter = options.dateFormatter();
-        DateTimeFormatter timeFormatter = options.timeFormatter();
-        DateTimeFormatter dateTimeFormatter = options.dateTimeFormatter();
-
         // Types to choose from. When more than one would work, we pick the first of the options
         ColumnType[] typeArray
-                = // we leave out category, as that is the default type
+                = // we leave out string, as that is the default type
                 {LOCAL_DATE_TIME, LOCAL_TIME, LOCAL_DATE, BOOLEAN, NUMBER};
 
+        List<StringParser> parsers = getParserList(typeArray, options);
+
         CopyOnWriteArrayList<ColumnType> typeCandidates = new CopyOnWriteArrayList<>(typeArray);
 
         for (String s : valuesList) {
-            if (isMissing(s, options)) {
-                continue;
-            }
-            if (dateTimeFormatter != null) {
-                if (typeCandidates.contains(LOCAL_DATE_TIME) && !isLocalDateTime(s, dateTimeFormatter)) {
-                    typeCandidates.remove(LOCAL_DATE_TIME);
-                }
-            } else {
-                if (typeCandidates.contains(LOCAL_DATE_TIME) && !isLocalDateTime.test(s, locale)) {
-                    typeCandidates.remove(LOCAL_DATE_TIME);
+            for (StringParser parser : parsers) {
+                if (!parser.canParse(s)) {
+                    typeCandidates.remove(parser.columnType());
                 }
             }
-            if (timeFormatter != null) {
-                if (typeCandidates.contains(LOCAL_TIME) && !isLocalTime(s, options.timeFormatter())) {
-                    typeCandidates.remove(LOCAL_TIME);
-                }
-            } else {
-                if (typeCandidates.contains(LOCAL_TIME) && !isLocalTime.test(s, locale)) {
-                    typeCandidates.remove(LOCAL_TIME);
-                }
-            }
-            if (dateFormatter != null) {
-                if (typeCandidates.contains(LOCAL_DATE) && !isLocalDate(s, options.dateFormatter())) {
-                    typeCandidates.remove(LOCAL_DATE);
-                }
-            } else {
-                if (typeCandidates.contains(LOCAL_DATE) && !isLocalDate.test(s, locale)) {
-                    typeCandidates.remove(LOCAL_DATE);
-                }
-            }
-            if (typeCandidates.contains(BOOLEAN) && !isBoolean.test(s)) {
-                typeCandidates.remove(BOOLEAN);
-            }
-            if (typeCandidates.contains(NUMBER) && !isDouble.test(s)) {
-                typeCandidates.remove(NUMBER);
-            }
         }
         return selectType(typeCandidates);
     }
 
-    /**
-     * Returns true if the given string indicates a missing value
-     *
-     * If the given string is empty, it's missing, otherwise if a missing value indicator is provided and the string
-     * matches, it's missing. If no missing value indicator is provided, a default missing values list is used.
-     */
-    private static boolean isMissing(String s, CsvReadOptions options) {
-        String missingValueIndicator = options.missingValueIndicator();
-        if (options.missingValueIndicator() != null) {
-            return missingValueIndicator.equals(s) || Strings.isNullOrEmpty(s);
-        }
-
-        return Strings.isNullOrEmpty(s)
-                || TypeUtils.MISSING_INDICATORS.contains(s);
-    }
-
     /**
      * Returns the selected candidate for a column of data, by picking the first value in the given list
      *
@@ -629,4 +462,21 @@ public class CsvReader {
             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 static List<StringParser> getParserList(ColumnType[] typeArray, CsvReadOptions options) {
+        // Types to choose from. When more than one would work, we pick the first of the options
+
+        List<StringParser> parsers = new ArrayList<>();
+        for (ColumnType type : typeArray) {
+            parsers.add(type.customParser(options));
+        }
+        return parsers;
+    }
 }
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java b/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java
index 20021eeb..c3aedfcc 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/components/Figure.java
@@ -16,7 +16,7 @@ import java.util.Map;
  * traces (objects that describe a single series of data in a graph like Scatter or Heatmap)
  * and layout attributes that apply to the rest of the chart, like the title, xaxis, or annotations).
  * <p>
- * Figure combines the two parts, associating one or more traces with a layout. If the layout is null a default layout
+ * Figure combines the two parts, associating one or more traces with a layout. If the layout is null a default layuut
  * is provided.
  */
 public class Figure {


Test patch:
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateStringParser.java b/core/src/main/java/tech/tablesaw/columns/dates/DateStringParser.java
new file mode 100644
index 00000000..ddf8bed7
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateStringParser.java
@@ -0,0 +1,111 @@
+package tech.tablesaw.columns.dates;
+
+import com.google.common.collect.Lists;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.StringParser;
+import tech.tablesaw.io.csv.CsvReadOptions;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.util.Locale;
+
+public class DateStringParser extends StringParser<LocalDate> {
+
+    private Locale locale = Locale.getDefault();
+    private DateTimeFormatter formatter = DEFAULT_FORMATTER;
+
+    public DateStringParser(ColumnType type, CsvReadOptions readOptions) {
+        super(type);
+        if (readOptions.dateFormatter() != null) {
+            formatter = readOptions.dateFormatter();
+        }
+        if (readOptions.locale() != null) {
+            locale = readOptions.locale();
+        }
+        if (readOptions.missingValueIndicator() != null) {
+            missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
+        }
+    }
+
+    public DateStringParser(ColumnType type) {
+        super(type);
+    }
+
+    @Override
+    public boolean canParse(String s) {
+        if (isMissing(s)) {
+            return true;
+        }
+        try {
+            LocalDate.parse(s, formatter.withLocale(locale));
+            return true;
+        } catch (DateTimeParseException e) {
+            // it's all part of the plan
+            return false;
+        }
+    }
+
+    public void setCustomFormatter(DateTimeFormatter f) {
+        formatter = f;
+    }
+
+    public void setLocale(Locale locale) {
+        this.locale = locale;
+    }
+
+    @Override
+    public LocalDate parse(String s) {
+        if (isMissing(s)) {
+            return null;
+        }
+        return LocalDate.parse(s, formatter);
+    }
+
+    // Formats that we accept in parsing dates from strings
+    private static final DateTimeFormatter dtf1 = DateTimeFormatter.ofPattern("yyyyMMdd");
+    private static final DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("MM/dd/yyyy");
+    private static final DateTimeFormatter dtf3 = DateTimeFormatter.ofPattern("MM-dd-yyyy");
+    private static final DateTimeFormatter dtf4 = DateTimeFormatter.ofPattern("MM.dd.yyyy");
+    private static final DateTimeFormatter dtf5 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter dtf6 = DateTimeFormatter.ofPattern("yyyy/MM/dd");
+    private static final DateTimeFormatter dtf7 = DateTimeFormatter.ofPattern("dd/MMM/yyyy");
+    private static final DateTimeFormatter dtf8 = DateTimeFormatter.ofPattern("dd-MMM-yyyy");
+    private static final DateTimeFormatter dtf9 = DateTimeFormatter.ofPattern("M/d/yyyy");
+    private static final DateTimeFormatter dtf10 = DateTimeFormatter.ofPattern("M/d/yy");
+    private static final DateTimeFormatter dtf11 = DateTimeFormatter.ofPattern("MMM/dd/yyyy");
+    private static final DateTimeFormatter dtf12 = DateTimeFormatter.ofPattern("MMM-dd-yyyy");
+    private static final DateTimeFormatter dtf13 = DateTimeFormatter.ofPattern("MMM/dd/yy");
+    private static final DateTimeFormatter dtf14 = DateTimeFormatter.ofPattern("MMM-dd-yy");
+    private static final DateTimeFormatter dtf15 = DateTimeFormatter.ofPattern("MMM/dd/yyyy");
+    private static final DateTimeFormatter dtf16 = DateTimeFormatter.ofPattern("MMM/d/yyyy");
+    private static final DateTimeFormatter dtf17 = DateTimeFormatter.ofPattern("MMM-dd-yy");
+    private static final DateTimeFormatter dtf18 = DateTimeFormatter.ofPattern("MMM dd, yyyy");
+    private static final DateTimeFormatter dtf19 = DateTimeFormatter.ofPattern("MMM d, yyyy");
+
+    // A formatter that handles all the date formats defined above
+    private static final DateTimeFormatter DEFAULT_FORMATTER =
+            new DateTimeFormatterBuilder()
+                    .appendOptional(dtf1)
+                    .appendOptional(dtf2)
+                    .appendOptional(dtf3)
+                    .appendOptional(dtf4)
+                    .appendOptional(dtf5)
+                    .appendOptional(dtf6)
+                    .appendOptional(dtf7)
+                    .appendOptional(dtf8)
+                    .appendOptional(dtf9)
+                    .appendOptional(dtf10)
+                    .appendOptional(dtf11)
+                    .appendOptional(dtf12)
+                    .appendOptional(dtf13)
+                    .appendOptional(dtf14)
+                    .appendOptional(dtf15)
+                    .appendOptional(dtf16)
+                    .appendOptional(dtf17)
+                    .appendOptional(dtf18)
+                    .appendOptional(dtf19)
+                    .toFormatter();
+
+}
diff --git a/core/src/test/java/tech/tablesaw/api/DateColumnMapTest.java b/core/src/test/java/tech/tablesaw/api/DateColumnMapTest.java
index 8774b739..d7f194c1 100644
--- a/core/src/test/java/tech/tablesaw/api/DateColumnMapTest.java
+++ b/core/src/test/java/tech/tablesaw/api/DateColumnMapTest.java
@@ -5,11 +5,9 @@ import org.junit.Test;
 
 import java.time.DayOfWeek;
 import java.time.temporal.ChronoUnit;
-import java.util.Locale;
 
-import static tech.tablesaw.columns.dates.PackedLocalDate.asLocalDate;
-import static tech.tablesaw.columns.dates.PackedLocalDate.pack;
 import static org.junit.Assert.assertEquals;
+import static tech.tablesaw.columns.dates.PackedLocalDate.*;
 
 public class DateColumnMapTest {
     private DateColumn column1;
@@ -17,7 +15,7 @@ public class DateColumnMapTest {
     @Before
     public void setUp() {
         Table table = Table.create("Test");
-        column1 = DateColumn.create("Game date", Locale.ENGLISH);
+        column1 = DateColumn.create("Game date");
         table.addColumns(column1);
     }
 
diff --git a/core/src/test/java/tech/tablesaw/api/DateColumnTest.java b/core/src/test/java/tech/tablesaw/api/DateColumnTest.java
index 2b5cde7e..ba5da8cc 100644
--- a/core/src/test/java/tech/tablesaw/api/DateColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/DateColumnTest.java
@@ -2,13 +2,12 @@ package tech.tablesaw.api;
 
 import org.junit.Before;
 import org.junit.Test;
+import tech.tablesaw.columns.dates.DateColumnType;
 
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
-import java.util.Locale;
 import java.util.stream.Collectors;
 
-import static tech.tablesaw.columns.dates.PackedLocalDate.asLocalDate;
 import static org.junit.Assert.assertEquals;
 
 public class DateColumnTest {
@@ -17,7 +16,7 @@ public class DateColumnTest {
     @Before
     public void setUp() {
         Table table = Table.create("Test");
-        column1 = DateColumn.create("Game date", Locale.ENGLISH);
+        column1 = DateColumn.create("Game date");
         table.addColumns(column1);
     }
 
@@ -140,7 +139,7 @@ public class DateColumnTest {
 
         LocalDate actual = column1.min();
 
-        assertEquals(asLocalDate(column1.convert("2013-10-23")), actual);
+        assertEquals(DateColumnType.DEFAULT_PARSER.parse("2013-10-23"), actual);
     }
 
     @Test
diff --git a/core/src/test/java/tech/tablesaw/columns/dates/DateMapFunctionsTest.java b/core/src/test/java/tech/tablesaw/columns/dates/DateMapFunctionsTest.java
index e82f52ef..58e13bf6 100644
--- a/core/src/test/java/tech/tablesaw/columns/dates/DateMapFunctionsTest.java
+++ b/core/src/test/java/tech/tablesaw/columns/dates/DateMapFunctionsTest.java
@@ -22,7 +22,6 @@ import tech.tablesaw.api.Table;
 import tech.tablesaw.api.TimeColumn;
 
 import java.time.LocalTime;
-import java.util.Locale;
 
 import static org.junit.Assert.*;
 
@@ -33,7 +32,7 @@ public class DateMapFunctionsTest {
     @Before
     public void setUp() {
         Table table = Table.create("Test");
-        column1 = DateColumn.create("Game date", Locale.ENGLISH);
+        column1 = DateColumn.create("Game date");
         table.addColumns(column1);
     }
 
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 7874f458..8a006b83 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -263,9 +263,9 @@ public class CsvReaderTest {
                 .build();
 
         Table t = Table.read().csv(options);
-        assertEquals(t.stringColumn(0).countMissing(), 1);
-        assertEquals(t.numberColumn(1).countMissing(), 1);
-        assertEquals(t.numberColumn(2).countMissing(), 1);
+        assertEquals(1, t.stringColumn(0).countMissing());
+        assertEquals(1, t.numberColumn(1).countMissing());
+        assertEquals(1, t.numberColumn(2).countMissing());
     }
 
     @Test