Skip to content

Instantly share code, notes, and snippets.

@Fristi
Last active August 10, 2021 20:24
Show Gist options
  • Save Fristi/4945160 to your computer and use it in GitHub Desktop.
Save Fristi/4945160 to your computer and use it in GitHub Desktop.
CSV Parser in C#, mapping via CSV headers and C# attributes.
public class HistoricalItemData
{
[CsvEntry(HeaderName = "Item ID")]
public int ItemId { get; set; }
[CsvEntry(HeaderName = "Item Name")]
public string ItemName { get; set; }
[CsvEntry(HeaderName = "Avg Daily Posted")]
public double AverageDailyPosted { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class CsvEntryAttribute : Attribute
{
public string HeaderName { get; set; }
}
public class CsvParser
{
private class CsvDescriptor
{
public PropertyInfo PropertyInfo { get; set; }
public string HeaderName { get; set; }
public int Index { get; set; }
public CsvDescriptor(PropertyInfo propertyInfo, string attributeName)
{
PropertyInfo = propertyInfo;
HeaderName = attributeName;
}
}
public static IEnumerable<T> Parse<T>(Stream stream) where T : new()
{
var results = new List<T>();
var csvDescriptors = InitializeCsvDescriptors<T>();
using (var sr = new StreamReader(stream))
{
var boundIndexCsvDescriptors = BindIndexToCsvDescriptors(sr.ReadLine(), csvDescriptors);
var sortedCsvDescriptors = new SortedDictionary<int, CsvDescriptor>(boundIndexCsvDescriptors.ToDictionary(x => x.Index, x => x));
var line = string.Empty;
while (!string.IsNullOrEmpty(line = sr.ReadLine()))
{
results.Add(BindData<T>(line, sortedCsvDescriptors));
}
}
return results;
}
private static T BindData<T>(string line, IDictionary<int, CsvDescriptor> csvDescriptors) where T : new()
{
var data = line.Split(',');
var row = new T();
for (var i = 0; i < data.Length; i++)
{
if (csvDescriptors.ContainsKey(i))
{
var csvDescriptor = csvDescriptors[i];
var col = csvDescriptor.PropertyInfo.PropertyType.IsNumeric()
? data[i].Replace(".", ",")
: data[i];
csvDescriptor.PropertyInfo.SetValue(
obj: row,
value: Convert.ChangeType(col, csvDescriptor.PropertyInfo.PropertyType),
index: null
);
}
}
return row;
}
private static IEnumerable<CsvDescriptor> InitializeCsvDescriptors<T>() where T : new()
{
var targetType = typeof(T);
var properties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
return from prop in properties
let customAttributes = prop.GetCustomAttributes(typeof(CsvEntryAttribute), false).Cast<CsvEntryAttribute>()
from attribute in customAttributes
where !string.IsNullOrEmpty(attribute.HeaderName)
select new CsvDescriptor(prop, attribute.HeaderName);
}
private static IEnumerable<CsvDescriptor> BindIndexToCsvDescriptors(string headerLine, IEnumerable<CsvDescriptor> csvDescriptors)
{
var headers = headerLine.Split(',');
var descriptors = new List<CsvDescriptor>(csvDescriptors);
for (var i = 0; i < headers.Length; i++)
{
var header = headers[i];
var csvDescriptor = descriptors.FirstOrDefault(x => x.HeaderName == header);
if (csvDescriptor == null)
{
continue;
}
csvDescriptor.Index = i;
}
return descriptors;
}
}
public static class NumericTypeExtension
{
public static bool IsNumeric(this Type dataType)
{
if (dataType == null) throw new ArgumentNullException("dataType");
return (dataType == typeof(int)
|| dataType == typeof(double)
|| dataType == typeof(long)
|| dataType == typeof(short)
|| dataType == typeof(float)
|| dataType == typeof(Int16)
|| dataType == typeof(Int32)
|| dataType == typeof(Int64)
|| dataType == typeof(uint)
|| dataType == typeof(UInt16)
|| dataType == typeof(UInt32)
|| dataType == typeof(UInt64)
|| dataType == typeof(sbyte)
|| dataType == typeof(Single)
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment