0

CsvHelper with class

Here's an F# script that uses CsvHelper and runs as expected:

#r "nuget: CsvHelper, 30.0.1"

open System.IO
open System.Net.Http
open CsvHelper

type WALCLRecord() =
    member val DATE = "" with get, set
    member val WALCL = "" with get, set

let uri = sprintf  "https://fred.stlouisfed.org/graph/fredgraph.csv?id=%s&cosd=%s" "WALCL" "2023-01-01"
        
let str = (new HttpClient()).GetStringAsync(uri).Result

use string_reader = new StringReader(str)
use csv_reader = new CsvReader(string_reader, System.Globalization.CultureInfo.InvariantCulture)

let records = csv_reader.GetRecords<WALCLRecord>()

let arr = records |> Array.ofSeq

for elt in arr do
    printfn "%10s %15s" elt.DATE elt.WALCL

exit 0

The output:

2023-01-04       8507429.0                                                                                                                                                                               
2023-01-11       8508587.0                                                                                                                                                                               
2023-01-18       8489039.0                                                                                                                                                                               
2023-01-25       8470557.0                                                                                                                                                                               
2023-02-01       8433610.0                                                                                                                                                                               
2023-02-08       8435369.0                                                                                                                                                                               
2023-02-15       8384767.0                                                                                                                                                                               
2023-02-22       8382190.0                                                                                                                                                                               
2023-03-01       8339684.0                                                                                                                                                                               
2023-03-08       8342283.0                                                                                                                                                                               
2023-03-15       8639300.0                                                                                                                                                                               
2023-03-22       8733787.0                                                                                                                                                                               
2023-03-29       8705942.0                                                                                                                                                                               
2023-04-05       8632384.0                                                                                                                                                                               
2023-04-12       8614797.0                                                                                                                                                                               
2023-04-19       8593263.0                                                                                                                                                                               
2023-04-26       8562768.0                                                                                                                                                                               
2023-05-03       8503994.0                                                                                                                                                                               
2023-05-10       8503017.0                                                                                                                                                                               
2023-05-17       8456760.0                                                                                                                                                                               
2023-05-24       8436255.0   

Note that it's using a class with properties to hold the data for each row:

type WALCLRecord() =
    member val DATE = "" with get, set
    member val WALCL = "" with get, set

CsvHelper with F# record

If I use an F# record instead:

type WALCLRecord = {
    DATE : string
    WALCL : float
}

it results in the following error:

CsvHelper.HeaderValidationException: Header with name 'dATE'[0] was not found.                                                                                                                           
Header with name 'wALCL'[0] was not found.                                                                                                                                                               
Headers: 'DATE', 'WALCL'                                                                                                                                                                                 
Headers: 'DATE', 'WALCL'                                                                                                                                                                                 
If you are expecting some headers to be missing and want to ignore this validation, set the configuration HeaderValidated to null. You can also change the functionality to do something else, like logging the issue.                                                                                                                                                                                            
                                                                                                                                                                                                         
IReader state:                                                                                                                                                                                           
   ColumnCount: 0                                                                                                                                                                                        
   CurrentIndex: -1                                                                                                                                                                                      
   HeaderRecord:                                                                                                                                                                                         
["DATE","WALCL"]                                                                                                                                                                                         
IParser state:                                                                                                                                                                                           
   ByteCount: 0                                                                                                                                                                                          
   CharCount: 11                                                                                                                                                                                         
   Row: 1                                                                                                                                                                                                
   RawRow: 1                                                                                                                                                                                             
   Count: 2                                                                                                                                                                                              
   RawRecord:                                                                                                                                                                                            
DATE,WALCL                                                                                                                                                                                               
                                                                                                                                                                                                         
                                                                                                                                                                                                         
   at CsvHelper.Configuration.ConfigurationFunctions.HeaderValidated(HeaderValidatedArgs args)                                                                                                           
   at CsvHelper.CsvReader.ValidateHeader(Type type)                                                                                                                                                      
   at CsvHelper.CsvReader.ValidateHeader[T]()                                                                                                                                                            
   at CsvHelper.CsvReader.GetRecords[T]()+MoveNext()                                                                                                                                                     
   at Microsoft.FSharp.Collections.SeqModule.ToArray[T](IEnumerable`1 source) in D:\a\_work\1\s\src\FSharp.Core\seq.fs:line 1037                                                                         
   at <StartupCode$FSI_0183>.$FSI_0183.main@() in c:\Users\dharm\Dropbox\Documents\net-liquidity.fsx\walcl-csvhelper.fsx:line 98                                                                         
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)                                                                                     
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)                                                                                                          
Stopped due to error                   

Question

Is there a way to use the record type with CsvHelper?

Notes

In this question, it seems like they're successfully using F# records with CsvHelper.

Similar in this issue.

NameAttribute

If I use NameAttribute to explicitly specify the field name:

type WALCLRecord = {
    [<CsvHelper.Configuration.Attributes.NameAttribute("DATE")>]
    date : string
    [<CsvHelper.Configuration.Attributes.NameAttribute("WALCL")>]
    walcl : float
}

the following is displayed:

CsvHelper.HeaderValidationException: Header with name 'date'[0] was not found.                                                                                                                           
Header with name 'walcl'[0] was not found.                                                                                                                                                               
Headers: 'DATE', 'WALCL'                                                                                                                                                                                 
Headers: 'DATE', 'WALCL'  
dharmatech
  • 8,979
  • 8
  • 42
  • 88
  • 1
    It looks like it is not recognizing the Name attribute and always makes the first character lowercase. I'm not sure how to do it in F#, but in C# you could use `CsvConfiguration` to ignore case. `PrepareHeaderForMatch = args => args.Header.ToLower()` https://stackoverflow.com/a/67167414/2355006 – David Specht May 30 '23 at 13:23
  • 1
    @DavidSpecht It looks like that works! I've added an answer based on your suggestion. – dharmatech May 30 '23 at 13:56

1 Answers1

1

Here's a version of the program based on David Specht's suggestion to consider setting PrepareHeaderForMatch:

#r "nuget: CsvHelper, 30.0.1"

open System.IO
open System.Net.Http
open CsvHelper

type WALCLRecord = {
    DATE : string
    WALCL : float
}

let config = new CsvHelper.Configuration.CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture)

config.PrepareHeaderForMatch <- fun args -> args.Header.ToLower()

let uri = sprintf  "https://fred.stlouisfed.org/graph/fredgraph.csv?id=%s&cosd=%s" "WALCL" "2023-01-01"
        
let str = (new HttpClient()).GetStringAsync(uri).Result

use string_reader = new StringReader(str)

use csv_reader = new CsvReader(string_reader, config)

let records = csv_reader.GetRecords<WALCLRecord>()

let arr = records |> Array.ofSeq

for elt in arr do
    printfn "%10s %15f" elt.DATE elt.WALCL

exit 0
dharmatech
  • 8,979
  • 8
  • 42
  • 88