Repository: jtablesaw/tablesaw
Pull request #914 (2021-04-28 13:37:42)

Printformatting
- [x] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Deprecated support in CsvWriter for writing Date and DateTime columns using Java's DateTimeFormatter directly.

Replaced them with a new option usePrintFormatters(Boolean) that tells CsvWriter to use any printFormatters defined on all types of columns, unifying the output created by printAll() and other toString() variations with the writing of delimited text files. The new approach also handles missing values flexibly.

If this looks ok, I will add support for the other file output types using WriteOptions: html, json, fixed-width. I'm not really sure how the excel writing works, and i'm happy that way.

The sonar failure is sonar being stupid. It says I should remove the deprecated code some day. Which is true, but not a failure or code-smell today. 

## Testing
I added 10 tests


sonarcloud[bot]: SonarCloud Quality Gate failed.

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [19 Code Smells](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50.png' alt='56.6%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list) [56.6% Coverage](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [4 Code Smells](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/50.png' alt='56.6%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list) [56.6% Coverage](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60.png' alt='65.5%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list) [65.5% Coverage](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [3 Code Smells](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60.png' alt='68.8%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list) [68.8% Coverage](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: SonarCloud Quality Gate failed.

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL) [2 Code Smells](https://sonarcloud.io/project/issues?id=jtablesaw_tablesaw&pullRequest=914&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/60.png' alt='69.9%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list) [69.9% Coverage](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=jtablesaw_tablesaw&pullRequest=914&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/core/src/main/java/tech/tablesaw/api/DateColumn.java b/core/src/main/java/tech/tablesaw/api/DateColumn.java
index 11f25d40..abaf74df 100644
--- a/core/src/main/java/tech/tablesaw/api/DateColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/DateColumn.java
@@ -34,6 +34,7 @@ import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
+import javax.annotation.Nonnull;
 import tech.tablesaw.columns.AbstractColumn;
 import tech.tablesaw.columns.AbstractColumnParser;
 import tech.tablesaw.columns.Column;
@@ -135,17 +136,32 @@ public class DateColumn extends AbstractColumn<DateColumn, LocalDate>
     return value == null ? setMissing(index) : set(index, PackedLocalDate.pack(value));
   }
 
+  /**
+   * Creates and sets a printFormatter based-on the given DateTimeFormatter. Missing values will be
+   * printed as the given missingValueString. Non missing values will be handled by the
+   * dateTimeFormatter
+   */
   public void setPrintFormatter(DateTimeFormatter dateTimeFormatter, String missingValueString) {
     Preconditions.checkNotNull(dateTimeFormatter);
     Preconditions.checkNotNull(missingValueString);
     this.printFormatter = new DateColumnFormatter(dateTimeFormatter, missingValueString);
   }
 
+  /**
+   * Creates and sets a printFormatter based-on the given DateTimeFormatter. Missing values will be
+   * printed as empty strings. Non missing values will be handled by the dateTimeFormatter
+   */
   public void setPrintFormatter(DateTimeFormatter dateTimeFormatter) {
     Preconditions.checkNotNull(dateTimeFormatter);
     this.printFormatter = new DateColumnFormatter(dateTimeFormatter);
   }
 
+  /** Sets the print formatter to the argument */
+  public void setPrintFormatter(@Nonnull DateColumnFormatter dateColumnFormatter) {
+    Preconditions.checkNotNull(dateColumnFormatter);
+    this.printFormatter = dateColumnFormatter;
+  }
+
   @Override
   public String getString(int row) {
     return printFormatter.format(getPackedDate(row));
diff --git a/core/src/main/java/tech/tablesaw/api/TextColumn.java b/core/src/main/java/tech/tablesaw/api/TextColumn.java
index 8c9778e8..0de50ad8 100644
--- a/core/src/main/java/tech/tablesaw/api/TextColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TextColumn.java
@@ -292,7 +292,8 @@ public class TextColumn extends AbstractStringColumn<TextColumn> {
 
   @Override
   public TextColumn appendCell(String object, AbstractColumnParser<?> parser) {
-    return appendObj(parser.parse(object));
+    values.add(String.valueOf(parser.parse(object)));
+    return this;
   }
 
   @Override
diff --git a/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java b/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java
index 07e5e152..d281461e 100644
--- a/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/AbstractColumnParser.java
@@ -83,4 +83,8 @@ public abstract class AbstractColumnParser<T> {
     }
     return new String(chars, 0, pos);
   }
+
+  public void setMissingValueStrings(List<String> missingValueStrings) {
+    this.missingValueStrings = missingValueStrings;
+  }
 }
diff --git a/core/src/main/java/tech/tablesaw/columns/ColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/ColumnFormatter.java
new file mode 100644
index 00000000..6712f80d
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/ColumnFormatter.java
@@ -0,0 +1,22 @@
+package tech.tablesaw.columns;
+
+/**
+ * Abstract class for Column Formatters Every column type has a specialized print formatter that is
+ * used for formatting output for both table printing via, for example, table.printAll(). It is also
+ * used for writing text files using table.write().csv()
+ */
+public abstract class ColumnFormatter {
+
+  // The string to use for missing values
+  private final String missingString;
+
+  /** Constructs a new Formatter with the given missing value string. */
+  protected ColumnFormatter(String missingString) {
+    this.missingString = missingString;
+  }
+
+  /** Returns the string to be used in place of any missing values in the column */
+  public String getMissingString() {
+    return missingString;
+  }
+}
diff --git a/core/src/main/java/tech/tablesaw/columns/TemporalColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/TemporalColumnFormatter.java
new file mode 100644
index 00000000..220a62ea
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/columns/TemporalColumnFormatter.java
@@ -0,0 +1,36 @@
+package tech.tablesaw.columns;
+
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Abstract class for Column Formatters for temporal columns Date, DateTime, Time, and Instant Every
+ * column type has a specialized print formatter that is used for formatting output for both table
+ * printing via, for example, table.printAll(). It is also used for writing text files using
+ * table.write().csv()
+ */
+public abstract class TemporalColumnFormatter extends ColumnFormatter {
+
+  private final DateTimeFormatter format;
+
+  /** Constructs a new Formatter with the given formatter and an empty missing value string. */
+  protected TemporalColumnFormatter(DateTimeFormatter format) {
+    super("");
+    this.format = format;
+  }
+
+  /** Constructs a new Formatter with the given formatter and missing value string. */
+  protected TemporalColumnFormatter(DateTimeFormatter format, String missingValueString) {
+    super(missingValueString);
+    this.format = format;
+  }
+
+  /** Constructs a new default Formatter. This produces unformatted output. */
+  protected TemporalColumnFormatter() {
+    super("");
+    this.format = null;
+  }
+
+  public DateTimeFormatter getFormat() {
+    return format;
+  }
+}
diff --git a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanFormatter.java b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanFormatter.java
index 1da18b34..a965f557 100644
--- a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanFormatter.java
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanFormatter.java
@@ -1,30 +1,30 @@
 package tech.tablesaw.columns.booleans;
 
-public class BooleanFormatter {
+import tech.tablesaw.columns.ColumnFormatter;
+
+public class BooleanFormatter extends ColumnFormatter {
   private String trueString = "true";
   private String falseString = "false";
 
-  private String missingString = "";
-
   public BooleanFormatter(String trueString, String falseString, String missingString) {
+    super(missingString);
     this.trueString = trueString;
     this.falseString = falseString;
-    this.missingString = missingString;
   }
 
   public BooleanFormatter(String trueString, String falseString) {
+    super("");
     this.trueString = trueString;
     this.falseString = falseString;
-    this.missingString = "";
   }
 
   public BooleanFormatter(String missingString) {
-    this.missingString = missingString;
+    super(missingString);
   }
 
   public String format(Boolean value) {
     if (value == null) {
-      return missingString;
+      return getMissingString();
     }
     if (value) {
       return trueString;
@@ -34,7 +34,7 @@ public class BooleanFormatter {
 
   public String format(byte value) {
     if (value == BooleanColumnType.MISSING_VALUE) {
-      return missingString;
+      return getMissingString();
     }
     if (value == (byte) 1) {
       return trueString;
@@ -52,7 +52,7 @@ public class BooleanFormatter {
         + falseString
         + '\''
         + ", missingString='"
-        + missingString
+        + getMissingString()
         + '\''
         + '}';
   }
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnFormatter.java
index 7b93efb4..5b755885 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnFormatter.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnFormatter.java
@@ -6,47 +6,44 @@ import static tech.tablesaw.columns.dates.PackedLocalDate.toDateString;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import javax.annotation.concurrent.Immutable;
+import tech.tablesaw.columns.TemporalColumnFormatter;
 
 @Immutable
-public class DateColumnFormatter {
+public class DateColumnFormatter extends TemporalColumnFormatter {
 
-  private final DateTimeFormatter format;
-  private String missingString = "";
-
-  public DateColumnFormatter() {
-    this.format = null;
+  public DateColumnFormatter(DateTimeFormatter format) {
+    super(format);
   }
 
-  public DateColumnFormatter(DateTimeFormatter format) {
-    this.format = format;
+  public DateColumnFormatter() {
+    super();
   }
 
-  public DateColumnFormatter(DateTimeFormatter format, String missingString) {
-    this.format = format;
-    this.missingString = missingString;
+  public DateColumnFormatter(DateTimeFormatter format, String missingValueString) {
+    super(format, missingValueString);
   }
 
   public String format(int value) {
     if (value == DateColumnType.missingValueIndicator()) {
-      return missingString;
+      return getMissingString();
     }
-    if (format == null) {
+    if (getFormat() == null) {
       return toDateString(value);
     }
     LocalDate date = asLocalDate(value);
     if (date == null) {
       return "";
     }
-    return format.format(date);
+    return getFormat().format(date);
   }
 
   @Override
   public String toString() {
     return "DateColumnFormatter{"
         + "format="
-        + format
+        + getFormat()
         + ", missingString='"
-        + missingString
+        + getMissingString()
         + '\''
         + '}';
   }
diff --git a/core/src/main/java/tech/tablesaw/columns/instant/InstantColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/instant/InstantColumnFormatter.java
index 2bf7f2e6..e76f582d 100644
--- a/core/src/main/java/tech/tablesaw/columns/instant/InstantColumnFormatter.java
+++ b/core/src/main/java/tech/tablesaw/columns/instant/InstantColumnFormatter.java
@@ -1,73 +1,71 @@
 package tech.tablesaw.columns.instant;
 
+import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import javax.annotation.concurrent.Immutable;
-import tech.tablesaw.columns.times.TimeColumnType;
+import tech.tablesaw.columns.TemporalColumnFormatter;
 
 @Immutable
-public class InstantColumnFormatter {
+public class InstantColumnFormatter extends TemporalColumnFormatter {
 
-  private final DateTimeFormatter format;
   private final ZoneId zoneId;
-  private String missingValueString = "";
 
   public InstantColumnFormatter() {
-    this.format = null;
+    super(null);
     this.zoneId = ZoneOffset.UTC;
   }
 
   public InstantColumnFormatter(ZoneId zoneId) {
-    this.format = null;
+    super(null);
     this.zoneId = zoneId;
   }
 
   public InstantColumnFormatter(DateTimeFormatter format) {
-    this.format = format;
+    super(format);
     this.zoneId = ZoneOffset.UTC;
   }
 
   public InstantColumnFormatter(DateTimeFormatter format, ZoneId zoneId) {
-    this.format = format;
+    super(format);
     this.zoneId = zoneId;
   }
 
   public InstantColumnFormatter(DateTimeFormatter format, String missingValueString) {
-    this.format = format;
-    this.missingValueString = missingValueString;
+    super(format, missingValueString);
     this.zoneId = ZoneOffset.UTC;
   }
 
   public InstantColumnFormatter(
       DateTimeFormatter format, ZoneId zoneId, String missingValueString) {
-    this.format = format;
-    this.missingValueString = missingValueString;
+    super(format, missingValueString);
     this.zoneId = zoneId;
   }
 
   public String format(long value) {
-    if (value == TimeColumnType.missingValueIndicator()) {
-      return missingValueString;
+    if (value == InstantColumnType.missingValueIndicator()) {
+      return getMissingString();
     }
-    if (format == null) {
+    if (getFormat() == null) {
       return PackedInstant.toString(value);
     }
-    ZonedDateTime time = PackedInstant.asInstant(value).atZone(zoneId);
-    if (time == null) {
+    Instant instant = PackedInstant.asInstant(value);
+    if (instant == null) {
       return "";
     }
-    return format.format(time);
+    ZonedDateTime time = instant.atZone(zoneId);
+    return getFormat().format(time);
   }
 
   @Override
   public String toString() {
     return "InstantColumnFormatter{"
         + "format="
-        + format
+        + getFormat()
         + ", missingValueString='"
-        + missingValueString
+        + getMissingString()
         + '\''
         + '}';
   }
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/NumberColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/numbers/NumberColumnFormatter.java
index f0e5a3a9..45877691 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/NumberColumnFormatter.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/NumberColumnFormatter.java
@@ -4,11 +4,11 @@ import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
 import java.util.Locale;
+import tech.tablesaw.columns.ColumnFormatter;
 
-public class NumberColumnFormatter {
+public class NumberColumnFormatter extends ColumnFormatter {
 
   private final NumberFormat format;
-  private String missingString = "";
 
   public static NumberColumnFormatter percent(int fractionalDigits) {
     NumberFormat format = NumberFormat.getPercentInstance();
@@ -63,26 +63,28 @@ public class NumberColumnFormatter {
   }
 
   public NumberColumnFormatter() {
+    super("");
     this.format = null;
   }
 
   public NumberColumnFormatter(NumberFormat format) {
+    super("");
     this.format = format;
   }
 
   public NumberColumnFormatter(NumberFormat format, String missingString) {
+    super(missingString);
     this.format = format;
-    this.missingString = missingString;
   }
 
   public NumberColumnFormatter(String missingString) {
+    super(missingString);
     this.format = null;
-    this.missingString = missingString;
   }
 
   public String format(long value) {
     if (isMissingValue(value)) {
-      return missingString;
+      return getMissingString();
     }
     if (format == null) {
       return String.valueOf(value);
@@ -92,7 +94,7 @@ public class NumberColumnFormatter {
 
   public String format(double value) {
     if (isMissingValue(value)) {
-      return missingString;
+      return getMissingString();
     }
     if (format == null) {
       return String.valueOf(value);
@@ -106,7 +108,7 @@ public class NumberColumnFormatter {
         + "format="
         + format
         + ", missingString='"
-        + missingString
+        + getMissingString()
         + '\''
         + '}';
   }
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnFormatter.java
index f801966d..b73fa4fe 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnFormatter.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnFormatter.java
@@ -1,29 +1,31 @@
 package tech.tablesaw.columns.strings;
 
 import java.util.function.Function;
+import tech.tablesaw.columns.ColumnFormatter;
 
-public class StringColumnFormatter {
+public class StringColumnFormatter extends ColumnFormatter {
 
   private final Function<String, String> formatter;
-  private String missingString = "";
 
   public StringColumnFormatter() {
+    super("");
     this.formatter = null;
   }
 
   public StringColumnFormatter(Function<String, String> formatFunction) {
+    super("");
     this.formatter = formatFunction;
   }
 
   public StringColumnFormatter(Function<String, String> formatFunction, String missingString) {
+    super(missingString);
     this.formatter = formatFunction;
-    this.missingString = missingString;
   }
 
   public String format(String value) {
 
     if (StringColumnType.missingValueIndicator().equals(value)) {
-      return missingString;
+      return getMissingString();
     }
     if (formatter == null) {
       return value;
@@ -37,7 +39,7 @@ public class StringColumnFormatter {
         + "format="
         + formatter
         + ", missingString='"
-        + missingString
+        + getMissingString()
         + '\''
         + '}';
   }
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnFormatter.java b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnFormatter.java
index e1f8b6d6..5cabce9d 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnFormatter.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnFormatter.java
@@ -6,29 +6,27 @@ import static tech.tablesaw.columns.times.PackedLocalTime.toShortTimeString;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import javax.annotation.concurrent.Immutable;
+import tech.tablesaw.columns.TemporalColumnFormatter;
 
 @Immutable
-public class TimeColumnFormatter {
+public class TimeColumnFormatter extends TemporalColumnFormatter {
 
-  private final DateTimeFormatter format;
-  private String missingString = "";
-
-  public TimeColumnFormatter() {
-    this.format = null;
+  public TimeColumnFormatter(DateTimeFormatter format) {
+    super(format);
   }
 
-  public TimeColumnFormatter(DateTimeFormatter format) {
-    this.format = format;
+  public TimeColumnFormatter() {
+    super();
   }
 
-  public TimeColumnFormatter(DateTimeFormatter format, String missingString) {
-    this.format = format;
-    this.missingString = missingString;
+  public TimeColumnFormatter(DateTimeFormatter format, String missingValueString) {
+    super(format, missingValueString);
   }
 
   public String format(int value) {
+    DateTimeFormatter format = getFormat();
     if (value == TimeColumnType.missingValueIndicator()) {
-      return missingString;
+      return getMissingString();
     }
     if (format == null) {
       return toShortTimeString(value);
@@ -44,9 +42,9 @@ public class TimeColumnFormatter {
   public String toString() {
     return "TimeColumnFormatter{"
         + "format="
-        + format
+        + getFormat()
         + ", missingString='"
-        + missingString
+        + getMissingString()
         + '\''
         + '}';
   }
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 2a497c9f..ac18ecb7 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvWriteOptions.java
@@ -16,6 +16,7 @@ public class CsvWriteOptions extends WriteOptions {
   private final boolean header;
   private final boolean ignoreLeadingWhitespaces;
   private final boolean ignoreTrailingWhitespaces;
+  private final boolean usePrintFormatter;
   private final Character separator;
   private final Character quoteChar;
   private final Character escapeChar;
@@ -38,6 +39,7 @@ public class CsvWriteOptions extends WriteOptions {
     this.dateFormatter = builder.dateFormatter;
     this.dateTimeFormatter = builder.dateTimeFormatter;
     this.columnNameMap = builder.columnNameMap;
+    this.usePrintFormatter = builder.usePrintFormatters;
   }
 
   public boolean header() {
@@ -64,6 +66,10 @@ public class CsvWriteOptions extends WriteOptions {
     return quoteAllFields;
   }
 
+  public boolean usePrintFormatters() {
+    return usePrintFormatter;
+  }
+
   public Map<String, String> columnNameMap() {
     return columnNameMap;
   }
@@ -110,6 +116,7 @@ public class CsvWriteOptions extends WriteOptions {
     private boolean ignoreLeadingWhitespaces = true;
     private boolean ignoreTrailingWhitespaces = true;
     private boolean quoteAllFields = false;
+    private boolean usePrintFormatters = false;
     private Character separator;
     private String lineEnd = System.lineSeparator();
     private Character escapeChar;
@@ -159,16 +166,37 @@ public class CsvWriteOptions extends WriteOptions {
       return this;
     }
 
+    /**
+     * Writes date column output using the given DateFormatter
+     *
+     * @deprecated
+     */
+    @Deprecated
     public CsvWriteOptions.Builder dateFormatter(DateTimeFormatter dateFormatter) {
       this.dateFormatter = dateFormatter;
       return this;
     }
 
+    /**
+     * Writes DateTime column output using the given DateFormatter
+     *
+     * @deprecated
+     */
+    @Deprecated
     public CsvWriteOptions.Builder dateTimeFormatter(DateTimeFormatter dateTimeFormatter) {
       this.dateTimeFormatter = dateTimeFormatter;
       return this;
     }
 
+    /**
+     * Sets the usePrintFormatters option @link{tech.tablesaw.columns.ColumnFormatter} When true,
+     * printFormatters will be used in writing the output text for any column that has one.
+     */
+    public CsvWriteOptions.Builder usePrintFormatters(boolean useFormatter) {
+      this.usePrintFormatters = useFormatter;
+      return this;
+    }
+
     /**
      * Causes all data exported as a CSV file to be enclosed in quotes. Note that this includes the
      * headers, and all columns regardless of type
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvWriter.java b/core/src/main/java/tech/tablesaw/io/csv/CsvWriter.java
index 4a3052c1..c2cbfc5b 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvWriter.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvWriter.java
@@ -50,30 +50,11 @@ public final class CsvWriter implements DataWriter<CsvWriteOptions> {
       csvWriter =
           new com.univocity.parsers.csv.CsvWriter(options.destination().createWriter(), settings);
 
-      if (options.header()) {
-        String[] header = new String[table.columnCount()];
-        for (int c = 0; c < table.columnCount(); c++) {
-          String name = table.column(c).name();
-          header[c] = options.columnNameMap().getOrDefault(name, name);
-        }
-        csvWriter.writeHeaders(header);
-      }
+      writeHeader(table, options, csvWriter);
       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);
-          DateTimeFormatter dateFormatter = options.dateFormatter();
-          DateTimeFormatter dateTimeFormatter = options.dateTimeFormatter();
-          ColumnType columnType = table.column(c).type();
-          if (dateFormatter != null && columnType.equals(ColumnType.LOCAL_DATE)) {
-            DateColumn dc = (DateColumn) table.column(c);
-            entries[c] = options.dateFormatter().format(dc.get(r));
-          } else if (dateTimeFormatter != null && columnType.equals(ColumnType.LOCAL_DATE_TIME)) {
-            DateTimeColumn dc = (DateTimeColumn) table.column(c);
-            entries[c] = options.dateTimeFormatter().format(dc.get(r));
-          } else {
-            entries[c] = table.getUnformatted(r, c);
-          }
+          writeValues(table, options, r, entries, c);
         }
         csvWriter.writeRow(entries);
       }
@@ -85,6 +66,37 @@ public final class CsvWriter implements DataWriter<CsvWriteOptions> {
     }
   }
 
+  private void writeValues(Table table, CsvWriteOptions options, int r, String[] entries, int c) {
+    DateTimeFormatter dateFormatter = options.dateFormatter();
+    DateTimeFormatter dateTimeFormatter = options.dateTimeFormatter();
+    ColumnType columnType = table.column(c).type();
+    if (dateFormatter != null && columnType.equals(ColumnType.LOCAL_DATE)) {
+      DateColumn dc = (DateColumn) table.column(c);
+      entries[c] = options.dateFormatter().format(dc.get(r));
+    } else if (dateTimeFormatter != null && columnType.equals(ColumnType.LOCAL_DATE_TIME)) {
+      DateTimeColumn dc = (DateTimeColumn) table.column(c);
+      entries[c] = options.dateTimeFormatter().format(dc.get(r));
+    } else {
+      if (options.usePrintFormatters()) {
+        entries[c] = table.getString(r, c);
+      } else {
+        entries[c] = table.getUnformatted(r, c);
+      }
+    }
+  }
+
+  private void writeHeader(
+      Table table, CsvWriteOptions options, com.univocity.parsers.csv.CsvWriter csvWriter) {
+    if (options.header()) {
+      String[] header = new String[table.columnCount()];
+      for (int c = 0; c < table.columnCount(); c++) {
+        String name = table.column(c).name();
+        header[c] = options.columnNameMap().getOrDefault(name, name);
+      }
+      csvWriter.writeHeaders(header);
+    }
+  }
+
   protected static CsvWriterSettings createSettings(CsvWriteOptions options) {
     CsvWriterSettings settings = new CsvWriterSettings();
     // Sets the character sequence to write for the values that are null.


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
index 7f1e22d5..aaacf51a 100644
--- a/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/StringColumnTest.java
@@ -21,6 +21,8 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static tech.tablesaw.columns.strings.StringPredicates.isEqualToIgnoringCase;
 
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -29,6 +31,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.TestDataUtil;
 import tech.tablesaw.columns.strings.StringColumnFormatter;
+import tech.tablesaw.columns.strings.StringParser;
 import tech.tablesaw.selection.Selection;
 
 class StringColumnTest {
@@ -63,6 +66,21 @@ class StringColumnTest {
     assertArrayEquals(sc.asList().toArray(), sc.asObjectArray());
   }
 
+  @Test
+  void appendAsterisk() {
+    final StringColumn sc = StringColumn.create("sc");
+    sc.append("*");
+    assertEquals(0, sc.countMissing());
+    StringParser parser = new StringParser(ColumnType.TEXT);
+    parser.setMissingValueStrings(new ArrayList<>());
+    sc.appendCell("*", parser);
+    assertEquals(0, sc.countMissing());
+    StringParser parser2 = new StringParser(ColumnType.TEXT);
+    parser2.setMissingValueStrings(Lists.newArrayList("45"));
+    sc.appendCell("45", parser2);
+    assertEquals(1, sc.countMissing());
+  }
+
   @Test
   void testForNulls() {
     String[] array1 = {"1", "2", "3", "4", null};
diff --git a/core/src/test/java/tech/tablesaw/api/TextColumnTest.java b/core/src/test/java/tech/tablesaw/api/TextColumnTest.java
index a30bb142..9d327e92 100644
--- a/core/src/test/java/tech/tablesaw/api/TextColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/TextColumnTest.java
@@ -21,6 +21,7 @@ import static tech.tablesaw.columns.strings.StringPredicates.isEqualToIgnoringCa
 
 import com.google.common.base.Joiner;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Function;
@@ -28,6 +29,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import tech.tablesaw.TestDataUtil;
 import tech.tablesaw.columns.strings.StringColumnFormatter;
+import tech.tablesaw.columns.strings.StringParser;
 import tech.tablesaw.io.csv.CsvReadOptions;
 import tech.tablesaw.selection.Selection;
 
@@ -109,6 +111,15 @@ public class TextColumnTest {
     assertEquals(3, joined.columnCount());
   }
 
+  @Test
+  void appendAsterisk() {
+    final TextColumn sc = TextColumn.create("sc");
+    StringParser parser = new StringParser(ColumnType.TEXT);
+    parser.setMissingValueStrings(new ArrayList<>());
+    sc.appendCell("*", parser);
+    assertEquals(0, sc.countMissing());
+  }
+
   @Test
   public void lead() {
     TextColumn c1 = column.lead(1);
diff --git a/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java b/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java
index 509a44aa..0792e02c 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvWriterTest.java
@@ -6,18 +6,23 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.io.StringWriter;
+import java.text.DecimalFormat;
+import java.time.Instant;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Map;
 import org.junit.jupiter.api.Test;
-import tech.tablesaw.api.DateTimeColumn;
-import tech.tablesaw.api.StringColumn;
-import tech.tablesaw.api.Table;
+import tech.tablesaw.api.*;
+import tech.tablesaw.columns.booleans.BooleanFormatter;
+import tech.tablesaw.columns.instant.InstantColumnFormatter;
+import tech.tablesaw.columns.numbers.NumberColumnFormatter;
+import tech.tablesaw.columns.strings.StringColumnFormatter;
 
 public class CsvWriterTest {
 
   @Test
-  public void toWriterWithExtension() throws IOException {
+  void toWriterWithExtension() throws IOException {
     StringColumn colA = StringColumn.create("colA", ImmutableList.of("a", "b"));
     StringColumn colB = StringColumn.create("colB", ImmutableList.of("1", "2"));
     Table table = Table.create("testTable", colA, colB);
@@ -27,7 +32,7 @@ public class CsvWriterTest {
   }
 
   @Test
-  public void quoteAll() throws IOException {
+  void quoteAll() throws IOException {
     StringColumn colA = StringColumn.create("colA", ImmutableList.of("a", "b"));
     StringColumn colB = StringColumn.create("colB", ImmutableList.of("1", "2"));
     Table table = Table.create("testTable", colA, colB);
@@ -39,7 +44,7 @@ public class CsvWriterTest {
   }
 
   @Test
-  public void dateFormatter() throws IOException {
+  void dateFormatter() throws IOException {
     Table table = Table.read().csv("../data/bush.csv").rows(1);
     StringWriter writer = new StringWriter();
     table
@@ -54,7 +59,156 @@ public class CsvWriterTest {
   }
 
   @Test
-  public void dateTimeFormatter() throws IOException {
+  void printFormatter_double() throws IOException {
+    Table table = Table.create("", DoubleColumn.create("percents"));
+    table.doubleColumn("percents").setPrintFormatter(NumberColumnFormatter.percent(2));
+    table.doubleColumn("percents").append(0.323).append(0.1192).append(1.0);
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "percents\n" + "32.30%\n" + "11.92%\n" + "100.00%\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_date() throws IOException {
+    Table table = Table.create("", DateColumn.create("dates"));
+    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-dd-MMM");
+    table.dateColumn("dates").setPrintFormatter(formatter, "WHAT?");
+    table
+        .dateColumn("dates")
+        .append(LocalDate.of(2021, 11, 3))
+        .appendObj(null)
+        .append(LocalDate.of(2021, 3, 11));
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "dates\n" + "2021-03-Nov\n" + "WHAT?\n" + "2021-11-Mar\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_int() throws IOException {
+    Table table = Table.create("", IntColumn.create("ints"));
+    table.intColumn("ints").setPrintFormatter(NumberColumnFormatter.intsWithGrouping());
+    table.intColumn("ints").append(102_123).append(2).append(-1_232_132);
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "ints\n" + "\"102,123\"\n" + "2\n" + "\"-1,232,132\"\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_float() throws IOException {
+    Table table = Table.create("", FloatColumn.create("floats"));
+    table.floatColumn("floats").setPrintFormatter(NumberColumnFormatter.fixedWithGrouping(2));
+    table.floatColumn("floats").append(032.3f).append(0.1192f).appendObj(null).append(1001.0f);
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "floats\n" + "32.30\n" + "0.12\n" + "\n" + "\"1,001.00\"\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_datetime() throws IOException {
+    Table table = Table.create("", DateTimeColumn.create("dates"));
+    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM d, yyyy - hh:mm");
+    table.dateTimeColumn("dates").setPrintFormatter(formatter, "WHAT?");
+    table.dateTimeColumn("dates").append(LocalDateTime.of(2011, 1, 1, 4, 30));
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "dates\n" + "\"Jan 1, 2011 - 04:30\"\n", writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_boolean() throws IOException {
+    Table table = Table.create("", BooleanColumn.create("bools"));
+    BooleanFormatter formatter = new BooleanFormatter("Yes", "No", "IDK");
+    table.booleanColumn("bools").setPrintFormatter(formatter);
+    table.booleanColumn("bools").append(true).append(false).appendMissing();
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "bools\n" + "Yes\n" + "No\n" + "IDK\n", writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_string() throws IOException {
+    Table table = Table.create("", StringColumn.create("strings"));
+    StringColumnFormatter formatter = new StringColumnFormatter(s -> "[" + s + "]", "N/A");
+    table.stringColumn("strings").setPrintFormatter(formatter);
+    table.stringColumn("strings").append("hey").append("you").appendMissing();
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "strings\n" + "[hey]\n" + "[you]\n" + "N/A\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_text() throws IOException {
+    Table table = Table.create("", TextColumn.create("strings"));
+    StringColumnFormatter formatter = new StringColumnFormatter(s -> "[" + s + "]", "N/A");
+    table.textColumn("strings").setPrintFormatter(formatter);
+    table.textColumn("strings").append("hey").append("you").appendMissing();
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "strings\n" + "[hey]\n" + "[you]\n" + "N/A\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_short() throws IOException {
+    Table table = Table.create("", ShortColumn.create("ints"));
+    table.shortColumn("ints").setPrintFormatter(NumberColumnFormatter.intsWithGrouping());
+    table.shortColumn("ints").append((short) 102).append((short) 12_132).append((short) -1_234);
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "ints\n" + "102\n" + "\"12,132\"\n" + "\"-1,234\"\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void printFormatter_instant() throws IOException {
+    Table table = Table.create("", InstantColumn.create("dates"));
+    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM d, yyyy - hh:mm");
+    table.instantColumn("dates").setPrintFormatter(new InstantColumnFormatter(formatter, "WHAT?"));
+    table.instantColumn("dates").append(Instant.parse("2007-12-03T10:15:30.00Z")).appendMissing();
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "dates\n" + "\"Dec 3, 2007 - 10:15\"\n" + "WHAT?\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  /** Test preventing scientific notation */
+  @Test
+  void printFormatter_scientific_notation() throws IOException {
+    Table table = Table.create("", DoubleColumn.create("doubles"));
+    DecimalFormat df = new DecimalFormat("0.#");
+    df.setMaximumFractionDigits(11);
+    NumberColumnFormatter formatter = new NumberColumnFormatter(df);
+    table.doubleColumn("doubles").setPrintFormatter(formatter);
+    table
+        .doubleColumn("doubles")
+        .append(32.32342489123)
+        .append(0.1192342224)
+        .appendObj(null)
+        .append(1001.0);
+    StringWriter writer = new StringWriter();
+    table.write().usingOptions(CsvWriteOptions.builder(writer).usePrintFormatters(true).build());
+    assertEquals(
+        "doubles\n" + "32.32342489123\n" + "0.1192342224\n" + "\n" + "1001\n",
+        writer.toString().replaceAll("\\r\\n", "\n"));
+  }
+
+  @Test
+  void dateTimeFormatter() throws IOException {
     Table table = Table.create("test", DateTimeColumn.create("dt"));
     table.dateTimeColumn(0).append(LocalDateTime.of(2011, 1, 1, 4, 30));
     StringWriter writer = new StringWriter();