CsvHelperというのを触ってみた（F#という言語で）

# Summary

CsvHelperのサンプルをF#で書いてみたというはなし

## サンプル

C#という言語でサンプルコードが書かれてます

```url
-- CsvHelper Sample
https://joshclose.github.io/CsvHelper/examples/
```

## Thanks 

いろいろと教えていただきました！（感謝！）

```
みどりさん（twitter id is @_midoliy_）
はぇ～さん（twitter id is @Haxe）
```

## Read

### sample1 

Get Class Record

```fsharp
(*

  Reading
  --------------------------
  01. Get Class Records

    Convert CSV rows into class objects.

  csv is
    Id,Name
    1,one

  result is
    seq [{ Id = 1 ; Name = "one" }]

*)

open System.IO
open System.Globalization
open CsvHelper

[<CLIMutable>]
type Foo = {
  Id:int
  Name:string
}

[<EntryPoint>]
let main argv =
  use reader = new StreamReader("./foo.csv")
  use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
  csv.GetRecords<Foo>()
  |> printfn "%A"

  0
```

### sample2

Get Dynamic Records

```fsharp
(*

  Reading
  --------------------------
  02. Get Dynamic Records

    Convert CSV rows into dynamic objects.
    Since there is no way to tell what type the properties should be,
    all the properties on the dynamic object are strings.

  csv is
    Id,Name
    1,one

  result is
    seq [Id, Name]
    seq [seq [1, one]]

*)

open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration

[<EntryPoint>]
let main argv =
  use reader = new StreamReader("./foo.csv")
  use csv = new CsvReader(reader,CultureInfo.CurrentCulture)

  let arr =
    // CSVを読込む
    csv.GetRecords<obj>()
    // obj を dict に型付けする
    |> Seq.map( fun x -> x :?> ExpandoObject :> IDictionary<string,obj>)
    // memory に乗せる（実際に計算する）
    |> Seq.toArray

  let keys = arr |> Seq.head |> Seq.map(fun (KeyValue(k,_)) -> k)
  let vals = arr |> Seq.map(fun x -> x |> Seq.map(fun (KeyValue(_,v)) -> v))

  printfn "%A" keys
  printfn "%A" vals



  0
```


### sample3

Get Anonyumouse Type Records

下記のコードは実現不可（いまのところ）

```fsharp
(*

  Reading
  --------------------------
  03. Get Anonyumouse Type Records

    Convert CSV rows into anonymous type objects.
    You just need to supply the anonymous type definition.

    -> unEnable in F# way
      fsharp / fslang-suggestions
        Mutable contents in Anonymous Records #732
        https://github.com/fsharp/fslang-suggestions/issues/732

  csv is
    Id,Name
    1,one

*)

open System.IO
open System.Globalization
open CsvHelper

[<EntryPoint>]
let main argv =
  use reader = new StreamReader("./foo.csv")
  use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
  csv.GetRecords(
    // unEnable in F# way, just now.
    {| mutable Id = 9999; mutable Name = "" |}
  )
  |> printfn "%A"

  0
```


### sample4

Enumerate Class Records

```fsharp
(*

  Reading
  --------------------------
  04. Enumerate Class Records

    Convert CSV rows into a class object that is re-used on every iteration of the enumerable.
    Each enumeration will hydrate the given record, but only the mapped members.
    If you supplied a map and didn't map one of the members, that member will not get hydrated with the current row's data.

    Be careful.
      Any methods that you call on the projection that force the evaluation of the IEnumerable, such as ToList(),
      you will get a list where all the records are the same instance you provided that is hydrated with the last record in the CSV file.

  csv is
    Id,Name
    1,one

  result is
    { Id = 1; Name = "one" }

*)



open System.IO
open System.Globalization
open CsvHelper

[<CLIMutable>]
type Foo = {
  Id:int
  Name:string
}

[<EntryPoint>]
let main argv =
  use reader = new StreamReader("./foo.csv")
  use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
  let records = csv.EnumerateRecords({ Id = 0 ; Name = "" })
  for r in records do
    printfn "%A" r

  0
```


### sample5

Reading by Hand

```fsharp
(*

  Reading
  --------------------------
  05. Reading by Hand

    Sometimes it's easier to not try and configure a mapping to match your class definition for various reasons.
    It's usually only a few more lines of code to just read the rows by hand instead.

  csv is
    Id,Name
    1,one

  result is
    seq [{ Id = 1 ; Name = "one" }]

*)

open System.IO
open System.Globalization
open CsvHelper

[<CLIMutable>]
type Foo = {
  Id:int
  Name:string
}

[<EntryPoint>]
let main argv =
  use reader = new StreamReader("./foo.csv")
  use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
  let records = new ResizeArray<Foo>()

  csv.Read() |> ignore
  // You must call ReadHeader() before any fields can be retrieved by name
  csv.ReadHeader() |> ignore

  while (csv.Read()) do
    records.Add(
      {
        Id = csv.GetField<int>("Id")
        Name = csv.GetField<string>("Name")
      }
    )

  records
  |> printfn  "%A"

  0
```


### sample6

Reading Multiple Data Sets

下記のコードだとエラーになる（追って調査）

```fsharp
(*

  Reading
  --------------------------
  06. Reading Multiple Data Sets

    For some reason there are CSV files out there that contain multiple sets of CSV data in them.
    You should be able to read files like this without issue.
    You will need to detect when to change class types you are retreiving.

  csv is

    FooId,Name
    1,foo

    BarId,Name
    07a0fca2-1b1c-4e44-b1be-c2b05da5afc7,bar

  result is
    seq [{ Id = "FooId"; Name = "Name" }; { Id = "1"; Name = "foo" }]
    seq [{ Id = "BarId"; Name = "Name" }; { Id = "07a0fca2-1b1c-4e44-b1be-c2b05da5afc7"; Name = "bar" }]

*)

open System
open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration

[<CLIMutable>]
type Foo = {
  (*
    Id:int にするとエラーになる

      Unhandled exception. CsvHelper.TypeConversion.TypeConverterException: The conversion cannot be performed.
      Text: 'FooId'
      MemberType: System.Int32
      TypeConverter: 'CsvHelper.TypeConversion.Int32Converter'
  *)
  // Id:int
  Id:string
  Name:string
}

[<Sealed>]
type FooMap () as this =
  inherit ClassMap<Foo>()
  do
    this.Map(fun x -> x.Id).Name([|"FooId"|]) |> ignore
    this.Map(fun x -> x.Name) |> ignore

[<CLIMutable>]
type Bar = {
  (*
    Id: System.Guidにするとエラーになる
      Unhandled exception. CsvHelper.ReaderException: An unexpected error occurred.
      ---> System.FormatException: Guid should contain 32 digits with 4 dashes (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
  *)
  // Id: System.Guid
  Id: string
  Name:string
}

[<Sealed>]
type BarMap () as this =
  inherit ClassMap<Bar>()
  do
    this.Map(fun x -> x.Id).Name([|"BarId"|]) |> ignore
    this.Map(fun x -> x.Name) |> ignore

[<EntryPoint>]
let main argv =

  let csvConfig : CsvConfiguration  =
    CsvConfiguration(CultureInfo.CurrentCulture)
    |> fun x ->
      x.IgnoreBlankLines <- false
      x.RegisterClassMap<FooMap>() |> ignore
      x.RegisterClassMap<BarMap>() |> ignore
      x

  use reader = new StreamReader("./multipleDataSets.csv")
  use csv = new CsvReader(reader,csvConfig)

  let fooRecords = new ResizeArray<Foo>()
  let barRecords = new ResizeArray<Bar>()
  let mutable isHeader = true

  while (csv.Read()) do

    if isHeader then
      csv.ReadHeader() |> ignore
      isHeader <- false

    if String.IsNullOrEmpty(csv.GetField(0)) then
      isHeader <- true

    else
      match ( csv.Context.HeaderRecord.[0] )with
      | "FooId" -> fooRecords.Add(csv.GetRecord<Foo>())
      | "BarId" -> barRecords.Add(csv.GetRecord<Bar>())
      | _   -> failwithf "error"

  fooRecords |> printfn "%A"
  barRecords |> printfn "%A"

  0

```


### sample7

Reading Multiple Record Types

```fsharp
(*

  Reading
  --------------------------
  07. Reading Multiple Record Types

    If you have CSV data where each row may be a different record type,
    you should be able to read based on a row type or something similar.

  csv is
    A,1,foo
    B,"あいうえお",bar

  result is
    seq [{ Id = 1            ; Name = "foo" }]
    seq [{ Id = "あいうえお" ; Name = "bar" }]

*)

open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration

[<CLIMutable>]
type Foo = {
  Id:int
  Name:string
}

[<Sealed>]
type FooMap () as this =
  inherit ClassMap<Foo>()
  do
    this.Map(fun x -> x.Id).Index(1) |> ignore
    this.Map(fun x -> x.Name).Index(2) |> ignore

[<CLIMutable>]
type Bar = {
  // Id: System.Guid
  Id : string
  Name:string
}

[<Sealed>]
type BarMap () as this =
  inherit ClassMap<Bar>()
  do
    this.Map(fun x -> x.Id).Index(1) |> ignore
    this.Map(fun x -> x.Name).Index(2) |> ignore

[<EntryPoint>]
let main argv =

  let csvConfig : CsvConfiguration  =
    CsvConfiguration(CultureInfo.CurrentCulture)
    |> fun x ->
      x.HasHeaderRecord <- false
      x.RegisterClassMap<FooMap>() |> ignore
      x.RegisterClassMap<BarMap>() |> ignore
      x

  use reader = new StreamReader("./multipleRecordTypes.csv")
  use csv = new CsvReader(reader,csvConfig)

  let fooRecords = new ResizeArray<Foo>()
  let barRecords = new ResizeArray<Bar>()

  while (csv.Read()) do

    match (csv.GetField(0)) with
    | "A" -> fooRecords.Add(csv.GetRecord<Foo>())
    | "B" -> barRecords.Add(csv.GetRecord<Bar>())
    | _   -> failwithf "error"

  fooRecords |> printfn "%A"
  barRecords |> printfn "%A"

  0
```

## Configuration ClassMap

### ConstantValue

```fsharp
(*

  Configuration
  --------------------------
  ConstantValue

    You can set a constant value to a property
    instead of mapping it to a field.

    => headerless csv only( header row needs to skip 1 Rows )
    => The constant statement must be listed below.

  csv is

    Id,Name
    1,one
    2,two

  result is

    { Id = 1
      Name = "one"
      IsDirty = true }
    { Id = 2
      Name = "two"
      IsDirty = true }

*)

open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration

[<CLIMutable>]
type Foo = {
  Id:int
  Name:string
  IsDirty:bool
}

[<Sealed>]
type FooMap () as this =
  inherit ClassMap<Foo>()
  do
    this.Map(fun m -> m.Id) |> ignore
    this.Map(fun m -> m.Name) |> ignore

    // The constant statement must be listed below.
    this.Map(fun m -> m.IsDirty).Constant(true) |> ignore

// util func
let skipRows (csv:CsvReader) i =
  for j in [1..i] do
    csv.Read() |> ignore

[<EntryPoint>]
let main argv =

  let csvConfig : CsvConfiguration  =
    CsvConfiguration(CultureInfo.CurrentCulture)
    |> fun x ->
      // headerless csv only
      x.HasHeaderRecord <- false
      x.RegisterClassMap<FooMap>() |> ignore
      x

  use reader = new StreamReader("./CsvHelperSample/csv/foo.csv")
  use csv = new CsvReader(reader,csvConfig)

  skipRows csv 1
  while (csv.Read()) do
    csv.GetRecord<Foo>()
    |> printfn "%A"

  0
```

### Validation

```fsharp

(*

  Configuration
  --------------------------
  Validation

    If you want to ensure your data conforms to some sort of standard,
    you can validate it.

  csv is

    Id,Name
    1,on-e
    2,two

  result is

    データ形式がおかしいです
    該当行番号:2
    該当データ:1,on-e

    データOK: { Id = 2 ;  Name = "two" }

  *)

open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration

[<CLIMutable>]
type Foo = {
  Id:int
  Name:string
}

[<Sealed>]
type FooMap () as this =
  inherit ClassMap<Foo>()
  do
    this.Map(fun x -> x.Id) |> ignore
    this.Map(fun x -> x.Name).Validate(fun field -> field.Contains("-") |> not ) |> ignore

[<EntryPoint>]
let main argv =

  let csvConfig : CsvConfiguration  =
    CsvConfiguration(CultureInfo.CurrentCulture)
    |> fun x ->
      x.RegisterClassMap<FooMap>() |> ignore
      x

  use reader = new StreamReader("./CsvHelperSample/csv/bar.csv")
  use csv = new CsvReader(reader,csvConfig)

  while (csv.Read()) do
    try
      csv.GetRecord<Foo>()
      |> printfn "データOK: %A"
    with
      | :? CsvHelper.FieldValidationException ->
        printfn "データ形式がおかしいです"
        printfn $"該当行番号:{csv.Context.RawRow}"
        printfn $"該当データ:{csv.Context.RawRecord.TrimEnd('\n')}"

      | _ -> printfn "その他エラー"

  0
```

現場からは以上です