Skip to content

Commit 8430669

Browse files
committed
Make csv write defaults better formatted for opening in excel
1 parent 52a7a0e commit 8430669

7 files changed

Lines changed: 229 additions & 57 deletions

File tree

src/DataPowerTools.Connectivity/Json/DataReaderJsonExtensions.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,28 @@ public static string ToJsonCurrentRecord(this IDataRecord record, bool indent =
6565
}
6666

6767
/// <summary>
68-
/// Creates insert statements from an array of json objects.
68+
/// Converts JSON array to CSV string with Excel-friendly formatting.
69+
///
70+
/// Excel Compatibility Notes:
71+
/// - Uses UTF-8 compatible formatting with RFC 4180 compliance
72+
/// - String output doesn't include BOM - for Excel with emojis, save string to file with UTF-8 BOM
6973
/// </summary>
70-
public static string FromJsonToCsv(this string jsonString, bool writeHeaders = true, bool useTabFormat = false)
74+
/// <param name="jsonString">The JSON array string to convert</param>
75+
/// <param name="writeHeaders">Whether to write headers</param>
76+
/// <param name="useTabFormat">Whether to use tab format (TSV)</param>
77+
/// <param name="format">The format to use for the CSV output</param>
78+
/// <returns>CSV formatted string</returns>
79+
public static string FromJsonToCsv(this string jsonString, bool writeHeaders = true, bool useTabFormat = false, CSVFormat format = CSVFormat.UTF8)
7180
{
7281
var sb = new StringBuilder();
73-
82+
7483
var el = JsonDocument.Parse(jsonString).RootElement;
7584

7685
var sw = new StringWriter(sb);
7786

7887
using var csvWriter = useTabFormat
79-
? new CSVWriter(sw, '\t', CSVWriter.DefaultQuoteCharacter, CSVWriter.DefaultEscapeCharacter, "\r\n")
80-
: new CSVWriter(sw,',', CSVWriter.DefaultQuoteCharacter, CSVWriter.DefaultEscapeCharacter, "\r\n");
88+
? new CSVWriter(sw, '\t', CSVWriter.DefaultQuoteCharacter, CSVWriter.DefaultEscapeCharacter, CSVWriter.Rfc4180LineEnd)
89+
: new CSVWriter(sw,',', CSVWriter.DefaultQuoteCharacter, CSVWriter.DefaultEscapeCharacter, CSVWriter.Rfc4180LineEnd);
8190

8291
var hashSetHeaders = new HashSet<string>();
8392

src/DataPowerTools.Tests/CsvTests/DataPowerToolsCsvTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,100 @@ public void TestCsvExportUsesWindowsLineEndings()
9898
if (System.IO.File.Exists(testFile))
9999
System.IO.File.Delete(testFile);
100100
}
101+
102+
[TestMethod]
103+
public void TestCsvFormatOptions()
104+
{
105+
var testData = new[]
106+
{
107+
new { Name = "Test×Data", Description = "Special characters: ñ©€Á" },
108+
new { Name = "Normal", Description = "Regular text" }
109+
};
110+
111+
// Test UTF8 format (now default)
112+
var csvContent = testData.ToCsvString(format: CSVFormat.UTF8);
113+
Assert.IsTrue(csvContent.Contains("×"));
114+
Assert.IsTrue(csvContent.Contains("ñ"));
115+
Assert.IsTrue(csvContent.Contains("©"));
116+
Assert.IsTrue(csvContent.Contains("€"));
117+
Assert.IsTrue(csvContent.Contains("Á"));
118+
119+
// Test ANSI format (should work with fallback to UTF-8)
120+
var csvContentAnsi = testData.ToCsvString(format: CSVFormat.ANSI);
121+
Assert.IsTrue(csvContentAnsi.Contains("×"));
122+
123+
// Test UTF16 format
124+
var csvContentUtf16 = testData.ToCsvString(format: CSVFormat.UTF16);
125+
Assert.IsTrue(csvContentUtf16.Contains("×"));
126+
Assert.IsTrue(csvContentUtf16.Contains("€"));
127+
}
128+
129+
[TestMethod]
130+
public void TestCsvEmojiSupportAndExcelCompatibility()
131+
{
132+
// Test data with emojis and special characters that require full Unicode support
133+
var testData = new[]
134+
{
135+
new { Name = "John 😀 Doe", Status = "Happy 🎉", Country = "🇺🇸 USA" },
136+
new { Name = "María José 🌟", Status = "Café ☕", Country = "🇪🇸 España" },
137+
new { Name = "陈小明 🐉", Status = "茶 🍵", Country = "🇨🇳 中国" },
138+
new { Name = "Владимир 🚀", Status = "работа 💼", Country = "🇷🇺 Россия" }
139+
};
140+
141+
// Test UTF8 format (default) - this should preserve all emojis and Unicode characters
142+
var csvContent = testData.ToCsvString(format: CSVFormat.UTF8);
143+
144+
// Verify all emojis are preserved
145+
Assert.IsTrue(csvContent.Contains("😀"), "Should contain smile emoji");
146+
Assert.IsTrue(csvContent.Contains("🎉"), "Should contain party emoji");
147+
Assert.IsTrue(csvContent.Contains("🇺🇸"), "Should contain US flag emoji");
148+
Assert.IsTrue(csvContent.Contains("🌟"), "Should contain star emoji");
149+
Assert.IsTrue(csvContent.Contains("☕"), "Should contain coffee emoji");
150+
Assert.IsTrue(csvContent.Contains("🇪🇸"), "Should contain Spain flag emoji");
151+
Assert.IsTrue(csvContent.Contains("🐉"), "Should contain dragon emoji");
152+
Assert.IsTrue(csvContent.Contains("🍵"), "Should contain tea emoji");
153+
Assert.IsTrue(csvContent.Contains("🇨🇳"), "Should contain China flag emoji");
154+
Assert.IsTrue(csvContent.Contains("🚀"), "Should contain rocket emoji");
155+
Assert.IsTrue(csvContent.Contains("💼"), "Should contain briefcase emoji");
156+
Assert.IsTrue(csvContent.Contains("🇷🇺"), "Should contain Russia flag emoji");
157+
158+
// Verify special characters from different languages
159+
Assert.IsTrue(csvContent.Contains("María"), "Should contain accented characters");
160+
Assert.IsTrue(csvContent.Contains("陈小明"), "Should contain Chinese characters");
161+
Assert.IsTrue(csvContent.Contains("Владимир"), "Should contain Cyrillic characters");
162+
Assert.IsTrue(csvContent.Contains("茶"), "Should contain Chinese tea character");
163+
Assert.IsTrue(csvContent.Contains("работа"), "Should contain Russian text");
164+
165+
// Test file output with BOM for Excel compatibility
166+
string emojiTestFile = "emoji_test.csv";
167+
testData.WriteCsv(emojiTestFile, format: CSVFormat.UTF8);
168+
169+
// Verify file was created
170+
Assert.IsTrue(System.IO.File.Exists(emojiTestFile), "Emoji CSV file should be created");
171+
172+
// Read raw bytes to verify BOM is present for Excel compatibility
173+
byte[] fileBytes = System.IO.File.ReadAllBytes(emojiTestFile);
174+
175+
// Check for UTF-8 BOM (0xEF, 0xBB, 0xBF)
176+
Assert.IsTrue(fileBytes.Length >= 3, "File should have at least 3 bytes");
177+
Assert.AreEqual(0xEF, fileBytes[0], "First byte should be 0xEF (UTF-8 BOM)");
178+
Assert.AreEqual(0xBB, fileBytes[1], "Second byte should be 0xBB (UTF-8 BOM)");
179+
Assert.AreEqual(0xBF, fileBytes[2], "Third byte should be 0xBF (UTF-8 BOM)");
180+
181+
Console.WriteLine("✅ Emoji CSV Test Results:");
182+
Console.WriteLine($"📁 File size: {fileBytes.Length} bytes");
183+
Console.WriteLine($"📋 UTF-8 BOM detected: {fileBytes[0] == 0xEF && fileBytes[1] == 0xBB && fileBytes[2] == 0xBF}");
184+
Console.WriteLine($"🎯 Contains emojis: {csvContent.Contains("😀")}");
185+
Console.WriteLine($"🌍 Contains international text: {csvContent.Contains("陈小明")}");
186+
187+
Console.WriteLine("\n📝 For Excel users:");
188+
Console.WriteLine("1. The generated CSV uses UTF-8 with BOM for maximum compatibility");
189+
Console.WriteLine("2. If Excel shows one column, use Data > From Text/CSV and select UTF-8 encoding");
190+
Console.WriteLine("3. All emojis and international characters should display correctly");
191+
192+
// Clean up
193+
//if (System.IO.File.Exists(emojiTestFile))
194+
// System.IO.File.Delete(emojiTestFile);
195+
}
101196
}
102197
}

src/DataPowerTools/Csv/CSV.cs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace DataPowerTools
1414
{
1515
public static class Csv
1616
{
17-
private static StreamWriter CreateBomAwareStreamWriter(string filePath, CSVFormat format = CSVFormat.ANSI)
17+
private static StreamWriter CreateBomAwareStreamWriter(string filePath, CSVFormat format = CSVFormat.UTF8)
1818
{
1919
return new StreamWriter(filePath, false, format.GetEncoding());
2020
}
@@ -88,12 +88,17 @@ public static DataSet GetDataSet(Stream fileStream, char csvDelimiter = ',', boo
8888

8989
/// <summary>
9090
/// Writes an enumerable of object arrays into a file.
91+
///
92+
/// Excel Compatibility Notes:
93+
/// - Default UTF-8 with BOM format ensures emoji and international character support
94+
/// - Uses RFC 4180 compliant formatting with CRLF line endings for Windows compatibility
95+
/// - If Excel shows one column, use Data > From Text/CSV and select UTF-8 encoding
9196
/// </summary>
92-
/// <param name="rowObjects"></param>
93-
/// <param name="headers"></param>
94-
/// <param name="outputFile"></param>
95-
/// <param name="format">The format to use for the CSV output</param>
96-
public static void Write(IEnumerable<object[]> rowObjects, IEnumerable<string> headers, string outputFile, CSVFormat format = CSVFormat.ANSI)
97+
/// <param name="rowObjects">The data rows to write</param>
98+
/// <param name="headers">Column headers</param>
99+
/// <param name="outputFile">Output file path</param>
100+
/// <param name="format">The format to use for the CSV output (UTF8 with BOM recommended for Excel)</param>
101+
public static void Write(IEnumerable<object[]> rowObjects, IEnumerable<string> headers, string outputFile, CSVFormat format = CSVFormat.UTF8)
97102
{
98103
using var sw = CreateBomAwareStreamWriter(outputFile, format);
99104
using var csvWriter = new CSVWriter(sw, CSVWriter.DefaultSeparator, CSVWriter.DefaultQuoteCharacter, CSVWriter.DefaultEscapeCharacter, CSVWriter.Rfc4180LineEnd);
@@ -109,12 +114,17 @@ public static void Write(IEnumerable<object[]> rowObjects, IEnumerable<string> h
109114

110115
/// <summary>
111116
/// Writes an IDataReader to a CSV file onto disk (streaming operation).
117+
///
118+
/// Excel Compatibility Notes:
119+
/// - Default UTF-8 with BOM format ensures emoji and international character support
120+
/// - Uses RFC 4180 compliant formatting with CRLF line endings for Windows compatibility
121+
/// - If Excel shows one column, use Data > From Text/CSV and select UTF-8 encoding
112122
/// </summary>
113-
/// <param name="reader"></param>
114-
/// <param name="outputFile"></param>
115-
/// <param name="writeHeaders"></param>
116-
/// <param name="format">The format to use for the CSV output</param>
117-
public static void Write(IDataReader reader, string outputFile, bool writeHeaders = true, CSVFormat format = CSVFormat.ANSI)
123+
/// <param name="reader">The data reader to write</param>
124+
/// <param name="outputFile">Output file path</param>
125+
/// <param name="writeHeaders">Whether to include column headers</param>
126+
/// <param name="format">The format to use for the CSV output (UTF8 with BOM recommended for Excel)</param>
127+
public static void Write(IDataReader reader, string outputFile, bool writeHeaders = true, CSVFormat format = CSVFormat.UTF8)
118128
{
119129
using var sw = CreateBomAwareStreamWriter(outputFile, format);
120130
using var csvWriter = new CSVWriter(sw, CSVWriter.DefaultSeparator, CSVWriter.DefaultQuoteCharacter, CSVWriter.DefaultEscapeCharacter, CSVWriter.Rfc4180LineEnd);
@@ -172,13 +182,18 @@ void Initialize()
172182

173183
/// <summary>
174184
/// Writes an IDataReader to a CSV string.
185+
///
186+
/// Excel Compatibility Notes:
187+
/// - Default UTF-8 with BOM format ensures emoji and international character support
188+
/// - Uses RFC 4180 compliant formatting with CRLF line endings for Windows compatibility
189+
/// - String output doesn't include BOM - use file output methods for Excel compatibility
175190
/// </summary>
176-
/// <param name="reader"></param>
177-
/// <param name="writeHeaders"></param>
178-
/// <param name="useTabFormat">Outputs CSV in tab format</param>
179-
/// <param name="format">The format to use for the CSV output</param>
180-
/// <returns></returns>
181-
public static string WriteString(IDataReader reader, bool writeHeaders = true, bool useTabFormat = false, CSVFormat format = CSVFormat.ANSI)
191+
/// <param name="reader">The data reader to convert</param>
192+
/// <param name="writeHeaders">Whether to include column headers</param>
193+
/// <param name="useTabFormat">Outputs CSV in tab format (TSV)</param>
194+
/// <param name="format">The format to use for the CSV output (affects character encoding for file writes)</param>
195+
/// <returns>CSV formatted string</returns>
196+
public static string WriteString(IDataReader reader, bool writeHeaders = true, bool useTabFormat = false, CSVFormat format = CSVFormat.UTF8)
182197
{
183198
using var sw = new StringWriter();
184199

src/DataPowerTools/Csv/CSVFormat.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
1+
using System;
12
using System.Text;
23

34
namespace DataPowerTools
45
{
56
/// <summary>
6-
/// Specifies the format for CSV output
7+
/// Specifies the format for CSV output with Excel compatibility considerations.
8+
///
9+
/// For optimal Excel compatibility and emoji support, use UTF8 (default).
10+
/// If Excel shows one column, use Data > From Text/CSV and select UTF-8 encoding.
711
/// </summary>
812
public enum CSVFormat
913
{
1014
/// <summary>
11-
/// ANSI encoding (Windows-1252) - Default
15+
/// ANSI encoding (Windows-1252) - Falls back to UTF-8 if not available.
16+
/// Limited emoji support. Use UTF8 for better compatibility.
1217
/// </summary>
1318
ANSI,
1419

1520
/// <summary>
16-
/// UTF-8 encoding with BOM
21+
/// UTF-8 encoding with BOM - Default and recommended for Excel compatibility.
22+
/// Supports all emojis and international characters. Excel-friendly with proper import.
1723
/// </summary>
1824
UTF8,
1925

2026
/// <summary>
21-
/// UTF-16 encoding (Unicode)
27+
/// UTF-16 encoding (Unicode) - Full Unicode support but larger file size.
28+
/// May require specific Excel import settings.
2229
/// </summary>
2330
UTF16,
2431

2532
/// <summary>
26-
/// UTF-16 Little Endian encoding
33+
/// UTF-16 Little Endian encoding - Full Unicode support but larger file size.
2734
/// </summary>
2835
UTF16LE,
2936

3037
/// <summary>
31-
/// UTF-16 Big Endian encoding
38+
/// UTF-16 Big Endian encoding - Full Unicode support but larger file size.
3239
/// </summary>
3340
UTF16BE
3441
}
@@ -45,13 +52,36 @@ public static Encoding GetEncoding(this CSVFormat format)
4552
{
4653
return format switch
4754
{
48-
CSVFormat.ANSI => Encoding.GetEncoding(1252), // Windows-1252
55+
CSVFormat.ANSI => GetAnsiEncoding(),
4956
CSVFormat.UTF8 => new UTF8Encoding(true), // UTF-8 with BOM
5057
CSVFormat.UTF16 => Encoding.Unicode, // UTF-16 LE
5158
CSVFormat.UTF16LE => Encoding.Unicode, // UTF-16 LE
5259
CSVFormat.UTF16BE => Encoding.BigEndianUnicode, // UTF-16 BE
53-
_ => Encoding.GetEncoding(1252) // Default to ANSI
60+
_ => new UTF8Encoding(true) // Default to UTF-8 with BOM
5461
};
5562
}
63+
64+
/// <summary>
65+
/// Gets ANSI encoding (Windows-1252) with fallback to UTF-8 for .NET Core/.NET 5+
66+
/// </summary>
67+
private static Encoding GetAnsiEncoding()
68+
{
69+
try
70+
{
71+
// Try to get Windows-1252 encoding
72+
return Encoding.GetEncoding(1252);
73+
}
74+
catch (NotSupportedException)
75+
{
76+
// In .NET Core/.NET 5+, legacy encodings may not be available
77+
// Fall back to UTF-8 which supports all the same characters and more
78+
return new UTF8Encoding(false); // UTF-8 without BOM for ANSI compatibility
79+
}
80+
catch (ArgumentException)
81+
{
82+
// Fallback for any other encoding issues
83+
return new UTF8Encoding(false);
84+
}
85+
}
5686
}
5787
}

src/DataPowerTools/Extensions/DataReaderExtensions.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,33 @@ namespace DataPowerTools.Extensions
1919
public static class DataReaderExtensions
2020
{
2121
/// <summary>
22-
/// Writes datareader to CSV.
22+
/// Writes datareader to CSV file with Excel-friendly formatting.
23+
///
24+
/// Excel Compatibility:
25+
/// - Uses UTF-8 with BOM by default for emoji and international character support
26+
/// - RFC 4180 compliant with CRLF line endings
27+
/// - If Excel shows one column, use Data > From Text/CSV and select UTF-8 encoding
2328
/// </summary>
2429
/// <param name="reader">The data reader to write</param>
2530
/// <param name="outputFile">The output file path</param>
26-
/// <param name="format">The format to use for the CSV output</param>
27-
public static void WriteCsv(this IDataReader reader, string outputFile, CSVFormat format = CSVFormat.ANSI)
31+
/// <param name="format">The format to use for the CSV output (UTF8 with BOM recommended for Excel)</param>
32+
public static void WriteCsv(this IDataReader reader, string outputFile, CSVFormat format = CSVFormat.UTF8)
2833
{
2934
Csv.Write(reader, outputFile, format: format);
3035
}
3136

3237
/// <summary>
33-
/// Writes datareader to CSV.
38+
/// Converts datareader to CSV string with Excel-friendly formatting.
39+
///
40+
/// Note: String output doesn't include BOM. For Excel compatibility with emojis/international characters,
41+
/// use WriteCsv() method to write directly to file which includes proper BOM.
3442
/// </summary>
35-
/// <param name="reader">The data reader to write</param>
43+
/// <param name="reader">The data reader to convert</param>
3644
/// <param name="writeHeaders">Whether to write headers</param>
37-
/// <param name="useTabFormat">Whether to use tab format</param>
45+
/// <param name="useTabFormat">Whether to use tab format (TSV)</param>
3846
/// <param name="format">The format to use for the CSV output</param>
3947
/// <returns>The CSV string</returns>
40-
public static string AsCsv(this IDataReader reader, bool writeHeaders = true, bool useTabFormat = false, CSVFormat format = CSVFormat.ANSI)
48+
public static string AsCsv(this IDataReader reader, bool writeHeaders = true, bool useTabFormat = false, CSVFormat format = CSVFormat.UTF8)
4149
{
4250
return Csv.WriteString(reader, writeHeaders, useTabFormat, format);
4351
}

0 commit comments

Comments
 (0)