using System; using System.Collections.Generic; using System.Data; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Text; namespace Quadarax.Foundation.Core.Data { public static class CsvHelper { /// /// Read CSV file and creates DataTable with filled content, handling quoted values. /// /// Used filesystem. /// Full path to the CSV file. /// The delimiter used in the CSV file. /// Indicates whether the CSV file has a header row. /// New created DataTable with content. 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; } /// /// Parses a single CSV line, handling quoted values and delimiters within quotes. /// /// The CSV line to parse. /// The delimiter used in the CSV line. /// An array of field values. private static string[]? ParseCsvLine(string? line, string delimiter) { if (string.IsNullOrEmpty(line)) return null; var result = new List(); 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(); } /// /// Writes a DataTable to a CSV file, properly handling quoting. /// /// Used filesystem. /// The DataTable to write. /// The full path of the output CSV file. /// The delimiter to use in the CSV file. 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().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)))); } } } /// /// Reads a CSV file and returns it as a list of dictionaries. /// /// Used filesystem. /// Full path to the CSV file. /// The delimiter used in the CSV file. /// List of dictionaries representing the CSV data. public static List> CsvToDictionaryList(IFileSystem fs, string csvFile, string delimiter = ",") { if (!fs.File.Exists(csvFile)) throw new FileNotFoundException($"File '{csvFile}' not found!", csvFile); var result = new List>(); 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(); 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; } } }