|
@@ -1,48 +1,194 @@
|
|
|
using System;
|
|
using System;
|
|
|
|
|
+using System.Collections.Generic;
|
|
|
using System.Data;
|
|
using System.Data;
|
|
|
using System.IO;
|
|
using System.IO;
|
|
|
|
|
+using System.IO.Abstractions;
|
|
|
|
|
+using System.Linq;
|
|
|
|
|
+using System.Text;
|
|
|
|
|
|
|
|
namespace Quadarax.Foundation.Core.Data
|
|
namespace Quadarax.Foundation.Core.Data
|
|
|
{
|
|
{
|
|
|
public static class CsvHelper
|
|
public static class CsvHelper
|
|
|
- {
|
|
|
|
|
|
|
+ {
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
- /// Read CSV file and creates <see cref="DataTable"/> with filled content.
|
|
|
|
|
- /// <remarks>
|
|
|
|
|
- /// This method expects first header row.
|
|
|
|
|
- /// </remarks>
|
|
|
|
|
|
|
+ /// Read CSV file and creates DataTable with filled content, handling quoted values.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- /// <param name="csvFile">(*.csv) full file name.</param>
|
|
|
|
|
- /// <param name="delimiter">Delimiter character</param>
|
|
|
|
|
|
|
+ /// <param name="fs">Used filesystem.</param>
|
|
|
|
|
+ /// <param name="csvFile">Full path to the CSV file.</param>
|
|
|
|
|
+ /// <param name="delimiter">The delimiter used in the CSV file.</param>
|
|
|
|
|
+ /// <param name="hasHeaderRow">Indicates whether the CSV file has a header row.</param>
|
|
|
/// <returns>New created DataTable with content.</returns>
|
|
/// <returns>New created DataTable with content.</returns>
|
|
|
- public static DataTable CsvToDataTable(string csvFile, string delimiter)
|
|
|
|
|
- {
|
|
|
|
|
- var dt = new DataTable();
|
|
|
|
|
- using (var sr = new StreamReader(csvFile))
|
|
|
|
|
- {
|
|
|
|
|
-
|
|
|
|
|
- var headers = sr?.ReadLine()?.Split(new string[] { delimiter }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
- if (headers == null) return dt;
|
|
|
|
|
- foreach (var header in headers)
|
|
|
|
|
- {
|
|
|
|
|
- dt.Columns.Add(header);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- while (sr is { EndOfStream: false })
|
|
|
|
|
- {
|
|
|
|
|
- var rows = sr?.ReadLine()?.Split(new[] { delimiter }, StringSplitOptions.None);
|
|
|
|
|
- if (rows == null) continue;
|
|
|
|
|
-
|
|
|
|
|
- var dr = dt.NewRow();
|
|
|
|
|
- for (var i = 0; i < headers.Length; i++)
|
|
|
|
|
- {
|
|
|
|
|
- dr[i] = rows[i];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- dt.Rows.Add(dr);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return dt;
|
|
|
|
|
|
|
+ public static DataTable CsvToDataTable(IFileSystem fs, string csvFile, string delimiter = ",", bool hasHeaderRow = true)
|
|
|
|
|
+ {
|
|
|
|
|
+
|
|
|
|
|
+ if (!fs.File.Exists(csvFile)) throw new FileNotFoundException($"File '{csvFile}' not found!", csvFile);
|
|
|
|
|
+
|
|
|
|
|
+ var dt = new DataTable();
|
|
|
|
|
+ using (var sr = fs.File.OpenText(csvFile))
|
|
|
|
|
+ {
|
|
|
|
|
+ string[]? headers = null;
|
|
|
|
|
+ if (hasHeaderRow)
|
|
|
|
|
+ {
|
|
|
|
|
+ headers = ParseCsvLine(sr.ReadLine(), delimiter);
|
|
|
|
|
+ if (headers == null) return dt;
|
|
|
|
|
+ foreach (var header in headers)
|
|
|
|
|
+ {
|
|
|
|
|
+ dt.Columns.Add(header.Trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ while (!sr.EndOfStream)
|
|
|
|
|
+ {
|
|
|
|
|
+ var rows = ParseCsvLine(sr.ReadLine(), delimiter);
|
|
|
|
|
+ if (rows == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ if (headers == null)
|
|
|
|
|
+ {
|
|
|
|
|
+ headers = new string[rows.Length];
|
|
|
|
|
+ for (int i = 0; i < rows.Length; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ headers[i] = $"Column{i + 1}";
|
|
|
|
|
+ dt.Columns.Add(headers[i]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var dr = dt.NewRow();
|
|
|
|
|
+ for (var i = 0; i < Math.Min(headers.Length, rows.Length); i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ dr[i] = rows[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ dt.Rows.Add(dr);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return dt;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Parses a single CSV line, handling quoted values and delimiters within quotes.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="line">The CSV line to parse.</param>
|
|
|
|
|
+ /// <param name="delimiter">The delimiter used in the CSV line.</param>
|
|
|
|
|
+ /// <returns>An array of field values.</returns>
|
|
|
|
|
+ private static string[]? ParseCsvLine(string? line, string delimiter)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (string.IsNullOrEmpty(line))
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
|
|
+ var result = new List<string>();
|
|
|
|
|
+ var currentField = new StringBuilder();
|
|
|
|
|
+ bool inQuotes = false;
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < line.Length; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (inQuotes)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (line[i] == '"' && (i + 1 == line.Length || line[i + 1] != '"'))
|
|
|
|
|
+ {
|
|
|
|
|
+ // End of quoted field
|
|
|
|
|
+ inQuotes = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (line[i] == '"' && i + 1 < line.Length && line[i + 1] == '"')
|
|
|
|
|
+ {
|
|
|
|
|
+ // Escaped quote
|
|
|
|
|
+ currentField.Append('"');
|
|
|
|
|
+ i++; // Skip next quote
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ currentField.Append(line[i]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ if (line[i] == '"')
|
|
|
|
|
+ {
|
|
|
|
|
+ inQuotes = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (line.Substring(i).StartsWith(delimiter))
|
|
|
|
|
+ {
|
|
|
|
|
+ result.Add(currentField.ToString().Trim());
|
|
|
|
|
+ currentField.Clear();
|
|
|
|
|
+ i += delimiter.Length - 1; // Skip the rest of the delimiter
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ currentField.Append(line[i]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.Add(currentField.ToString().Trim());
|
|
|
|
|
+ return result.ToArray();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Writes a DataTable to a CSV file, properly handling quoting.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="fs">Used filesystem.</param>
|
|
|
|
|
+ /// <param name="dataTable">The DataTable to write.</param>
|
|
|
|
|
+ /// <param name="filePath">The full path of the output CSV file.</param>
|
|
|
|
|
+ /// <param name="delimiter">The delimiter to use in the CSV file.</param>
|
|
|
|
|
+ public static void DataTableToCsv(IFileSystem fs,DataTable dataTable, string filePath, string delimiter = ",")
|
|
|
|
|
+ {
|
|
|
|
|
+ using (var sw = fs.File.CreateText(filePath))
|
|
|
|
|
+ {
|
|
|
|
|
+
|
|
|
|
|
+ // Write headers
|
|
|
|
|
+ sw.WriteLine(string.Join(delimiter, dataTable.Columns.Cast<DataColumn>().Select(column => QuoteValue(column.ColumnName, delimiter))));
|
|
|
|
|
+
|
|
|
|
|
+ // Write rows
|
|
|
|
|
+ foreach (DataRow row in dataTable.Rows)
|
|
|
|
|
+ {
|
|
|
|
|
+ sw.WriteLine(string.Join(delimiter, row.ItemArray.Select(field => QuoteValue(field?.ToString(), delimiter))));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Reads a CSV file and returns it as a list of dictionaries.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="fs">Used filesystem.</param>
|
|
|
|
|
+ /// <param name="csvFile">Full path to the CSV file.</param>
|
|
|
|
|
+ /// <param name="delimiter">The delimiter used in the CSV file.</param>
|
|
|
|
|
+ /// <returns>List of dictionaries representing the CSV data.</returns>
|
|
|
|
|
+ public static List<Dictionary<string, string>> CsvToDictionaryList(IFileSystem fs, string csvFile, string delimiter = ",")
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!fs.File.Exists(csvFile)) throw new FileNotFoundException($"File '{csvFile}' not found!", csvFile);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ var result = new List<Dictionary<string, string>>();
|
|
|
|
|
+ using (var sr = new StreamReader(csvFile))
|
|
|
|
|
+ {
|
|
|
|
|
+ var headers = ParseCsvLine(sr.ReadLine(), delimiter);
|
|
|
|
|
+ if (headers == null) return result;
|
|
|
|
|
+
|
|
|
|
|
+ while (!sr.EndOfStream)
|
|
|
|
|
+ {
|
|
|
|
|
+ var rows = ParseCsvLine(sr.ReadLine(), delimiter);
|
|
|
|
|
+ if (rows == null) continue;
|
|
|
|
|
+
|
|
|
|
|
+ var dict = new Dictionary<string, string>();
|
|
|
|
|
+ for (var i = 0; i < headers.Length; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ dict[headers[i].Trim()] = i < rows.Length ? rows[i] : string.Empty;
|
|
|
|
|
+ }
|
|
|
|
|
+ result.Add(dict);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string QuoteValue(string? value, string delimiter)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (string.IsNullOrEmpty(value))
|
|
|
|
|
+ return string.Empty;
|
|
|
|
|
+
|
|
|
|
|
+ bool needsQuoting = value.Contains(delimiter) || value.Contains("\"") || value.Contains("\r") || value.Contains("\n");
|
|
|
|
|
+ if (needsQuoting)
|
|
|
|
|
+ {
|
|
|
|
|
+ return $"\"{value.Replace("\"", "\"\"")}\"";
|
|
|
|
|
+ }
|
|
|
|
|
+ return value;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|