using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
namespace Tango.CSV
{
///
/// CSV reader that supports unknown schemas and typed access with defaults.
/// - Case-insensitive column lookup
/// - Robust CSV parsing (RFC-4180 style: quotes, commas, escaped quotes)
/// - Typed Read with defaults: string, int, double, bool, and enums
///
public sealed class CsvDynamicReader
{
private readonly Dictionary _colIndex;
private readonly List _rows;
public IReadOnlyList Rows => _rows;
public char Delimiter { get; }
public CsvDynamicReader(string path, char delimiter = ',')
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (!File.Exists(path)) throw new FileNotFoundException("CSV file not found.", path);
Delimiter = delimiter;
_colIndex = new Dictionary(StringComparer.OrdinalIgnoreCase);
_rows = new List();
using (var sr = new StreamReader(path))
{
// Read to first non-empty line for headers
string headerLine;
do
{
headerLine = sr.ReadLine();
if (headerLine == null)
throw new InvalidDataException("CSV file has no header row.");
} while (string.IsNullOrWhiteSpace(headerLine));
var headers = ParseCsvLine(headerLine, Delimiter);
for (int i = 0; i < headers.Count; i++)
{
var clean = CleanHeader(headers[i]);
if (!_colIndex.ContainsKey(clean))
_colIndex.Add(clean, i);
// If duplicate header name appears, first one wins.
}
// Read all rows
string line;
while ((line = sr.ReadLine()) != null)
{
if (line.Length == 0) continue; // skip empty
var fields = ParseCsvLine(line, Delimiter).ToArray();
_rows.Add(new Row(this, fields));
}
}
}
internal bool TryGetIndex(string columnName, out int index)
{
if (columnName == null) { index = -1; return false; }
return _colIndex.TryGetValue(columnName.Trim(), out index);
}
private static string CleanHeader(string s)
{
if (string.IsNullOrEmpty(s)) return string.Empty;
// Trim quotes if header was quoted
var t = s.Trim();
if (t.Length >= 2 && t[0] == '"' && t[t.Length - 1] == '"')
{
t = t.Substring(1, t.Length - 2).Replace("\"\"", "\"");
}
// Remove BOM if present
t = t.Trim('\uFEFF').Trim();
return t;
}
///
/// RFC-4180-ish CSV line parser: supports quoted fields, commas, escaped quotes ("").
///
private static List ParseCsvLine(string line, char delimiter)
{
var result = new List();
if (line == null)
{
result.Add(string.Empty);
return result;
}
var sb = new System.Text.StringBuilder(line.Length);
bool inQuotes = false;
for (int i = 0; i < line.Length; i++)
{
var c = line[i];
if (inQuotes)
{
if (c == '"')
{
// Escaped quote?
if (i + 1 < line.Length && line[i + 1] == '"')
{
sb.Append('"');
i++; // skip next
}
else
{
inQuotes = false;
}
}
else
{
sb.Append(c);
}
}
else
{
if (c == '"')
{
inQuotes = true;
}
else if (c == delimiter)
{
result.Add(sb.ToString());
sb.Clear();
}
else
{
sb.Append(c);
}
}
}
result.Add(sb.ToString());
return result;
}
// ------- Row --------
public sealed class Row
{
private readonly CsvDynamicReader _reader;
private readonly string[] _values;
internal Row(CsvDynamicReader reader, string[] values)
{
_reader = reader;
_values = values ?? new string[0];
}
public bool Exists(string columnName)
{
int idx;
return _reader.TryGetIndex(columnName, out idx);
}
///
/// Typed read with a default fallback. Works for string, int, double, bool, and enums.
/// Usage: var v = row.Read("Col", 0); var s = row.Read("Col","def"); var b = row.Read("Flag", false);
///
public T Read(string columnName, T defaultValue)
{
int idx;
if (!_reader.TryGetIndex(columnName, out idx))
return defaultValue;
var raw = (idx >= 0 && idx < _values.Length) ? _values[idx] : null;
return ConvertValue(raw, defaultValue);
}
// -------- Conversion helpers --------
private static T ConvertValue(string raw, T defaultValue)
{
if (typeof(T) == typeof(string))
{
// For strings return raw as-is (trim optional)
object s = raw ?? (object)defaultValue ?? string.Empty;
return (T)s;
}
if (string.IsNullOrWhiteSpace(raw))
return defaultValue;
var trimmed = raw.Trim();
// int
if (typeof(T) == typeof(int))
{
int v;
if (int.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out v))
return (T)(object)v;
return defaultValue;
}
// double
if (typeof(T) == typeof(double))
{
double v;
if (double.TryParse(trimmed, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out v))
return (T)(object)v;
return defaultValue;
}
// bool (accepts true/false, 1/0, yes/no, y/n)
if (typeof(T) == typeof(bool))
{
bool bv;
if (TryParseBool(trimmed, out bv))
return (T)(object)bv;
return defaultValue;
}
// Enums (case-insensitive; also accept underlying numeric)
var t = typeof(T);
if (t.IsEnum)
{
try
{
// Numeric?
var underlying = Enum.GetUnderlyingType(t);
object numericObj;
if (TryParseNumeric(trimmed, underlying, out numericObj))
{
var boxed = Enum.ToObject(t, numericObj);
return (T)boxed;
}
T parsed;
if (TryParseEnum(trimmed, out parsed))
return parsed;
}
catch { /* fall through to default */ }
return defaultValue;
}
// Fallback: try ChangeType
try
{
object any = System.Convert.ChangeType(trimmed, typeof(T), CultureInfo.InvariantCulture);
return (T)any;
}
catch
{
return defaultValue;
}
}
private static bool TryParseBool(string s, out bool value)
{
// Standard
if (bool.TryParse(s, out value)) return true;
// Common variants
switch (s.Trim().ToLowerInvariant())
{
case "1":
case "yes":
case "y":
case "true":
case "t":
value = true; return true;
case "0":
case "no":
case "n":
case "false":
case "f":
value = false; return true;
}
value = false; return false;
}
private static bool TryParseNumeric(string s, Type numericType, out object boxed)
{
if (numericType == typeof(byte)) { byte v; if (byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(sbyte)) { sbyte v; if (sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(short)) { short v; if (short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(ushort)) { ushort v; if (ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(int)) { int v; if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(uint)) { uint v; if (uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(long)) { long v; if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
if (numericType == typeof(ulong)) { ulong v; if (ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) { boxed = v; return true; } }
boxed = null;
return false;
}
private static bool TryParseEnum(string s, out T value)
{
try
{
value = (T)Enum.Parse(typeof(T), s, ignoreCase: true);
return true;
}
catch
{
value = default(T);
return false;
}
}
}
}
}