CsvHelper.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.IO;
  5. using System.IO.Abstractions;
  6. using System.Linq;
  7. using System.Text;
  8. namespace Quadarax.Foundation.Core.Data
  9. {
  10. public static class CsvHelper
  11. {
  12. /// <summary>
  13. /// Read CSV file and creates DataTable with filled content, handling quoted values.
  14. /// </summary>
  15. /// <param name="fs">Used filesystem.</param>
  16. /// <param name="csvFile">Full path to the CSV file.</param>
  17. /// <param name="delimiter">The delimiter used in the CSV file.</param>
  18. /// <param name="hasHeaderRow">Indicates whether the CSV file has a header row.</param>
  19. /// <returns>New created DataTable with content.</returns>
  20. public static DataTable CsvToDataTable(IFileSystem fs, string csvFile, string delimiter = ",", bool hasHeaderRow = true)
  21. {
  22. if (!fs.File.Exists(csvFile)) throw new FileNotFoundException($"File '{csvFile}' not found!", csvFile);
  23. var dt = new DataTable();
  24. using (var sr = fs.File.OpenText(csvFile))
  25. {
  26. string[]? headers = null;
  27. if (hasHeaderRow)
  28. {
  29. headers = ParseCsvLine(sr.ReadLine(), delimiter);
  30. if (headers == null) return dt;
  31. foreach (var header in headers)
  32. {
  33. dt.Columns.Add(header.Trim());
  34. }
  35. }
  36. while (!sr.EndOfStream)
  37. {
  38. var rows = ParseCsvLine(sr.ReadLine(), delimiter);
  39. if (rows == null) continue;
  40. if (headers == null)
  41. {
  42. headers = new string[rows.Length];
  43. for (int i = 0; i < rows.Length; i++)
  44. {
  45. headers[i] = $"Column{i + 1}";
  46. dt.Columns.Add(headers[i]);
  47. }
  48. }
  49. var dr = dt.NewRow();
  50. for (var i = 0; i < Math.Min(headers.Length, rows.Length); i++)
  51. {
  52. dr[i] = rows[i];
  53. }
  54. dt.Rows.Add(dr);
  55. }
  56. }
  57. return dt;
  58. }
  59. /// <summary>
  60. /// Parses a single CSV line, handling quoted values and delimiters within quotes.
  61. /// </summary>
  62. /// <param name="line">The CSV line to parse.</param>
  63. /// <param name="delimiter">The delimiter used in the CSV line.</param>
  64. /// <returns>An array of field values.</returns>
  65. private static string[]? ParseCsvLine(string? line, string delimiter)
  66. {
  67. if (string.IsNullOrEmpty(line))
  68. return null;
  69. var result = new List<string>();
  70. var currentField = new StringBuilder();
  71. bool inQuotes = false;
  72. for (int i = 0; i < line.Length; i++)
  73. {
  74. if (inQuotes)
  75. {
  76. if (line[i] == '"' && (i + 1 == line.Length || line[i + 1] != '"'))
  77. {
  78. // End of quoted field
  79. inQuotes = false;
  80. }
  81. else if (line[i] == '"' && i + 1 < line.Length && line[i + 1] == '"')
  82. {
  83. // Escaped quote
  84. currentField.Append('"');
  85. i++; // Skip next quote
  86. }
  87. else
  88. {
  89. currentField.Append(line[i]);
  90. }
  91. }
  92. else
  93. {
  94. if (line[i] == '"')
  95. {
  96. inQuotes = true;
  97. }
  98. else if (line.Substring(i).StartsWith(delimiter))
  99. {
  100. result.Add(currentField.ToString().Trim());
  101. currentField.Clear();
  102. i += delimiter.Length - 1; // Skip the rest of the delimiter
  103. }
  104. else
  105. {
  106. currentField.Append(line[i]);
  107. }
  108. }
  109. }
  110. result.Add(currentField.ToString().Trim());
  111. return result.ToArray();
  112. }
  113. /// <summary>
  114. /// Writes a DataTable to a CSV file, properly handling quoting.
  115. /// </summary>
  116. /// <param name="fs">Used filesystem.</param>
  117. /// <param name="dataTable">The DataTable to write.</param>
  118. /// <param name="filePath">The full path of the output CSV file.</param>
  119. /// <param name="delimiter">The delimiter to use in the CSV file.</param>
  120. public static void DataTableToCsv(IFileSystem fs,DataTable dataTable, string filePath, string delimiter = ",")
  121. {
  122. using (var sw = fs.File.CreateText(filePath))
  123. {
  124. // Write headers
  125. sw.WriteLine(string.Join(delimiter, dataTable.Columns.Cast<DataColumn>().Select(column => QuoteValue(column.ColumnName, delimiter))));
  126. // Write rows
  127. foreach (DataRow row in dataTable.Rows)
  128. {
  129. sw.WriteLine(string.Join(delimiter, row.ItemArray.Select(field => QuoteValue(field?.ToString(), delimiter))));
  130. }
  131. }
  132. }
  133. /// <summary>
  134. /// Reads a CSV file and returns it as a list of dictionaries.
  135. /// </summary>
  136. /// <param name="fs">Used filesystem.</param>
  137. /// <param name="csvFile">Full path to the CSV file.</param>
  138. /// <param name="delimiter">The delimiter used in the CSV file.</param>
  139. /// <returns>List of dictionaries representing the CSV data.</returns>
  140. public static List<Dictionary<string, string>> CsvToDictionaryList(IFileSystem fs, string csvFile, string delimiter = ",")
  141. {
  142. if (!fs.File.Exists(csvFile)) throw new FileNotFoundException($"File '{csvFile}' not found!", csvFile);
  143. var result = new List<Dictionary<string, string>>();
  144. using (var sr = new StreamReader(csvFile))
  145. {
  146. var headers = ParseCsvLine(sr.ReadLine(), delimiter);
  147. if (headers == null) return result;
  148. while (!sr.EndOfStream)
  149. {
  150. var rows = ParseCsvLine(sr.ReadLine(), delimiter);
  151. if (rows == null) continue;
  152. var dict = new Dictionary<string, string>();
  153. for (var i = 0; i < headers.Length; i++)
  154. {
  155. dict[headers[i].Trim()] = i < rows.Length ? rows[i] : string.Empty;
  156. }
  157. result.Add(dict);
  158. }
  159. }
  160. return result;
  161. }
  162. private static string QuoteValue(string? value, string delimiter)
  163. {
  164. if (string.IsNullOrEmpty(value))
  165. return string.Empty;
  166. bool needsQuoting = value.Contains(delimiter) || value.Contains("\"") || value.Contains("\r") || value.Contains("\n");
  167. if (needsQuoting)
  168. {
  169. return $"\"{value.Replace("\"", "\"\"")}\"";
  170. }
  171. return value;
  172. }
  173. }
  174. }