54

I have a CSV file with a listing of varied data(datetime, decimal). Sample line from CSV:

Date,Open,High,Low,Close,Volume,Adj Close  //I need to skip this first line as well
2012-11-01,77.60,78.12,77.37,78.05,186200,78.05

I have a list of objects created that I want to read each of the lines into. The constructor for the objects is below, each of the fields from each CSV line is used and assigned here.

    public DailyValues(DateTime date, decimal open, decimal high, decimal low,
        decimal close, decimal volume, decimal adjClose)
        : this()
    {
        Date = date;
        Open = open;
        High = high;
        Low = low;
        Close = close;
        Volume = volume;
        AdjClose = adjClose;
    }

    List<DailyValues> values = new List<DailyValues>();

Is there an easy way to read each line of the CSV into my list values and appropriately assign each attribute (i.e. date, open, high)?

user3066571
  • 1,381
  • 4
  • 14
  • 37
  • There is a `TextFieldParser` class hidden away in the `Microsoft.VisualBasic.FileIO` namespace. Even though it says VisualBasic, you can use it with C# too. – cost Nov 06 '14 at 22:24
  • I would do this via Linq - see this post: http://stackoverflow.com/questions/3497699/csv-to-object-model-mapping – Kevin Nov 06 '14 at 22:24
  • @Kevin That doesn't fully conform to the CSV spec. There would be problems if there was any data that contained a comma within the data – cost Nov 06 '14 at 22:28
  • 1
    @cost - I completely agree, but looking at the example data I see no problems. – Kevin Nov 06 '14 at 22:29

1 Answers1

160

Why not just parse these explicitly? You have a limited number of properties, so it's not very difficult. Instead of using a constructor requiring many arguments, I used a static method that returns a new DailyValues instance as it's return type. This is similar to DateTime.FromBinary etc.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace CsvDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            List<DailyValues> values = File.ReadAllLines("C:\\Users\\Josh\\Sample.csv")
                                           .Skip(1)
                                           .Select(v => DailyValues.FromCsv(v))
                                           .ToList();
        }
    }

    class DailyValues
    {
        DateTime Date;
        decimal Open;
        decimal High;
        decimal Low;
        decimal Close;
        decimal Volume;
        decimal AdjClose;

        public static DailyValues FromCsv(string csvLine)
        {
            string[] values = csvLine.Split(',');
            DailyValues dailyValues = new DailyValues();
            dailyValues.Date = Convert.ToDateTime(values[0]);
            dailyValues.Open = Convert.ToDecimal(values[1]);
            dailyValues.High = Convert.ToDecimal(values[2]);
            dailyValues.Low = Convert.ToDecimal(values[3]);
            dailyValues.Close = Convert.ToDecimal(values[4]);
            dailyValues.Volume = Convert.ToDecimal(values[5]);
            dailyValues.AdjClose = Convert.ToDecimal(values[6]);
            return dailyValues;
        }
    }
}

Of course, you can still add a default constructor, and you will want to add exception handling in case the parsing fails (you can also use TryParse for that).

  • The File.ReadAllLines reads all lines from the CSV file into a string array.
  • The .Skip(1) skips the header line.
  • The .Select(v => DailyValues.FromCsv(v)) uses Linq to select each line and create a new DailyValues instance using the FromCsv method. This creates a System.Collections.Generic.IEnumerable<CsvDemo.DailyValues> type.
  • Finally, the .ToList() convers the IEnumerable to a List to match the type you want.

Instead of using Linq you could have simply used a foreach loop to add each DailyValues instance to your list.

grovesNL
  • 6,016
  • 2
  • 20
  • 32
  • I like this approach, and you could implement more complex CSV parsing in FromCsv if required. +1 – Kevin Nov 07 '14 at 12:07
  • 2
    This is much simpler than any other solution provided on stackoverflow :) – A Bright Worker Mar 25 '16 at 18:49
  • 2
    Simple and Effective ! +1 – anil Oct 22 '16 at 07:54
  • 18
    Isn't in flawed though, what if there was a comma in the actual data, example "Los Angeles, CA" would split across 2 seperate fields. OK if you know the data doesnt contain strings though. – Paul Feb 04 '17 at 10:57
  • 1
    @Paul: Definitely, but most implementations have different standards to handle comma/special character escaping (i.e. one way is to wrap values containing commas in double quotes, as you said). So you'd probably be better off modifying the logic for those cases when they are known, or using a library that handles CSV parsing instead. My answer assumes the simplest case as described in the question. – grovesNL Feb 04 '17 at 17:46
  • As said before, simple and effective ! +1 – Artur Kedzior Feb 13 '17 at 12:24
  • can this be done with a constructor to keep original variables private? – Mwspencer Apr 03 '18 at 04:02
  • @Mwspencer Yes, it's fine to use a constructor instead. Please create a new question if you're having a problem with it – grovesNL Apr 04 '18 at 13:55
  • you can acutally leave away the v => DailyValues.FromCsv(v) and just use .Select(DailyValues.FromCsv).ToList(); instead – Tim Gerhard Nov 22 '18 at 10:44
  • @TimGerhard you're right :-) I believe I was trying to be slightly more explicit by using a lambda, in case people were unfamiliar with creating/passing a delegate created from method groups like that – grovesNL Nov 22 '18 at 19:54
  • Probably is not a solution, i'm pretty beginner level! But could be a solution implement the statement `backslash` for correctly escape de comma directly in csv file? --> `"Los Angeles\, CA"`, or you have to check if there are some other value after the first comma encountered – Zenek Jan 14 '21 at 20:53