Read values from CSV when columns names are vertical - c#

I have an implementation of CSV helper which currently reads CSV's in the traditional format:
Name, Address, Age
"Foo", "Foo's address", 24
"Bar", "Bar's address", 19
I use a class map to map the fields by name to my Person object in the following way:
using (var reader = new StreamReader(file, Encoding.UTF8))
{
using (var csvReader = new CsvReader(reader))
{
csvReader.Configuration.RegisterClassMap<ContentMapper>();
var records = csvReader.GetRecords<Person>().ToArray();
}
}
I need an implementation which reads the exact same data but in a vertical format which originates from data where the column names are vertical in the first column and the data follows in columns instead of rows.
Name, "Foo", "Bar"
Address, "Foo's address", "Bar's address"
Age, 24, 19
What would be the best way to handle a CSV in this format whilst retaining the original mapping?

Try this one
public class Person
{
public string Name { get; set; }
public string Address { get; set; }
public int Age { get; set; }
public List<Person> ReadFile(string path)
{
char[] charsToTrim1 = {'\\', ' ', '"', '\"'};
var fileData = File.ReadAllLines(path);
var outputData = new List<Person>();
for (var i = 0; i < fileData.Length; i++)
{
var tmpData = fileData[i].Split(',');
for (var j = 0; j < tmpData.Length; j++)
{
var t1 = tmpData[j].Trim(charsToTrim1);
if (j == 0)
continue;
switch (i)
{
case 0:
{
var tmPerson = new Person {Name = t1};
outputData.Add(tmPerson);
}
break;
case 1:
{
outputData[j - 1].Address = t1;
}
break;
case 2:
{
outputData[j - 1].Age = Convert.ToInt32(t1);
}
break;
}
}
}
return outputData;
}
}

This could probably be cleaned up a bit, but it does seem to work.
Use CsvHelper to pull in the records as List<dynamic>
Rotate the records into a new List<dynamic> so the first field in each row becomes the property name of the dynamic record.
Use CsvHelper to write the new list to memory.
Use CsvHelper to read the records back in using your ClassMap
public class Program
{
public static void Main(string[] args)
{
var flippedRecords = new List<dynamic>();
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csv = new CsvReader(reader))
{
writer.WriteLine("Name,Foo,Bar");
writer.WriteLine("Address,Foo's address,\"Bar's address with, comma\"");
writer.WriteLine("Age,24,19");
writer.Flush();
stream.Position = 0;
csv.Configuration.HasHeaderRecord = false;
// Get the records from the CSV file.
var records = csv.GetRecords<dynamic>().ToList();
// Rotate the records into a new dynamic list.
var rows = new List<IDictionary<string, object>>();
foreach (var row in records)
{
rows.Add(row as IDictionary<string, object>);
}
for (int i = 2; i <= rows[0].Count; i++)
{
var flippedRecord = new ExpandoObject() as IDictionary<string, object>;
foreach (var row in rows)
{
flippedRecord.Add((string)row["Field1"], row["Field" + i]);
}
flippedRecords.Add(flippedRecord);
}
}
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (CsvWriter csvWriter = new CsvWriter(writer))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csvReader = new CsvReader(reader))
{
// Write the new list to memory
csvWriter.WriteRecords(flippedRecords);
writer.Flush();
stream.Position = 0;
// Read in the person records using a ClassMap.
csvReader.Configuration.RegisterClassMap<PersonMap>();
var people = csvReader.GetRecords<Person>().ToArray();
}
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public int Age { get; set; }
}
public class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Map(m => m.FirstName).Name("Name");
Map(m => m.Address);
Map(m => m.Age);
}
}

Related

CSV mapper class does map from list

(updated version)
I'm reading dates from csv using csv mapper (CsvClassMap)
void Main()
{
using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Context.RegisterClassMap<LoanRecord >();
var records = csv.GetRecords<LoanRecord >();
}
}
public class LoanRecord : Model
{
public LoanRecord()
{
}
public string[] LoanStart_DateTime = new string[2];
public string[] LoanEnd_DateTime = new string[2];
}
Mapper class:
public sealed class LoanMapper: CsvClassMap<LoanRecord>
{
public LoanMapper()
{
Map(m => m.LoanStart_DateTime[0]).Index(0);
Map(m => m.LoanEnd_DateTime[0]).Index(1);
Map(m => m.LoanStart_DateTime[1]).Index(2);
Map(m => m.LoanEnd_DateTime[1]).Index(3);
}}
csv format:
LoanStart_DateTime1,LoanEnd_DateTime1,LoanStart_DateTime2,LoanEnd_DateTime2
01/12/2022 00:00,02/12/2022 00:00,23/05/2022 00:00,23/05/2022 03:00,
somehow my mapper class doesn't recognize the list 'Not a member access Parameter name: expression', any help would be appreciated.
Using the CSVHelper package, you can manually iterate over the csv as it's being read. It then just requires some indexing logic to create a class for each pair of columns (obviously changing the index increment if you need more/less columns per Class):
static void Main(string[] args)
{
using (var reader = new StreamReader(#"C:\code\local\CSVReader\Loans.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var records = new LoanRecord();
records.Loans = new List<Loan>();
int index = 0;
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
for(int i = 1; index < csv.HeaderRecord.Length; i++)
{
var loanItem = new Loan()
{
LoanStart = csv.GetField("LoanStart_DateTime" + i.ToString()),
LoanEnd = csv.GetField("LoanEnd_DateTime" + i.ToString())
};
records.Loans.Add(loanItem);
index += 2;
}
}
}
}
Classes:
public class LoanRecord
{
public List<Loan> Loans { get; set; }
}
public class Loan
{
public string LoanStart { get; set; }
public string LoanEnd { get; set; }
}
You'll then need to do something with your records but they'll be available as a list of loans:

Mapping data to a list when I have the position index

(I'm using CSVHelper package)
Hi,
I have an array of sbyte[] that holds the position of each header column in a CSV file. The array is defined as follows,
public sbyte[] ColumnIndex = new sbyte[Enum.GetNames(typeof(MyEnum)).Length];
and say I have a CSV file that looks like this:
col1,col2,col3,col4
name1,empId1,241682-27638-USD-CIGGNT ,1
name2,empId2,241682-27638-USD-OCGGINT ,1
name3,empId3,241942-37190-USD-GGDIV ,2
name4,empId4,241942-37190-USD-CHYOF ,1
name5,empId5,241942-37190-USD-EQPL ,1
name6,empId6,241942-37190-USD-INT ,1
name7,empId7,242066-15343-USD-CYJOF ,3
name8,empId8,242066-15343-USD-CYJOF ,3
name9,empId9,242066-15343-USD-CYJOF ,3
name10,empId10,241942-37190-USD-GGDIV ,2
Now calling the byte array and passing the Enum index would give me back where that position in the header row:
int conversion = (int)MyEnum.col3;
ColumnIndex[conversion]);
returns 2
Now this all works fine, but I seem to struggle to figure out how to map each column with its info to separate List<string> based off the position index I have.
I tried to put all fields in a single List<string> using this code:
public List<string> ParseEntire(aliasType type, string PathToFile) {
List<string> result = new List<string>();
using (TextReader fileReader = File.OpenText(PathToFile)) {
var csv = new CsvReader(fileReader, CultureInfo.InvariantCulture);
string value;
while (csv.Read()) {
for (int i = 0; csv.TryGetField<string>(i, out value); i++) {
result.Add(value);
}
}
}
return result;
}
However, that's useless because I won't be able to know the position of every header. I feel like there is an easier way to do it using the CSVHelper package and I'm just complicating a simple task. Any Help would be appreciated.
EDIT:
For the following Emun
public enum aliasType {
col5,
col4,
col3,
col2,
col1
}
After executing the code to map the position indices to ColumnIndex, it would look like this:
ColumnIndex {sbyte[4]}
[0] [-1]
[1] [3]
[2] [2]
[3] [1]
[4] [0]
I'm mainly doing this because I do not know what the header row contains. So, I extract as much info as I can and when I get back an index of -1, I know that that specific field doesn't exist.
UPDATE:
the following code uses the Intersect to compare against different aliases for the same header column I'm trying to extract.
public List<string> HeaderColumnParser(aliasType type, string PathToFile) {
List<string> result = new List<string>();
using (TextReader fileReader = File.OpenText(PathToFile)) {
var csv = new CsvReader(fileReader, CultureInfo.InvariantCulture);
CSVBOM extract = new CSVBOM("", CSVBOM.BOMFileType.csv);
csv.Read();
csv.ReadHeader();
string[] header = csv.Context.HeaderRecord;
IEnumerable<string> CommonHeaders;
foreach (aliasType foo in Enum.GetValues(typeof(aliasType))) {
int res = Convert.ToInt32(foo);
switch (res) {
case 0:
// get matching string
CommonHeaders = header.Intersect(ReferenceDesignatorAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 1:
CommonHeaders = header.Intersect(ManufacturersPartNumberAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 2:
CommonHeaders = header.Intersect(ValueAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 3:
CommonHeaders = header.Intersect(DescriptionShortAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 4:
CommonHeaders = header.Intersect(DescriptionLongAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 5:
CommonHeaders = header.Intersect(ManufacturerAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 6:
CommonHeaders = header.Intersect(DNIAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
case 7:
CommonHeaders = header.Intersect(DataSheetAliases);
ColumnIndex[res] = extract.ExtractIndexHeader(CommonHeaders, header);
break;
default:
throw new Exception("Alias type is not recognized");
}
}
}
return result;
}
My Enum and array of strings for the aliases:
public enum aliasType {
ReferenceDesignatorAliases,
ManufacturersPartNumberAliases,
ValueAliases,
DescriptionShortAliases,
DescriptionLongAliases,
ManufacturerAliases,
DNIAliases,
DataSheetAliases
}
//Returns -1 meaning not found
public sbyte[] ColumnIndex = new sbyte[Enum.GetNames(typeof(aliasType)).Length];
public string[] ReferenceDesignatorAliases = { "Reference Designator", "RefDes", "Designator", "Annotation" };
public string[] ManufacturersPartNumberAliases = { "Manufacturer's Part Number", "MPN", "PN", "part Number" };
public string[] ValueAliases = { "Value" };
public string[] DescriptionShortAliases = { "Description Short", "Description" };
public string[] DescriptionLongAliases = { "Description Long" };
public string[] ManufacturerAliases = { "Manufacturer", "MF" };
public string[] DNIAliases = { "DNI", "Do Not Install" };
public string[] DataSheetAliases = { "DataSheet", "Data Sheet" };
I believe I have a little better idea of what you are trying to accomplish. Let me know if this solves your issue.
public static void Main(string[] args)
{
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
writer.WriteLine("MF,RefDes,MPN,Value");
writer.WriteLine("name1,empId1,241682-27638-USD-CIGGNT ,1");
writer.WriteLine("name2,empId2,241682-27638-USD-OCGGINT ,1");
writer.WriteLine("name3,empId3,241942-37190-USD-GGDIV ,2");
writer.WriteLine("name4,empId4,241942-37190-USD-CHYOF ,1");
writer.Flush();
stream.Position = 0;
string[] ReferenceDesignatorAliases = { "Reference Designator", "RefDes", "Designator", "Annotation" };
csv.Read();
csv.ReadHeader();
var result = new List<string>();
if (csv.Context.HeaderRecord.Intersect(ReferenceDesignatorAliases).Count() > 0)
{
while (csv.Read())
{
if (csv.TryGetField(csv.GetFieldIndex(ReferenceDesignatorAliases), out string value))
{
result.Add(value);
}
}
}
}
Console.ReadKey();
}
Here is another option that gets all of the columns at once and then you can split them up into the individual column lists.
public class Program
{
public static void Main(string[] args)
{
List<Foo> records;
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
writer.WriteLine("MF,RefDes,MPN,Value");
writer.WriteLine("name1,empId1,241682-27638-USD-CIGGNT ,1");
writer.WriteLine("name2,empId2,241682-27638-USD-OCGGINT ,1");
writer.WriteLine("name3,empId3,241942-37190-USD-GGDIV ,2");
writer.WriteLine("name4,empId4,241942-37190-USD-CHYOF ,1");
writer.Flush();
stream.Position = 0;
csv.Configuration.RegisterClassMap<FooClassMap>();
records = csv.GetRecords<Foo>().ToList();
}
if (!records.All(r => r.ReferenceDesignator == null))
{
var ReferenceResult = records.Select(r => r.ReferenceDesignator).ToList();
}
if (!records.All(r => r.Manufacturer == null))
{
var ManufacturerResult = records.Select(r => r.Manufacturer).ToList();
}
Console.ReadKey();
}
}
public class Foo
{
public string ReferenceDesignator { get; set; }
public string ManufacturersPartNumber { get; set; }
public int? Value { get; set; }
public string DescriptionShort { get; set; }
public string DescriptionLong { get; set; }
public string Manufacturer { get; set; }
public string Dni { get; set; }
public string DataSheet { get; set; }
}
public class FooClassMap : ClassMap<Foo>
{
public FooClassMap()
{
Map(m => m.ReferenceDesignator).Optional().Name("Reference Designator", "RefDes", "Designator", "Annotation");
Map(m => m.ManufacturersPartNumber).Optional().Name("Manufacturer's Part Number", "MPN", "PN", "part Number");
Map(m => m.Value).Optional();
Map(m => m.DescriptionShort).Optional().Name("Description Short", "Description");
Map(m => m.DescriptionLong).Optional().Name("Description Long");
Map(m => m.Manufacturer).Optional().Name("Manufacturer", "MF");
Map(m => m.Dni).Optional().Name("DNI", "Do Not Install");
Map(m => m.DataSheet).Optional().Name("DataSheet", "Data Sheet");
}
}

CSV reading in C#, I just want to read some fields

I want to read a csv file that has 9 columns with headers and many data rows below, but I am just interested in three of the columns, and they are not contiguous. I have tried with this code but it doesn't work, it stops in the foreach loop with a runtime exception from CsvHelper "'Field with name 'Y' does not exist. You can ignore missing fields by setting MissingFieldFound to null.'".
The csv file is like this:
FrameNO , Ttotal, TNo, X, Y , Z , Speed , Intensity, ILog ;
1 , 9 , 1 , 0.08, 1.4 , 0 , 0 , 78 , 19 , ;;
1 , 9 , 2 ,0.1 , 1.56 , 0 , 0 , 228 , 28, ;;
using CsvHelper;
namespace RadarPrototipo.Clases
{
public class Foo
{
public int FrameNO { get; set; }
public double Y { get; set; }
public int Intensity { get; set; }
}
class CCalc
{
public double Calc(int f)
{
double d=1.5;
int inten=0;
using (var reader = new StreamReader("C:/Users/Usuario/Desktop/Uni/AlumnoInterno/grab.csv"))
using (var csv = new CsvReader(reader))
{
csv.Configuration.Delimiter = ",";
var records = csv.GetRecords<Foo>();
csv.Configuration.HeaderValidated = null;
foreach (var Foo in records)
{
if (Foo.FrameNO == f)
{
if (Foo.Y < 1.8 && Foo.Y > 1.5)
{
if (Foo.Intensity > inten)
{
inten = Foo.Intensity;
d = Foo.Y;
}
}
}
}
}
return d;
}
}
}
The function analyses the data on those three columns and selects the best answer according to the conditions, then returns the value Y which is a distance.
Any help is really thanked.
The following works for me.
public class Program
{
public static void Main(string[] args)
{
using (MemoryStream stream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(stream))
using (StreamReader reader = new StreamReader(stream))
using (CsvReader csv = new CsvReader(reader))
{
writer.WriteLine("FrameNO,Column2,Y,Column4,Column5,Column6,Intensity,Column8,Column9");
writer.WriteLine("1,two,1.123,four,five,six,10,eight,nine");
writer.WriteLine("2,two,2.345,four,five,six,20,eight,nine");
writer.Flush();
stream.Position = 0;
var records = csv.GetRecords<Foo>();
foreach (var Foo in records)
{
Console.WriteLine(Foo.FrameNO);
}
}
Console.ReadLine();
}
}
public class Foo
{
public int FrameNO { get; set; }
public double Y { get; set; }
public int Intensity { get; set; }
}
One thing you might try is setting your delimiter. Your cultural default might not be a comma. Also, if you have spaces between your data and the commas, you will need to set the TrimOptions.
using (var csv = new CsvReader(reader))
{
csv.Configuration.Delimiter = ",";
csv.Configuration.TrimOptions = TrimOptions.Trim;
var records = csv.GetRecords<Foo>();

CsvHelper : Adding a title using CsvHelper

I am using CsvHelper to convert dapper objects to CsvFiles. I am using classmaps to map properties for indices and name mapping. The issue is I need a row with the table title before the records are written as mentioned below:
My old code without the title:
using (var writer = new StreamWriter(#"C:\Users\NPandian\Desktop\test.csv", false, System.Text.Encoding.UTF8))
using (var csvWriter = new CsvWriter(writer))
{
var ReportName = "Test Class";
csvWriter.Configuration.RegisterClassMap(classMap);
csvWriter.WriteRecords(records);
writer.Close();
}
Old Csv:
My Current work around code:
using (var writer = new StringWriter())
using (var csvWriter = new CsvWriter(writer))
{
var ReportName = "Test Class";
csvWriter.Configuration.RegisterClassMap(classMap);
csvWriter.WriteRecords(records);
writer.Close();
return $"ReportName:, {ReportName},\n{csvWriter.Context.Writer.ToString()}";
}
My Questions:
1) Is it possible with CsvHelper?
2) If so How?
You can write fields and rows by hand.
void Main()
{
var records = new List<Foo>
{
new Foo { Id = 1, Name = "one" },
new Foo { Id = 2, Name = "two" },
};
using (var writer = new StringWriter())
using (var csv = new CsvWriter(writer))
{
csv.Configuration.RegisterClassMap<FooMap>();
csv.WriteField("Title:");
csv.WriteField("Title");
csv.NextRecord();
csv.WriteRecords(records);
writer.ToString().Dump();
}
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
public class FooMap : ClassMap<Foo>
{
public FooMap()
{
Map(m => m.Id).Index(0).Name("S.No.");
Map(m => m.Name).Index(1);
}
}
Output:
Title:,Title
S.No.,Name
1,one
2,two
CSV files have no titles. The question describes a flat text report, not a CSV file. CsvHelper, as the name implies, is a helper library that writes CSVs, it's not a full featured report generator.
The library allows other code to write arbitrary text before or after it finished though, as it works on top of an abstract TextWriter instead of a stream or file. One could even write extra text between records if required.
You can use the writer you already have to write whatever you want before or after the call to csvWriter.WriteRecords(records);, eg :
using (var writer = new StreamWriter(#"C:\Users\NPandian\Desktop\test.csv", false, System.Text.Encoding.UTF8))
using (var csvWriter = new CsvWriter(writer))
{
var ReportName = "Test Class";
csvWriter.Configuration.RegisterClassMap(classMap);
//Doesn't matter where this is called as long as it is before `WriteRecords`
writer.WriteLine($"ReportName:, {ReportName}");
csvWriter.WriteRecords(records);
//No need to explicitly close, that's what `using` is for
}
CsvWriter accepts any TextWriter object and just writes its data to that writer. It doesn't try to modify it in any other way. It won't affect any other text already written to that TextWriter
For CSVHelper version 15.0.0 and above use:
void Main()
{
var records = new List<Foo>
{
new Foo { Id = 1, Name = "one" },
new Foo { Id = 2, Name = "two" },
};
using (var writer = new StringWriter())
using (var csv = new CsvWriter(writer, CultureInfo.CurrentCulture))
{
csv.Configuration.RegisterClassMap<FooMap>();
csv.WriteField("Title:");
csv.WriteField("Title");
csv.NextRecord();
csv.WriteRecords(records);
writer.ToString().Dump();
}
}
public class Foo
{
public int Id { get; set; }
public string Name { get; set; }
}
public class FooMap : ClassMap<Foo>
{
public FooMap()
{
Map(m => m.Id).Index(0).Name("S.No.");
Map(m => m.Name).Index(1);
}
}

How can I write headers with spaces using CsvHelper?

I am using the CsvHelper library to generate a CSV file from an IEnumerable<Person>, where Person is a basic class.
public class Person
{
public string DisplayName { get; set; }
public int Age { get; set; }
}
I need to write quoted headers, i.e. instead of "DisplayName", the column in the resulting file should be "Display Name".
How can I do this?
Create a ClassMap.
void Main()
{
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
using (var reader = new StreamReader(stream))
using (var csv = new CsvWriter(writer))
{
var records = new List<Person>
{
new Test { DisplayName = "one", Age = 1},
new Test { DisplayName = "two", Age = 2 },
};
csv.Configuration.RegisterClassMap<PersonMap>();
csv.WriteRecords(records);
writer.Flush();
stream.Position = 0;
Console.WriteLine(reader.ReadToEnd());
}
}
public class Person
{
public string DisplayName { get; set; }
public int Age { get; set; }
}
public sealed class PersonMap : ClassMap<Person>
{
public PersonMap()
{
Map(m => m.DisplayName).Name("Display Name");
Map(m => m.Age);
}
}
Output:
Display Name,Age
one,1
two,2
I had no headers with workaround, so I did this quick workaround instead:
foreach (var property in typeof(MyCsvClass).GetProperties())
{
csvWriter.WriteField(property.Name.Replace('_', ' '));
}
csvWriter.NextRecord();
csvWriter.WriteRecords(models);
This takes the property names and replace underscore with space, so I could just name the properties with underscores instead of spaces, and it mapped correctly.
It requires, however, that you use the HasHeaderRecord = false option on the CsvWriter.

Categories