Repository: jtablesaw/tablesaw
Pull request #449 (2019-01-27 23:14:10)

feat(FWF): Add fixed width file IO
Thanks for contributing.

- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description
Parsing Fixed width format with same pattern for all the lines (Done).
Add skip invalid row option (Done).
## Testing

Testing (Partially, need more testing).

Don't hesitate to ping me in order to make the PR code better. 
Thanks


benmccann: The build is failing. Maybe you left out some files? https://travis-ci.org/jtablesaw/tablesaw/builds/485171551?utm_source=github_status&utm_medium=notification

I only glanced quickly, but it looks like there might be some code duplicated between this and the CSVReader. Is there any refactoring that can be done to improve code sharing?

Sparow199: I got some problems with merging current Tablesaw code with my fork version. I agree with that there is a lot of duplicated code between  fwf and csv. 
Please look at @lwhite1 comment about this in this issue => https://github.com/jtablesaw/tablesaw/issues/428
benmccann: I've done most of the refactoring to support new reader types with less duplicated code in https://github.com/jtablesaw/tablesaw/pull/452. It'd make sense to merge that one first and then rebase this PR on top of it
lwhite1: Ben's changes have been merged, if you can rebase these on top we can try to get it in. 
benmccann: You might want to revert your changes to the parsers and column types and then squash your commits before rebasing. I don't think you'll need those changes and that will save you the trouble of having to merge so many files
Sparow199: Hello, I'll do my best this evening. Thanks

On Sat, Feb 9, 2019, 16:10 Ben McCann <notifications@github.com> wrote:

> You might want to revert your changes to the parsers and column types and
> then squash your commits before rebasing. I don't think you'll need those
> changes and that will save you the trouble of having to merge so many files
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/jtablesaw/tablesaw/pull/449#issuecomment-462051966>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AVP80sceygSIGDHRLP6MlbqjPN8OG4akks5vLuTIgaJpZM4aU_w5>
> .
>

Sparow199: We could do better with extracting common utility methods from both Csv and FwF reader classes.
benmccann: It could be nice to drop the files that are only have imports changed from Eclipse to Intellij style to avoid muddying up their commit log
Sparow199: I tried to resolve all the code smells, i'm waiting for your feedbacks. Thanks 
Sparow199: Hello, feel free to change the name of the 3 private functions in FixedWidthWriter class. I was not inspired, it's useful to reduce the complexity of the existing method.

Patch:
diff --git a/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java b/core/src/main/java/tech/tablesaw/io/AddCellToColumnException.java
similarity index 99%
rename from core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
rename to core/src/main/java/tech/tablesaw/io/AddCellToColumnException.java
index 74c5a64a..5029a54e 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
+++ b/core/src/main/java/tech/tablesaw/io/AddCellToColumnException.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package tech.tablesaw.io.csv;
+package tech.tablesaw.io;
 
 import java.io.PrintStream;
 import java.util.List;
diff --git a/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
index 9aa376b2..30619a4b 100644
--- a/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
+++ b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
@@ -1,16 +1,9 @@
 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 com.google.common.collect.Lists;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.AbstractParser;
+import tech.tablesaw.columns.strings.StringColumnType;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -18,11 +11,18 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
-import com.google.common.collect.Lists;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE_TIME;
+import static tech.tablesaw.api.ColumnType.LOCAL_TIME;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE;
+import static tech.tablesaw.api.ColumnType.BOOLEAN;
+import static tech.tablesaw.api.ColumnType.SHORT;
+import static tech.tablesaw.api.ColumnType.INTEGER;
+import static tech.tablesaw.api.ColumnType.LONG;
+import static tech.tablesaw.api.ColumnType.FLOAT;
+import static tech.tablesaw.api.ColumnType.DOUBLE;
+import static tech.tablesaw.api.ColumnType.STRING;
+import static tech.tablesaw.api.ColumnType.TEXT;
 
-import tech.tablesaw.api.ColumnType;
-import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.columns.strings.StringColumnType;
 
 public class ColumnTypeDetector {
 
diff --git a/core/src/main/java/tech/tablesaw/io/DataFrameReader.java b/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
index b2f2ed01..9a227bd0 100644
--- a/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
+++ b/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
@@ -14,25 +14,23 @@
 
 package tech.tablesaw.io;
 
-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.fixed.FixedWidthReadOptions;
+import tech.tablesaw.io.fixed.FixedWidthReader;
 import tech.tablesaw.io.html.HtmlTableReader;
 import tech.tablesaw.io.jdbc.SqlResultSetReader;
 import tech.tablesaw.io.json.JsonReader;
 
+import java.io.*;
+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;
+
 public class DataFrameReader {
 
     public Table csv(String file) throws IOException {
@@ -78,6 +76,38 @@ public class DataFrameReader {
 	return new JsonReader().read(ReadOptions.builder(contents, tableName).build());
     }
 
+    public Table fixedWidth(String file) throws IOException {
+        return fixedWidth(FixedWidthReadOptions.builder(file));
+    }
+
+    public Table fixedWidth(String contents, String tableName) {
+        try {
+            return fixedWidth(new StringReader(contents), tableName);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    public Table fixedWidth(File file) throws IOException {
+        return fixedWidth(FixedWidthReadOptions.builder(file));
+    }
+
+    public Table fixedWidth(InputStream stream, String tableName) throws IOException {
+        return fixedWidth(FixedWidthReadOptions.builder(stream, tableName));
+    }
+
+    public Table fixedWidth(Reader reader, String tableName) throws IOException {
+        return fixedWidth(FixedWidthReadOptions.builder(reader, tableName));
+    }
+
+    public Table fixedWidth(FixedWidthReadOptions.Builder options) throws IOException {
+        return fixedWidth(options.build());
+    }
+
+    public Table fixedWidth(FixedWidthReadOptions options) throws IOException {
+        return new FixedWidthReader().read(options);
+    }
+
     public Table db(ResultSet resultSet, String tableName) throws SQLException {
         return SqlResultSetReader.read(resultSet, tableName);
     }
diff --git a/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java b/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java
index 37359d0c..e52f1737 100644
--- a/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java
+++ b/core/src/main/java/tech/tablesaw/io/DataFrameWriter.java
@@ -14,18 +14,20 @@
 
 package tech.tablesaw.io;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.Writer;
-
 import tech.tablesaw.api.Table;
 import tech.tablesaw.io.csv.CsvWriteOptions;
 import tech.tablesaw.io.csv.CsvWriter;
+import tech.tablesaw.io.fixed.FixedWidthWriteOptions;
+import tech.tablesaw.io.fixed.FixedWidthWriter;
 import tech.tablesaw.io.html.HtmlTableWriter;
 import tech.tablesaw.io.json.JsonWriteOptions;
 import tech.tablesaw.io.json.JsonWriter;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+
 public class DataFrameWriter {
 
     private final Table table;
@@ -58,6 +60,31 @@ public class DataFrameWriter {
         new CsvWriter(table, options).write();
     }
 
+    public void fwf(String file) throws IOException {
+        FixedWidthWriteOptions options = new FixedWidthWriteOptions.Builder(file).build();
+        new FixedWidthWriter(table, options).write();
+    }
+
+    public void fwf(File file) throws IOException {
+        FixedWidthWriteOptions options = new FixedWidthWriteOptions.Builder(file).build();
+        new FixedWidthWriter(table, options).write();
+    }
+
+    public void fwf(FixedWidthWriteOptions options) {
+        new FixedWidthWriter(table, options).write();
+    }
+
+    public void fwf(OutputStream stream) {
+        FixedWidthWriteOptions options = new FixedWidthWriteOptions.Builder(stream).build();
+        new FixedWidthWriter(table, options).write();
+    }
+
+    public void fwf(Writer writer) {
+        FixedWidthWriteOptions options = new FixedWidthWriteOptions.Builder(writer).build();
+        new FixedWidthWriter(table, options).write();
+    }
+
+
     public void html(OutputStream stream) {
         HtmlTableWriter.write(table, stream);
     }
diff --git a/core/src/main/java/tech/tablesaw/io/ReadOptions.java b/core/src/main/java/tech/tablesaw/io/ReadOptions.java
index b3abb94f..a253125f 100644
--- a/core/src/main/java/tech/tablesaw/io/ReadOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/ReadOptions.java
@@ -45,9 +45,9 @@ public class ReadOptions {
         if (builder.inputStream != null) sourceCount++;
 
         if (sourceCount == 0) {
-            throw new IllegalArgumentException("CsvReadOptions Builder configured with no data source");
+            throw new IllegalArgumentException("ReadOptions Builder configured with no data source");
         } else if (sourceCount > 1) {
-            throw new IllegalArgumentException("CsvReadOptions Builder configured with more than one data source");
+            throw new IllegalArgumentException("ReadOptions Builder configured with more than one data source");
         }
 
         file = builder.file;
diff --git a/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java b/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
index 61f8faba..1cb09251 100644
--- a/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
+++ b/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
@@ -1,16 +1,12 @@
 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;
 
+import java.io.*;
+import java.util.Iterator;
+import java.util.List;
+
 public class TableBuildingUtils {
 
     public static Reader createReader(ReadOptions options, byte[] cachedBytes) throws IOException {
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 ac7ec8f2..1ee96d77 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -20,26 +20,21 @@ import com.google.common.io.CharStreams;
 import com.univocity.parsers.csv.CsvFormat;
 import com.univocity.parsers.csv.CsvParser;
 import com.univocity.parsers.csv.CsvParserSettings;
+import org.apache.commons.lang3.tuple.Pair;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.columns.AbstractParser;
 import tech.tablesaw.columns.Column;
+import tech.tablesaw.io.AddCellToColumnException;
 import tech.tablesaw.io.ColumnTypeDetector;
 import tech.tablesaw.io.TableBuildingUtils;
 
 import javax.annotation.concurrent.Immutable;
-
-import org.apache.commons.lang3.tuple.Pair;
-
 import java.io.IOException;
 import java.io.Reader;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
-import static tech.tablesaw.api.ColumnType.*;
+import static tech.tablesaw.api.ColumnType.SKIP;
 
 @Immutable
 public class CsvReader {
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java b/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
index 6e60a8a1..67b9878c 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
@@ -1,11 +1,6 @@
 package tech.tablesaw.io.csv;
 
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
+import java.io.*;
 import java.nio.file.Paths;
 
 public class CsvWriteOptions {
diff --git a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java
new file mode 100644
index 00000000..5f61463e
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReadOptions.java
@@ -0,0 +1,303 @@
+/*
+ * 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.fixed;
+
+
+import com.google.common.base.Strings;
+import com.univocity.parsers.fixed.FixedWidthFields;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.io.ReadOptions;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+public class FixedWidthReadOptions extends ReadOptions {
+
+    // we always have one of these (file, reader, or inputStream)
+    private final ColumnType[] columnTypes;
+    private final boolean header;
+    private final FixedWidthFields columnSpecs;
+    private final String lineEnding;
+    private final char padding;
+    private final char lookupWildcard;
+    private final boolean skipTrailingCharsUntilNewline;
+    private final boolean recordEndsOnNewline;
+    private final boolean skipInvalidRows;
+    private final Integer maxNumberOfColumns;
+
+    private FixedWidthReadOptions(FixedWidthReadOptions.Builder builder) {
+    super(builder);
+
+        columnTypes = builder.columnTypes;
+        header = builder.header;
+        columnSpecs = builder.columnSpecs;
+        padding = builder.padding;
+        lookupWildcard = builder.lookupWildcard;
+        skipTrailingCharsUntilNewline = builder.skipTrailingCharsUntilNewline;
+        recordEndsOnNewline = builder.recordEndsOnNewline;
+        skipInvalidRows = builder.skipInvalidRows;
+        lineEnding = builder.lineEnding;
+        maxNumberOfColumns = builder.maxNumberOfColumns;
+    }
+
+    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 ColumnType[] columnTypes() {
+        return columnTypes;
+    }
+
+    public boolean header() {
+        return header;
+    }
+
+    public FixedWidthFields columnSpecs() {
+        return columnSpecs;
+    }
+
+    public String lineEnding() {
+        return lineEnding;
+    }
+
+    public char padding() {
+        return padding;
+    }
+
+    public char lookupWildcard() {
+        return lookupWildcard;
+    }
+
+    public boolean skipTrailingCharsUntilNewline() {
+        return skipTrailingCharsUntilNewline;
+    }
+
+    public boolean recordEndsOnNewline() {
+        return recordEndsOnNewline;
+    }
+
+    public boolean skipInvalidRows() {
+        return skipInvalidRows;
+    }
+
+    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 Integer maxNumberOfColumns() {
+        return maxNumberOfColumns;
+    }
+
+    public static class Builder extends ReadOptions.Builder {
+
+        private boolean header = true;
+        private FixedWidthFields columnSpecs;
+        private String lineEnding;
+        private char padding = ' ';
+        private char lookupWildcard = '?';
+        private boolean skipTrailingCharsUntilNewline = false;
+        private boolean recordEndsOnNewline = false;
+        private boolean skipInvalidRows = false;
+        private ColumnType[] columnTypes;
+        private Integer maxNumberOfColumns = 10_000;
+
+        public Builder(File file) {
+            super(file);
+        }
+
+        /**
+         * 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) {
+            super(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) {
+            super(stream);
+        }
+
+        public Builder tableName(String tableName) {
+            this.tableName = tableName;
+            return this;
+        }
+
+        public Builder header(boolean header) {
+            this.header = header;
+            return this;
+        }
+
+        public Builder missingValueIndicator(String missingValueIndicator) {
+            this.missingValueIndicator = missingValueIndicator;
+            return this;
+        }
+
+        public Builder columnSpecs(FixedWidthFields columnSpecs) {
+            this.columnSpecs = columnSpecs;
+            return this;
+        }
+
+
+        public Builder lineEnding(String lineEnding) {
+            this.lineEnding = lineEnding;
+            return this;
+        }
+
+        public Builder padding(char padding) {
+            this.padding = padding;
+            return this;
+        }
+
+        public Builder lookupWildcard(char lookupWildcard) {
+            this.lookupWildcard = lookupWildcard;
+            return this;
+        }
+
+        public Builder skipTrailingCharsUntilNewline(boolean skipTrailingCharsUntilNewline) {
+            this.skipTrailingCharsUntilNewline = skipTrailingCharsUntilNewline;
+            return this;
+        }
+
+        public Builder recordEndsOnNewline(boolean recordEndsOnNewline) {
+            this.recordEndsOnNewline = recordEndsOnNewline;
+            return this;
+        }
+
+        public Builder skipInvalidRows(boolean skipInvalidRows) {
+            this.skipInvalidRows = skipInvalidRows;
+            return this;
+        }
+
+        public Builder sample(boolean sample) {
+            this.sample = sample;
+            return this;
+        }
+
+        public Builder locale(Locale locale) {
+            this.locale = locale;
+            return this;
+        }
+
+        public Builder columnTypes(ColumnType[] columnTypes) {
+            this.columnTypes = columnTypes;
+            return this;
+        }
+
+        /**
+         * Defines maximal value of columns in fixed-width file.
+         *
+         * @param maxNumberOfColumns - must be positive integer. Default is 512.         *
+         */
+        public Builder maxNumberOfColumns(Integer maxNumberOfColumns) {
+            this.maxNumberOfColumns = maxNumberOfColumns;
+            return this;
+        }
+
+        public FixedWidthReadOptions build() {
+            return new FixedWidthReadOptions(this);
+        }
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
new file mode 100644
index 00000000..4d22f5db
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthReader.java
@@ -0,0 +1,361 @@
+/*
+ * 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.fixed;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.io.CharStreams;
+import com.univocity.parsers.fixed.FixedWidthFormat;
+import com.univocity.parsers.fixed.FixedWidthParser;
+import com.univocity.parsers.fixed.FixedWidthParserSettings;
+import org.apache.commons.lang3.tuple.Pair;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.columns.AbstractParser;
+import tech.tablesaw.columns.Column;
+import tech.tablesaw.io.AddCellToColumnException;
+import tech.tablesaw.io.ColumnTypeDetector;
+import tech.tablesaw.io.TableBuildingUtils;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.*;
+import java.util.*;
+
+import static tech.tablesaw.api.ColumnType.SKIP;
+
+@Immutable
+public class FixedWidthReader {
+
+    private List<ColumnType> typeArrayOverrides = null;
+
+    /**
+     * Constructs a FixedWidthReader
+     */
+    public FixedWidthReader() {
+    }
+
+    /**
+     * Constructs a FixedWidthReader with the given list of ColumnTypes
+     * <p>
+     * These are the only types that the FixedWidthReader can detect and parse
+     */
+    public FixedWidthReader(List<ColumnType> typeDetectionList) {
+        this.typeArrayOverrides = typeDetectionList;
+    }
+
+    /**
+     * Determines column types if not provided by the user
+     * Reads all input into memory unless File was provided
+     */
+    private Pair<Reader, ColumnType[]> getReaderAndColumnTypes(FixedWidthReadOptions options) throws IOException {
+        ColumnType[] types = options.columnTypes();
+        byte[] bytesCache = null;
+
+        if (types == null) {
+            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);
+            }
+            types = detectColumnTypes(reader, options);
+        }
+
+        return Pair.of(TableBuildingUtils.createReader(options, bytesCache), types);
+    }
+
+    public Table read(FixedWidthReadOptions options) throws IOException {
+        return read(options, false);
+    }
+
+    private Table read(FixedWidthReadOptions options, boolean headerOnly) throws IOException {
+        Pair<Reader, ColumnType[]> pair = getReaderAndColumnTypes(options);
+        Reader reader = pair.getLeft();
+        ColumnType[] types = pair.getRight();
+
+        FixedWidthParser parser = fixedWidthParser(options);
+
+        try {
+            parser.beginParsing(reader);
+            Table table = Table.create(options.tableName());
+
+            List<String> headerRow = Lists.newArrayList(getHeaderNames(options, types, parser));
+
+            for (int x = 0; x < types.length; x++) {
+                if (types[x] != SKIP) {
+                    String columnName = cleanName(headerRow.get(x));
+                    if (Strings.isNullOrEmpty(columnName)) {
+                        columnName = "Column " + table.columnCount();
+                    }
+                    Column<?> newColumn = types[x].create(columnName);
+                    table.addColumns(newColumn);
+                }
+            }
+
+            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);
+            }
+
+            return table;
+        } finally {
+            if (options.reader() == null) {
+                // if we get a reader back from options it means the client opened it, so let the client close it
+                // if it's null, we close it here.
+                parser.stopParsing();
+                reader.close();
+            }
+        }
+    }
+
+    private String[] getHeaderNames(FixedWidthReadOptions options, ColumnType[] types, FixedWidthParser parser) {
+        if (options.header()) {
+            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 {
+            // 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;
+        }
+    }
+
+    private void addRows(FixedWidthReadOptions options, ColumnType[] types, FixedWidthParser reader, Table table, int[] columnIndexes) {
+        String[] nextLine;
+
+        Map<String, AbstractParser<?>> parserMap = getParserMap(options, table);
+
+        // Add the rows
+        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 Fixed Width file. Row "
+                            + rowNumber
+                            + " is empty. Continuing.");
+                    continue;
+                } else {
+                    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 + " 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++;
+            }
+        }
+    }
+
+    private Map<String, AbstractParser<?>> getParserMap(FixedWidthReadOptions options, Table table) {
+        Map<String, AbstractParser<?>> parserMap = new HashMap<>();
+        for (Column<?> column : table.columns()) {
+            AbstractParser<?> parser = column.type().customParser(options);
+            parserMap.put(column.name(), parser);
+        }
+        return parserMap;
+    }
+
+    private String cleanName(String name) {
+        return name.trim();
+    }
+
+    /**
+     * Returns a string representation of the column types in file {@code fixed widthFilename},
+     * as determined by the type-detection algorithm
+     * <p>
+     * This method is intended to help analysts quickly fix any erroneous types, by printing out the types in a format
+     * such that they can be edited to correct any mistakes, and used in an array literal
+     * <p>
+     * For example:
+     * <p>
+     * LOCAL_DATE, // 0     date
+     * SHORT,      // 1     approval
+     * STRING,     // 2     who
+     * <p>
+     * Note that the types are array separated, and that the index position and the column name are printed such that
+     * they would be interpreted as comments if you paste the output into an array:
+     * <p>
+     *
+     * @throws IOException if file cannot be read
+     */
+    public String printColumnTypes(FixedWidthReadOptions options) throws IOException {
+
+        Table structure = read(options, true).structure();
+
+        StringBuilder buf = new StringBuilder();
+        buf.append("ColumnType[] columnTypes = {");
+        buf.append(System.lineSeparator());
+
+        Column<?> typeCol = structure.column("Column Type");
+        Column<?> indxCol = structure.column("Index");
+        Column<?> nameCol = structure.column("Column Name");
+
+        // add the column headers
+        int typeColIndex = structure.columnIndex(typeCol);
+        int indxColIndex = structure.columnIndex(indxCol);
+        int nameColIndex = structure.columnIndex(nameCol);
+
+        int typeColWidth = typeCol.columnWidth();
+        int indxColWidth = indxCol.columnWidth();
+        int nameColWidth = nameCol.columnWidth();
+
+        final char padChar = ' ';
+        for (int r = 0; r < structure.rowCount(); r++) {
+            String cell = Strings.padEnd(structure.get(r, typeColIndex) + ",", typeColWidth, padChar);
+            buf.append(cell);
+            buf.append(" // ");
+
+            cell = Strings.padEnd(structure.getUnformatted(r, indxColIndex), indxColWidth, padChar);
+            buf.append(cell);
+            buf.append(' ');
+
+            cell = Strings.padEnd(structure.getUnformatted(r, nameColIndex), nameColWidth, padChar);
+            buf.append(cell);
+            buf.append(' ');
+
+            buf.append(System.lineSeparator());
+        }
+        buf.append("}");
+        buf.append(System.lineSeparator());
+        return buf.toString();
+    }
+
+    /**
+     * Reads column names from header, skipping any for which the type == SKIP
+     */
+    private String[] selectColumnNames(List<String> names, ColumnType[] types) {
+        List<String> header = new ArrayList<>();
+        for (int i = 0; i < types.length; i++) {
+            if (types[i] != SKIP) {
+                String name = names.get(i);
+                name = name.trim();
+                header.add(name);
+            }
+        }
+        String[] result = new String[header.size()];
+        return header.toArray(result);
+    }
+
+    /**
+     * Estimates and returns the type for each column in the delimited text file {@code file}
+     * <p>
+     * The type is determined by checking a sample of the data in the file. 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(Reader reader, FixedWidthReadOptions options) throws IOException {
+
+        boolean header = options.header();
+        int linesToSkip = header ? 1 : 0;
+
+        FixedWidthParser fixedWidthParser = fixedWidthParser(options);
+
+        try {
+            fixedWidthParser.beginParsing(reader);
+
+            for (int i = 0; i < linesToSkip; i++) {
+                fixedWidthParser.parseNext();
+            }
+
+            ColumnTypeDetector detector = typeArrayOverrides == null
+                    ? new ColumnTypeDetector() : new ColumnTypeDetector(typeArrayOverrides);
+            return detector.detectColumnTypes(new Iterator<String[]>() {
+
+                String[] nextRow = fixedWidthParser.parseNext();
+
+                @Override
+                public boolean hasNext() {
+                    return nextRow != null;
+                }
+
+                @Override
+                public String[] next() {
+                    String[] tmp = nextRow;
+                    nextRow = fixedWidthParser.parseNext();
+                    return tmp;
+                }
+
+            }, options);
+        } finally {
+            fixedWidthParser.stopParsing();
+            // we don't close the reader since we didn't create it
+        }
+    }
+
+    private FixedWidthParser fixedWidthParser(FixedWidthReadOptions options) {
+        FixedWidthParserSettings settings = new FixedWidthParserSettings();
+
+        if (options.columnSpecs() != null) {
+            settings = new FixedWidthParserSettings(options.columnSpecs());
+        }
+        settings.setFormat(fixedWidthFormat(options));
+        if (options.skipTrailingCharsUntilNewline()) {
+            settings.setSkipTrailingCharsUntilNewline(options.skipTrailingCharsUntilNewline());
+        }
+        if (options.maxNumberOfColumns() != null) {
+            settings.setMaxColumns(options.maxNumberOfColumns());
+        }
+        if (options.recordEndsOnNewline()) {
+            settings.setRecordEndsOnNewline(true);
+        }
+        return new FixedWidthParser(settings);
+    }
+
+    private FixedWidthFormat fixedWidthFormat(FixedWidthReadOptions options) {
+        FixedWidthFormat format = new FixedWidthFormat();
+        if (options.padding() != ' ') {
+            format.setPadding(options.padding());
+        }
+        if (options.lookupWildcard() != '?') {
+            format.setLookupWildcard(options.lookupWildcard());
+        }
+        if (options.lineEnding() != null) {
+            format.setLineSeparator(options.lineEnding());
+        }
+        return format;
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthWriteOptions.java b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthWriteOptions.java
new file mode 100644
index 00000000..d33810db
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthWriteOptions.java
@@ -0,0 +1,306 @@
+package tech.tablesaw.io.fixed;
+
+import com.univocity.parsers.fixed.FieldAlignment;
+import com.univocity.parsers.fixed.FixedWidthFields;
+
+import java.io.*;
+import java.nio.file.Paths;
+
+public class FixedWidthWriteOptions {
+
+    private final Writer writer;
+    private final boolean header;
+    private final char[] lineSeparator;
+    private final String lineSeparatorString;
+    private final FixedWidthFields columnSpecs;
+    private final boolean defaultPaddingForHeaders;
+    private final boolean skipBitsAsWhitespace;
+    private final FieldAlignment defaultAlignmentForHeaders;
+    private final boolean skipEmptyLines;
+    private final boolean expandIncompleteRows;
+    private final boolean autoConfigurationEnabled;
+    private final int errorContentLength;
+    private final boolean writeLineSeparatorAfterRecord;
+    private final boolean ignoreTrailingWhitespaces;
+    private final boolean ignoreLeadingWhitespaces;
+    private final boolean columnReorderingEnabled;
+    private final String emptyValue;
+    private final String nullValue;
+    private final char comment;
+    private final char padding;
+    private final char lookupWildcard;
+    private final char normalizedNewline;
+
+    private FixedWidthWriteOptions(Builder builder) {
+        this.writer = builder.writer;
+        this.header = builder.header;
+        this.lineSeparator = builder.lineSeparator;
+        this.lineSeparatorString = builder.lineSeparatorString;
+        this.columnSpecs = builder.columnSpecs;
+        this.defaultAlignmentForHeaders = builder.defaultAlignmentForHeaders;
+        this.defaultPaddingForHeaders = builder.defaultPaddingForHeaders;
+        this.skipBitsAsWhitespace = builder.skipBitsAsWhitespace;
+        this.skipEmptyLines = builder.skipEmptyLines;
+        this.expandIncompleteRows = builder.expandIncompleteRows;
+        this.autoConfigurationEnabled = builder.autoConfigurationEnabled;
+        this.writeLineSeparatorAfterRecord = builder.writeLineSeparatorAfterRecord;
+        this.ignoreTrailingWhitespaces = builder.ignoreTrailingWhitespaces;
+        this.ignoreLeadingWhitespaces = builder.ignoreLeadingWhitespaces;
+        this.columnReorderingEnabled = builder.columnReorderingEnabled;
+        this.errorContentLength = builder.errorContentLength;
+        this.emptyValue = builder.emptyValue;
+        this.nullValue = builder.nullValue;
+        this.comment = builder.comment;
+        this.padding = builder.padding;
+        this.lookupWildcard = builder.lookupWildcard;
+        this.normalizedNewline = builder.normalizedNewline;
+    }
+
+    public Writer writer() {
+        return writer;
+    }
+
+    public boolean header() {
+        return header;
+    }
+
+    public char[] lineSeparator() {
+        return lineSeparator;
+    }
+    public String lineSeparatorString() {
+        return lineSeparatorString;
+    }
+
+    public FixedWidthFields columnSpecs() {
+        return columnSpecs;
+    }
+
+    public boolean defaultPaddingForHeaders() {
+        return defaultPaddingForHeaders;
+    }
+
+    public boolean skipBitsAsWhitespace() {
+        return skipBitsAsWhitespace;
+    }
+
+    public FieldAlignment defaultAlignmentForHeaders() {
+        return defaultAlignmentForHeaders;
+    }
+
+    public boolean skipEmptyLines() {
+        return skipEmptyLines;
+    }
+
+    public boolean expandIncompleteRows() {
+        return expandIncompleteRows;
+    }
+
+    public boolean autoConfigurationEnabled() {
+        return autoConfigurationEnabled;
+    }
+
+    public int errorContentLength() {
+        return errorContentLength;
+    }
+
+    public boolean writeLineSeparatorAfterRecord() {
+        return writeLineSeparatorAfterRecord;
+    }
+
+    public boolean ignoreTrailingWhitespaces() {
+        return ignoreTrailingWhitespaces;
+    }
+
+    public boolean ignoreLeadingWhitespaces() {
+        return ignoreLeadingWhitespaces;
+    }
+
+    public boolean columnReorderingEnabled() {
+        return columnReorderingEnabled;
+    }
+
+    public String emptyValue() {
+        return emptyValue;
+    }
+
+    public String nullValue() {
+        return nullValue;
+    }
+
+    public char comment() {
+        return comment;
+    }
+
+    public char padding() {
+        return padding;
+    }
+
+    public char lookupWildcard() {
+        return lookupWildcard;
+    }
+
+    public char normalizedNewline() {
+        return normalizedNewline;
+    }
+
+    public static Builder builder(File file) throws IOException {
+        return new Builder(file);
+    }
+
+    public static Builder builder(String fileName) throws IOException {
+        return builder(new File(fileName));
+    }
+
+    public static class Builder {
+
+        private Writer writer;
+        private boolean header = true;
+        private char[] lineSeparator;
+        private String lineSeparatorString;
+        private FixedWidthFields columnSpecs;
+        private boolean defaultPaddingForHeaders = true;
+        private boolean skipBitsAsWhitespace = true;
+        private FieldAlignment defaultAlignmentForHeaders;
+        private boolean skipEmptyLines = true;
+        private boolean expandIncompleteRows = false;
+        private boolean autoConfigurationEnabled = true;
+        private int errorContentLength = -1;
+        private boolean writeLineSeparatorAfterRecord = true;
+        private boolean ignoreTrailingWhitespaces = true;
+        private boolean ignoreLeadingWhitespaces = true;
+        private boolean columnReorderingEnabled = true;
+        private String emptyValue;
+        private String nullValue;
+        private char comment = '#';
+        private char padding = '_';
+        private char lookupWildcard = '?';
+        private char normalizedNewline = '\n';
+
+
+        public Builder(String fileName) throws IOException {
+            File file = Paths.get(fileName).toFile();
+            this.writer = new FileWriter(file);
+        }
+
+        public Builder(File file) throws IOException {
+            this.writer = new FileWriter(file);
+        }
+
+        public Builder(Writer writer) {
+            this.writer = writer;
+        }
+
+        public Builder(OutputStream stream) {
+            this.writer = new OutputStreamWriter(stream);
+        }
+
+        public FixedWidthWriteOptions.Builder lineSeparator(char[] lineSeparator) {
+            this.lineSeparator = lineSeparator;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder lineSeparatorString(String lineSeparatorString) {
+            this.lineSeparatorString = lineSeparatorString;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder header(boolean header) {
+            this.header = header;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder header(FixedWidthFields columnSpecs) {
+            this.columnSpecs = columnSpecs;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder defaultPaddingForHeaders(boolean defaultPaddingForHeaders) {
+            this.defaultPaddingForHeaders = defaultPaddingForHeaders;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder skipBitsAsWhitespace(boolean skipBitsAsWhitespace) {
+            this.skipBitsAsWhitespace = skipBitsAsWhitespace;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder defaultAlignmentForHeaders(FieldAlignment defaultAlignmentForHeaders) {
+            this.defaultAlignmentForHeaders = defaultAlignmentForHeaders;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder skipEmptyLines(boolean skipEmptyLines) {
+            this.skipEmptyLines = skipEmptyLines;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder expandIncompleteRows(boolean expandIncompleteRows) {
+            this.expandIncompleteRows = expandIncompleteRows;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder autoConfigurationEnabled(boolean autoConfigurationEnabled) {
+            this.autoConfigurationEnabled = autoConfigurationEnabled;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder errorContentLength(int errorContentLength) {
+            this.errorContentLength = errorContentLength;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder writeLineSeparatorAfterRecord(boolean writeLineSeparatorAfterRecord) {
+            this.writeLineSeparatorAfterRecord = writeLineSeparatorAfterRecord;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder ignoreTrailingWhitespaces(boolean ignoreTrailingWhitespaces) {
+            this.ignoreTrailingWhitespaces = ignoreTrailingWhitespaces;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder ignoreLeadingWhitespaces(boolean ignoreLeadingWhitespaces) {
+            this.ignoreLeadingWhitespaces = ignoreLeadingWhitespaces;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder columnReorderingEnabled(boolean columnReorderingEnabled) {
+            this.columnReorderingEnabled = columnReorderingEnabled;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder emptyValue(String emptyValue) {
+            this.emptyValue = emptyValue;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder nullValue(String nullValue) {
+            this.nullValue = nullValue;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder comment(char comment) {
+            this.comment = comment;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder padding(char padding) {
+            this.padding = padding;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder lookupWildcard(char lookupWildcard) {
+            this.lookupWildcard = lookupWildcard;
+            return this;
+        }
+
+        public FixedWidthWriteOptions.Builder normalizedNewline(char normalizedNewline) {
+            this.normalizedNewline = normalizedNewline;
+            return this;
+        }
+
+        public FixedWidthWriteOptions build() {
+            return new FixedWidthWriteOptions(this);
+        }
+    }
+}
diff --git a/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthWriter.java b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthWriter.java
new file mode 100644
index 00000000..05b7cff8
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/fixed/FixedWidthWriter.java
@@ -0,0 +1,178 @@
+/*
+ * 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.fixed;
+
+import com.univocity.parsers.fixed.FixedWidthFormat;
+import com.univocity.parsers.fixed.FixedWidthWriterSettings;
+import tech.tablesaw.api.Table;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Writer;
+
+/**
+ * Class that writes tables and individual columns to FixedWidth files
+ */
+@Immutable
+final public class FixedWidthWriter {
+
+    private final Table table;
+    private final boolean header;
+    private final Writer writer;
+    private final FixedWidthWriterSettings settings;
+    private final FixedWidthFormat format;
+
+    /**
+     * Private constructor to prevent instantiation
+     */
+    public FixedWidthWriter(Table table, FixedWidthWriteOptions options) {
+        this.table = table;
+        this.header = options.header();
+        this.writer = options.writer();
+        this.settings = fixedWidthWriterSettings(options);
+        this.format = fixedWidthFormat(options);
+        settings.setFormat(format);
+    }
+
+    public void write() {
+
+        com.univocity.parsers.fixed.FixedWidthWriter fixedWidthWriter = null;
+        // Creates a writer with the above settings;
+        try {
+
+            fixedWidthWriter = new com.univocity.parsers.fixed.FixedWidthWriter(writer, settings);
+            if (header) {
+                String[] header = new String[table.columnCount()];
+                for (int c = 0; c < table.columnCount(); c++) {
+                    header[c] = table.column(c).name();
+                }
+                fixedWidthWriter.writeHeaders(header);
+            }
+            for (int r = 0; r < table.rowCount(); r++) {
+                String[] entries = new String[table.columnCount()];
+                for (int c = 0; c < table.columnCount(); c++) {
+                    table.get(r, c);
+                    entries[c] = table.getUnformatted(r, c);
+                }
+                fixedWidthWriter.writeRow(entries);
+            }
+        } finally {
+            if (fixedWidthWriter != null) {
+                fixedWidthWriter.flush();
+                fixedWidthWriter.close();
+            }
+        }
+    }
+
+    public Table getTable() {
+        return table;
+    }
+
+    public boolean getHeader() {
+        return header;
+    }
+
+    public FixedWidthWriterSettings getSettings() {
+        return settings;
+    }
+
+    public FixedWidthFormat getFormat() {
+        return format;
+    }
+
+
+    private FixedWidthFormat fixedWidthFormat(FixedWidthWriteOptions options) {
+        FixedWidthFormat format = new FixedWidthFormat();
+
+        if (options.padding() != ' ') {
+            format.setPadding(options.padding());
+        }
+        if (options.lookupWildcard() != '?') {
+            format.setLookupWildcard(options.lookupWildcard());
+        }
+        if (options.comment() != '#') {
+            format.setComment(options.comment());
+        }
+        if (options.lineSeparator() != null) {
+            format.setLineSeparator(options.lineSeparator());
+        }
+        if (options.lineSeparatorString() != null) {
+            format.setLineSeparator(options.lineSeparatorString());
+        }
+        if (options.normalizedNewline() != '\n') {
+            format.setNormalizedNewline(options.normalizedNewline());
+        }
+
+        return format;
+    }
+
+    private FixedWidthWriterSettings fixedWidthWriterSettings(FixedWidthWriteOptions options) {
+        FixedWidthWriterSettings settings = new FixedWidthWriterSettings();
+
+        if (options.autoConfigurationEnabled()) {
+            settings.setAutoConfigurationEnabled(options.autoConfigurationEnabled());
+        } else {
+            columnRowSettings(settings, options);
+            errorSettings(settings, options);
+            skipIgnoreSettings(settings, options);
+        }
+        return settings;
+    }
+
+    private void columnRowSettings(FixedWidthWriterSettings settings, FixedWidthWriteOptions options) {
+        if (options.defaultAlignmentForHeaders() != null) {
+            settings.setDefaultAlignmentForHeaders(options.defaultAlignmentForHeaders());
+        }
+        if (options.columnReorderingEnabled()) {
+            settings.setColumnReorderingEnabled(options.columnReorderingEnabled());
+        }
+        if (options.expandIncompleteRows()) {
+            settings.setExpandIncompleteRows(options.expandIncompleteRows());
+        }
+        if (!options.defaultPaddingForHeaders()) {
+            settings.setUseDefaultPaddingForHeaders(options.defaultPaddingForHeaders());
+        }
+        if (!options.writeLineSeparatorAfterRecord()) {
+            settings.setWriteLineSeparatorAfterRecord(options.writeLineSeparatorAfterRecord());
+        }
+    }
+
+    private void errorSettings(FixedWidthWriterSettings settings, FixedWidthWriteOptions options) {
+        if (options.errorContentLength() <= -1) {
+            settings.setErrorContentLength(options.errorContentLength());
+        }
+        if (options.nullValue() != null) {
+            settings.setNullValue(options.nullValue());
+        }
+        if (options.emptyValue() != null) {
+            settings.setEmptyValue(options.emptyValue());
+        }
+    }
+
+    private void skipIgnoreSettings(FixedWidthWriterSettings settings, FixedWidthWriteOptions options) {
+        if (!options.ignoreTrailingWhitespaces()) {
+            settings.setIgnoreTrailingWhitespaces(options.ignoreTrailingWhitespaces());
+        }
+        if (!options.ignoreLeadingWhitespaces()) {
+            settings.setIgnoreLeadingWhitespaces(options.ignoreLeadingWhitespaces());
+        }
+        if (!options.skipBitsAsWhitespace()) {
+            settings.setSkipBitsAsWhitespace(options.skipBitsAsWhitespace());
+        }
+        if (!options.skipEmptyLines()) {
+            settings.setSkipEmptyLines(options.skipEmptyLines());
+        }
+    }
+}
+
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 63b592a6..84a49590 100644
--- a/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
+++ b/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
@@ -1,13 +1,12 @@
 package tech.tablesaw.io.html;
 
-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 tech.tablesaw.api.Table;
+import tech.tablesaw.io.ReadOptions;
+import tech.tablesaw.io.TableBuildingUtils;
 
 import java.io.IOException;
 import java.io.StringReader;
diff --git a/core/src/main/java/tech/tablesaw/io/html/HtmlTableWriter.java b/core/src/main/java/tech/tablesaw/io/html/HtmlTableWriter.java
index 6a25bcab..fb912045 100644
--- a/core/src/main/java/tech/tablesaw/io/html/HtmlTableWriter.java
+++ b/core/src/main/java/tech/tablesaw/io/html/HtmlTableWriter.java
@@ -22,7 +22,8 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.List;
 
-import static tech.tablesaw.io.ParsingUtils.*;
+import static tech.tablesaw.io.ParsingUtils.splitCamelCase;
+import static tech.tablesaw.io.ParsingUtils.splitOnUnderscore;
 
 /**
  * Static utility that writes Tables in HTML format for display
diff --git a/core/src/main/java/tech/tablesaw/io/json/JsonReader.java b/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
index dc7f594d..40fb673e 100644
--- a/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
+++ b/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
@@ -1,20 +1,15 @@
 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;
 
+import java.io.IOException;
+import java.util.*;
+
 public class JsonReader {
 
     private static final ObjectMapper mapper = new ObjectMapper();
diff --git a/core/src/main/java/tech/tablesaw/io/json/JsonWriter.java b/core/src/main/java/tech/tablesaw/io/json/JsonWriter.java
index cdc59ee6..d2927c79 100644
--- a/core/src/main/java/tech/tablesaw/io/json/JsonWriter.java
+++ b/core/src/main/java/tech/tablesaw/io/json/JsonWriter.java
@@ -14,16 +14,15 @@
 
 package tech.tablesaw.io.json;
 
-import javax.annotation.concurrent.Immutable;
-
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-
 import tech.tablesaw.api.Table;
 
+import javax.annotation.concurrent.Immutable;
+
 @Immutable
 final public class JsonWriter {
 
diff --git a/data/fixed_width_missing_values.txt b/data/fixed_width_missing_values.txt
new file mode 100644
index 00000000..67e1d69a
--- /dev/null
+++ b/data/fixed_width_missing_values.txt
@@ -0,0 +1,5 @@
+YearMake_Model___________________________________Description_____________________________Price___
+nullFord_null____________________________________ac, abs, moon___________________________3000.00_
+1999null_Venture "Extended Edition"______________________________________________________4900.00_
+1999ChevyVenture "Extended Edition, Very Large"__________________________________________5000.00_
+_________Venture "Extended Edition"______________________________________________________4900.00_
\ No newline at end of file
diff --git a/data/fixed_width_wrong_line_length.txt b/data/fixed_width_wrong_line_length.txt
new file mode 100644
index 00000000..daa37337
--- /dev/null
+++ b/data/fixed_width_wrong_line_length.txt
@@ -0,0 +1,7 @@
+YearMake_Model___________________________________Description_____________________________Price___
+1997Ford_E350____________________________________ac, abs, moon___________________________3000.00_ignored_part
+1999ChevyVenture "Extended Edition"______________________________________________________4900.00_ignored_part
+1996Jeep_Grand Cherokee__________________________MUST SELL!
+air, moon roof, loaded_______4799.00_
+1999ChevyVenture "Extended Edition, Very Large"__________________________________________5000.00_ignored_part
+_________Venture "Extended Edition"______________________________________________________4900.00_ignored_part
\ No newline at end of file
diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md
index 147c8408..8ae0f75b 100644
--- a/docs/gettingstarted.md
+++ b/docs/gettingstarted.md
@@ -182,7 +182,7 @@ You can create a table in code. Here we create a table and add two new columns t
 String[] animals = {"bear", "cat", "giraffe"};
 double[] cuteness = {90.1, 84.3, 99.7};
 
-Table cuteAnimals = Table.create("Cute Animals)
+Table cuteAnimals = Table.create("Cute Animals")
 	.addColumns(
 		StringColumn.create("Animal types", animals),
 		DoubleColumn.create("rating", cuteness));


Test patch:
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 f27e3888..c35abc15 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -24,6 +24,7 @@ import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.api.LongColumn;
 import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.api.Table;
+import tech.tablesaw.io.AddCellToColumnException;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java b/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java
new file mode 100644
index 00000000..a6ecef16
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthReaderTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.fixed;
+
+import com.univocity.parsers.fixed.FixedWidthFields;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.Table;
+
+import java.io.*;
+import java.util.Locale;
+
+import static tech.tablesaw.api.ColumnType.SHORT;
+import static tech.tablesaw.api.ColumnType.STRING;
+import static tech.tablesaw.api.ColumnType.FLOAT;
+import static tech.tablesaw.api.ColumnType.SKIP;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for CSV Reading
+ */
+public class FixedWidthReaderTest {
+
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    private final FixedWidthFields car_fields_specs = new FixedWidthFields(4, 5, 40, 40, 8);
+    private final ColumnType[] car_types = {SHORT, STRING, STRING, STRING, FLOAT};
+    private final ColumnType[] car_types_with_SKIP = {SHORT, STRING, STRING, SKIP, FLOAT};
+
+    @Test
+    public void testWithCarsData() throws Exception {
+
+        Table table = Table.read().fixedWidth(FixedWidthReadOptions
+                .builder("../data/fixed_width_cars_test.txt")
+                .header(true)
+                .columnTypes(car_types)
+                .columnSpecs(car_fields_specs)
+                .padding('_')
+                .lineEnding("\n")
+                .build()
+        );
+
+        assertEquals("[Year, Make, Model, Description, Price]", table.columnNames().toString());
+
+        table = table.sortDescendingOn("Year");
+        table.removeColumns("Description");
+
+        assertEquals("[Year, Make, Model, Price]", table.columnNames().toString());
+
+    }
+
+    @Test
+    public void testWithColumnSKIP() throws Exception {
+
+        Table table = Table.read().fixedWidth(FixedWidthReadOptions
+                .builder("../data/fixed_width_cars_test.txt")
+                .header(true)
+                .columnTypes(car_types_with_SKIP)
+                .columnSpecs(car_fields_specs)
+                .padding('_')
+                .lineEnding("\n")
+                .build());
+
+        assertEquals(4, table.columnCount());
+
+        assertEquals("[Year, Make, Model, Price]", table.columnNames().toString());
+    }
+
+    @Test
+    public void testWithColumnSKIPWithoutHeader() throws Exception {
+
+        Table table = Table.read().fixedWidth(FixedWidthReadOptions
+                .builder("../data/fixed_width_cars_no_header_test.txt")
+                .header(false)
+                .columnTypes(car_types_with_SKIP)
+                .columnSpecs(car_fields_specs)
+                .padding('_')
+                .lineEnding("\n")
+                .skipTrailingCharsUntilNewline(true)
+                .build());
+
+
+        assertEquals(4, table.columnCount());
+
+        assertEquals("[C0, C1, C2, C4]", table.columnNames().toString());
+    }
+
+    @Test
+    public void testDataTypeDetection() throws Exception {
+
+        InputStream stream = new FileInputStream(new File("../data/fixed_width_cars_test.txt"));
+        FixedWidthReadOptions options = FixedWidthReadOptions.builder(stream, "")
+                .header(true)
+                .columnSpecs(car_fields_specs)
+                .padding('_')
+                .lineEnding("\n")
+                .sample(false)
+                .locale(Locale.getDefault())
+                .build();
+
+        Reader reader = new FileReader("../data/fixed_width_missing_values.txt");
+        ColumnType[] columnTypes = new FixedWidthReader().detectColumnTypes(reader, options);
+        assertArrayEquals(car_types, columnTypes);
+    }
+
+    @Test
+    public void testWithMissingValue() throws Exception {
+
+        Reader reader = new FileReader("../data/fixed_width_missing_values.txt");
+        FixedWidthReadOptions options = FixedWidthReadOptions.builder(reader, "")
+                .header(true)
+                .columnSpecs(car_fields_specs)
+                .padding('_')
+                .lineEnding("\n")
+                .missingValueIndicator("null")
+                .sample(false)
+                .build();
+
+        Table t = Table.read().fixedWidth(options);
+
+        assertEquals(2, t.shortColumn(0).countMissing());
+        assertEquals(2, t.stringColumn(1).countMissing());
+        assertEquals(1, t.stringColumn(2).countMissing());
+        assertEquals(3, t.stringColumn(3).countMissing());
+    }
+
+
+    @Test
+    public void testWithSkipTrailingCharsUntilNewline() throws Exception {
+
+        Table table = Table.read().fixedWidth(FixedWidthReadOptions
+                .builder("../data/fixed_width_wrong_line_length.txt")
+                .header(true)
+                .columnTypes(car_types)
+                .columnSpecs(car_fields_specs)
+                .padding('_')
+                .lineEnding("\n")
+                .skipTrailingCharsUntilNewline(true)
+                .build()
+        );
+
+        assertEquals("[Year, Make, Model, Description, Price]", table.columnNames().toString());
+
+        table = table.sortDescendingOn("Year");
+        table.removeColumns("Price");
+
+        assertEquals("[Year, Make, Model, Description]", table.columnNames().toString());
+    }
+
+}
diff --git a/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthWriteOptionsTest.java b/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthWriteOptionsTest.java
new file mode 100644
index 00000000..6064df62
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/io/fixed/FixedWidthWriteOptionsTest.java
@@ -0,0 +1,54 @@
+package tech.tablesaw.io.fixed;
+
+import org.junit.Test;
+import tech.tablesaw.api.StringColumn;
+import tech.tablesaw.api.Table;
+
+import java.io.ByteArrayOutputStream;
+
+import static org.junit.Assert.*;
+
+public class FixedWidthWriteOptionsTest {
+
+    @Test
+    public void testSettingsPropagation() {
+
+        Table test = Table.create("test", StringColumn.create("t"));
+        test.stringColumn(0).appendCell("testing");
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        FixedWidthWriteOptions options = new FixedWidthWriteOptions.Builder(stream)
+                .header(true)
+                .lineSeparatorString("\r\n")
+                .padding('~')
+                .build();
+
+        assertTrue(options.header());
+
+        FixedWidthWriter writer = new FixedWidthWriter(test, options);
+        assertTrue(writer.getHeader());
+        assertEquals("\r\n", writer.getFormat().getLineSeparatorString());
+        assertEquals('~', writer.getFormat().getPadding());
+    }
+
+    @Test
+    public void testSettingsAutoConfigurationPropagation() {
+
+        Table test = Table.create("test", StringColumn.create("t"));
+        test.stringColumn(0).appendCell("testing");
+
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        FixedWidthWriteOptions options = new FixedWidthWriteOptions.Builder(stream)
+                .autoConfigurationEnabled(true)
+                .build();
+
+        assertTrue(options.autoConfigurationEnabled());
+
+        assertTrue(options.header());
+
+        FixedWidthWriter writer = new FixedWidthWriter(test, options);
+        assertTrue(writer.getHeader());
+    }
+
+
+}
diff --git a/data/fixed_width_cars_no_header_test.txt b/data/fixed_width_cars_no_header_test.txt
new file mode 100644
index 00000000..e880198b
--- /dev/null
+++ b/data/fixed_width_cars_no_header_test.txt
@@ -0,0 +1,4 @@
+1997Ford_E350____________________________________ac, abs, moon___________________________3000.00_
+1999ChevyVenture "Extended Edition"______________________________________________________4900.00_
+1999ChevyVenture "Extended Edition, Very Large"__________________________________________5000.00_
+_________Venture "Extended Edition"______________________________________________________4900.00_
\ No newline at end of file
diff --git a/data/fixed_width_cars_test.txt b/data/fixed_width_cars_test.txt
new file mode 100644
index 00000000..35cb3fc0
--- /dev/null
+++ b/data/fixed_width_cars_test.txt
@@ -0,0 +1,5 @@
+YearMake_Model___________________________________Description_____________________________Price___
+1997Ford_E350____________________________________ac, abs, moon___________________________3000.00_
+1999ChevyVenture "Extended Edition"______________________________________________________4900.00_
+1999ChevyVenture "Extended Edition, Very Large"__________________________________________5000.00_
+_________Venture "Extended Edition"______________________________________________________4900.00_
\ No newline at end of file