3

I am trying to save an F# record collection to a csv file using the .Net CsvHelper library. The problem is that option types are not being converted to string correctly.

#r "nuget: CsvHelper"

open System.IO 
open System.Globalization
open CsvHelper

type Record = { X : string; Y : float option }

let writeString x =
    use writer = new StringWriter()
    use csv = new CsvWriter(writer, CultureInfo.InvariantCulture)
    csv.WriteRecords(x)
    writer.ToString()

[{ X = "hi"; Y = Some 1.00}
 { X = "bye"; Y = None }]
|> writeString

I expect a blank value (or any other nan value that CsvProvider will understand) for the second field in the second data row here. Instead, CsvHelper is converting the None value to 0.

val it : string = "X,Value
hi,1
bye,0
"

I am aware that FSharp.Data.CsvProvider would let me convert this simple record to a CsvFile.Row that can be saved. But that's a poor solution when you have many fields because it is hard to keep the order of the fields in the CsvFile.Row straight (imagine 50 float fields). I'd prefer a solution in which the record fields are converted automatically.

nh2
  • 610
  • 3
  • 10

1 Answers1

6

It looks like there's no built-in support for F# in CsvHelper, so you probably have to write your own type converter to handle options. Here's a quick one I whipped up:

open System.IO 
open System.Globalization
open CsvHelper
open CsvHelper.TypeConversion

type OptionConverter<'T>() =
    inherit DefaultTypeConverter()
    override __.ConvertToString(value, row, memberMapData) =
        match value :?> Option<'T> with
            | None -> ""
            | Some x -> base.ConvertToString(x, row, memberMapData)

type Record = { X : string; Y : float option }

let writeString x =
    use writer = new StringWriter()
    use csv = new CsvWriter(writer, CultureInfo.InvariantCulture)
    csv.Context.TypeConverterCache.AddConverter<Option<float>>(OptionConverter<float>())
    csv.WriteRecords(x)
    writer.ToString()

[<EntryPoint>] 
let main _ =
    [{ X = "hi"; Y = Some 1.00}
     { X = "bye"; Y = None }]
        |> writeString
        |> printfn "%s"
    0

Output is:

X,Y
hi,1
bye,

FWIW, there are CSV libraries that are easier to use from F#, such as the FSharp.Data CSV provider.

Brian Berns
  • 15,499
  • 2
  • 30
  • 40
  • That looks really good! I think that in OptionConverter, instead of `None->null` we need `None -> ""` so that we get a "," after bye. Also, do you by any chance know how to use the [automapper](https://joshclose.github.io/CsvHelper/examples/configuration/class-maps/auto-mapping) with this so that I don't have to type out the Mapper for X? – nh2 Feb 27 '21 at 18:35
  • I use CsvProvider a lot, but it doesn't have an easy type-safe way to convert a record to a CsvFile.Row. `fun (x:Record) -> CsvFile.Row(x.X,x.Y)` is easy to result in mixed up fields when you have 50 float fields. – nh2 Feb 27 '21 at 18:46
  • I've updated my answer to auto-map and output the empty string for `None`. – Brian Berns Feb 27 '21 at 19:11