diff --git a/source/Sylvan.Data.Csv.Tests/CsvDataWriterTests.cs b/source/Sylvan.Data.Csv.Tests/CsvDataWriterTests.cs index da27bdf..680a8e6 100644 --- a/source/Sylvan.Data.Csv.Tests/CsvDataWriterTests.cs +++ b/source/Sylvan.Data.Csv.Tests/CsvDataWriterTests.cs @@ -471,12 +471,18 @@ public void CsvWriteBatches() } [Theory] - [InlineData(true, "Name\n\"\"\n\n")] - [InlineData(false, "Name\n\n\n")] - public void QuoteEmptyStrings(bool mode, string result) + [InlineData(CsvStringQuoting.Default, "Name\n1\n\n\n")] + [InlineData(CsvStringQuoting.AlwaysQuoteEmpty, "Name\n1\n\"\"\n\n")] + [InlineData(CsvStringQuoting.AlwaysQuoteNonEmpty, "\"Name\"\n\"1\"\n\n\n")] + [InlineData(CsvStringQuoting.AlwaysQuote, "\"Name\"\n\"1\"\n\"\"\n\n")] + public void QuoteStringsOptions(CsvStringQuoting mode, string result) { var data = new[] { + new + { + Name = "1" + }, new { Name = "" @@ -487,7 +493,7 @@ public void QuoteEmptyStrings(bool mode, string result) }, }; var reader = data.AsDataReader(); - var opts = new CsvDataWriterOptions { QuoteEmptyStrings = mode, NewLine = "\n" }; + var opts = new CsvDataWriterOptions { QuoteStrings = mode, NewLine = "\n" }; var sw = new StringWriter(); var writer = CsvDataWriter.Create(sw, opts); writer.Write(reader); diff --git a/source/Sylvan.Data.Csv/CsvDataWriter.cs b/source/Sylvan.Data.Csv/CsvDataWriter.cs index 22e1631..c2d46f3 100644 --- a/source/Sylvan.Data.Csv/CsvDataWriter.cs +++ b/source/Sylvan.Data.Csv/CsvDataWriter.cs @@ -447,7 +447,7 @@ bool IsFastTimeOnly readonly CsvWriter csvWriter; readonly bool writeHeaders; - readonly bool quoteEmptyStrings; + readonly CsvStringQuoting quoteStrings; readonly char delimiter; readonly char quote; readonly char escape; @@ -530,7 +530,7 @@ public static CsvDataWriter Create(TextWriter writer, char[] buffer, CsvDataWrit this.dateOnlyFormat = options.DateOnlyFormat; #endif this.writeHeaders = options.WriteHeaders; - this.quoteEmptyStrings = options.QuoteEmptyStrings; + this.quoteStrings = options.QuoteStrings; this.delimiter = options.Delimiter; this.quote = options.Quote; this.escape = options.Escape; diff --git a/source/Sylvan.Data.Csv/CsvDataWriterOptions.cs b/source/Sylvan.Data.Csv/CsvDataWriterOptions.cs index 459b3db..c32036e 100644 --- a/source/Sylvan.Data.Csv/CsvDataWriterOptions.cs +++ b/source/Sylvan.Data.Csv/CsvDataWriterOptions.cs @@ -3,6 +3,33 @@ namespace Sylvan.Data.Csv; +/// +/// Specifies how strings are quoted +/// +[Flags] +public enum CsvStringQuoting +{ + /// + /// Strings are only quoted when it is required. + /// + Default = 0, + + /// + /// Empty strings are always quoted. This distinguishes them from null. + /// + AlwaysQuoteEmpty = 1, + + /// + /// Non-empty strings are always quoted. This helps prevent confusion with numbers and dates. + /// + AlwaysQuoteNonEmpty = 2, + + /// + /// All strings are always quoted. + /// + AlwaysQuote = 3, +} + /// /// Options for configuring a CsvWriter. /// @@ -30,6 +57,7 @@ public CsvDataWriterOptions() this.BinaryEncoding = BinaryEncoding.Base64; this.Style = CsvStyle.Standard; this.Delimiter = DefaultDelimiter; + this.QuoteStrings = CsvStringQuoting.Default; this.Quote = DefaultQuote; this.Escape = DefaultEscape; this.Comment = DefaultComment; @@ -121,10 +149,26 @@ public string? TimeFormat { public char Delimiter { get; set; } /// - /// Empty strings will be written as empty quotes in the CSV. + /// The rules to use when quoting strings, defaults to + /// + public CsvStringQuoting QuoteStrings { get; set; } + + /// + /// Empty strings will be written as empty quotes in the CSV. /// This allows distinguishing empty strings from null. /// - public bool QuoteEmptyStrings { get; set; } + [Obsolete("Use QuoteStrings instead.")] + public bool QuoteEmptyStrings + { + get => QuoteStrings.HasFlag(CsvStringQuoting.AlwaysQuoteEmpty); + set + { + if (value) + QuoteStrings |= CsvStringQuoting.AlwaysQuoteEmpty; + else + QuoteStrings &= ~CsvStringQuoting.AlwaysQuoteEmpty; + } + } /// /// The character to use for quoting fields. The default is '"'. @@ -184,7 +228,7 @@ internal void Validate() Quote >= 128 || Escape >= 128 || Comment >= 128 || - (QuoteEmptyStrings && Style == CsvStyle.Escaped) + (QuoteStrings != CsvStringQuoting.Default && Style == CsvStyle.Escaped) ; if (invalid) throw new CsvConfigurationException(); diff --git a/source/Sylvan.Data.Csv/CsvWriter.cs b/source/Sylvan.Data.Csv/CsvWriter.cs index 4480c4a..b81031b 100644 --- a/source/Sylvan.Data.Csv/CsvWriter.cs +++ b/source/Sylvan.Data.Csv/CsvWriter.cs @@ -114,10 +114,15 @@ static int WriteValueOptimistic(WriterContext context, string value, char[] buff { var pos = offset; var needsEscape = context.writer.needsEscape; - if (value.Length == 0 && context.writer.quoteEmptyStrings) + if (value.Length == 0) { - return NeedsQuoting; + if (context.writer.quoteStrings.HasFlag(CsvStringQuoting.AlwaysQuoteEmpty)) + return NeedsQuoting; + else + return 0; } + else if (context.writer.quoteStrings.HasFlag(CsvStringQuoting.AlwaysQuoteNonEmpty)) + return NeedsQuoting; if (pos + value.Length >= buffer.Length) return InsufficientSpace;