using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Tango.CSV { public class CsvDynamicWriter { private sealed class ColumnInfo { public string Name; public string Default; public int Index; public int CreationOrder; } private readonly Dictionary _colMap = new Dictionary(); private readonly List _cols = new List(); private int _creationCounter = 0; private readonly List> _rows = new List>(); private Dictionary _currentRow = new Dictionary(); public void Write(int value, string columnName, int defaultValue, int index) { Write(value.ToString(), columnName, defaultValue.ToString(), index); } public void Write(double value, string columnName, double defaultValue, int index) { Write(value.ToString(), columnName, defaultValue.ToString(), index); } public void Write(bool value, string columnName, bool defaultValue, int index) { Write(value.ToString(), columnName, defaultValue.ToString(), index); } public void Write(Enum value, string columnName, Enum defaultValue, int index) { Write(value.ToString(), columnName, defaultValue.ToString(), index); } /// /// Writes a value to the specified column in the current row. /// If the column does not exist, it is created with the given default value and index. /// Column order in the CSV header is by (Index asc, CreationOrder asc). /// public void Write(string value, string columnName, string defaultValue, int index) { ColumnInfo col; if (!_colMap.TryGetValue(columnName, out col)) { col = new ColumnInfo { Name = columnName, Default = defaultValue, Index = index, CreationOrder = _creationCounter++ }; _colMap[columnName] = col; _cols.Add(col); // Backfill all prior rows with the column's default. foreach (var row in _rows) { if (!row.ContainsKey(columnName)) row[columnName] = defaultValue; } } // If the column already exists, we keep the original index/default. // (If you need reindexing, add an explicit method to change col.Index.) _currentRow[columnName] = value ?? ""; } /// /// Moves to the next row, finalizing the current one. /// Missing cells get their column default value. /// public void Next() { if (_currentRow == null) _currentRow = new Dictionary(); foreach (var col in _cols) { if (!_currentRow.ContainsKey(col.Name)) _currentRow[col.Name] = col.Default ?? ""; } _rows.Add(_currentRow); _currentRow = new Dictionary(); } /// /// Saves the CSV to disk using the column order defined by (Index, CreationOrder). /// public void Save(string filePath) { // Auto-finalize last row if it has any values. if (_currentRow != null && _currentRow.Count > 0) Next(); var orderedCols = _cols .OrderBy(c => c.Index) .ThenBy(c => c.CreationOrder) .ToList(); var sb = new StringBuilder(); // Header sb.AppendLine(string.Join(",", orderedCols.Select(c => EscapeCsv(c.Name)))); // Rows foreach (var row in _rows) { var values = new List(orderedCols.Count); for (int i = 0; i < orderedCols.Count; i++) { var name = orderedCols[i].Name; string v; if (!row.TryGetValue(name, out v)) { // Shouldn't happen (Next() fills), but be defensive. v = orderedCols[i].Default ?? ""; } values.Add(EscapeCsv(v)); } sb.AppendLine(string.Join(",", values)); } File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); } private static string EscapeCsv(string input) { if (input == null) return ""; if (input.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0) return "\"" + input.Replace("\"", "\"\"") + "\""; return input; } } }