C# .NetCoreで、ボートレース結果の解析　読み込みその1

# やらなきゃいけないこと
前回からの続き。
C# .NetCoreで、ボートレース結果の解析　構造検討
https://qiita.com/TamanegiTarao/items/04f43d96e50cb3eeaa03

以下の情報取得を行えるようにする。

- 節間タイトル
- レースタイトル
- レース情報
- 選手結果

## 開発環境
- Windows10
- Visual Studio Community 2019
- C# .Net Core 3.1

## リスト
```
    public class RaceResults
    {
        public List<InternodeTitle> InternodeTitles;
        public List<RaceTitle> RaceTitles;
        public List<RaceResult> Results;
        public List<PlayerResult> PlayerResults;
    }
```

``InternodeTitles``、``RaceTitles``はそれぞれ、節間、レースタイトルのリストとして登録する。
``Results``はレース結果（各配当、人気）をリストとして登録する。
（解析時、配当オンリーの際はこちらのほうが、負荷が少ないため。）
``PlayerResult``は``InternodeTitles``,``ReceTitles``は各リストに紐づけるようにする。

## Shift-JISのエンコード
.NetCoreの場合 Shift-JISのEncodingを使用するには

```
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
```
をEncoding前までに宣言しておく必要がある。```System.Text```をusingしておくこと。
(https://qiita.com/sugasaki/items/0639ea9ca07f1ba7a9e0)

## List重複対策
上記の通り、節間タイトル、レースタイトルはリストで管理するため、
読み込んだ文字列がリスト内にあるか確認する必要がある。

```
if( List.Exist(x => x.SearchParameter == ReadParameter))
{
    /* 存在する際の処理 */
}
else
{
    /* 存在しない際の処理 */
}
```
https://csharp-ref.com/program_linq-lambda.html

## 読み込みの流れ
前回の例にとってデータの取得する位置を決める。
- 
```
STARTK　←無視
24KBGN　←１.開催場コードを取得
大　村［成績］     10/ 1      第６回夜の本命・波乱  第 5日　←無視
←無視
                            ＊＊＊　競走成績　＊＊＊←無視
←無視
          第６回夜の本命・波乱決定戦　　　　　　　　　　　←２.節間タイトルを取得　
←無視
   第 5日          2019/10/ 1                             ボートレース大　村←３.節間日、開催年月日の取得
←ここから無視
               －内容については主催者発行のものと照合して下さい－

   [払戻金]       ３連単           ３連複           ２連単         ２連複
           1R  3-1-2    3360    1-2-3     260    3-1    1560    1-3     290
          ...略...
          12R  1-4-6    2010    1-4-6     810    1-4     530    1-4     390


←ここまで無視
   1R       一　般　　　                 H1800m  曇り  風  北西　 1m  波　  1cm←４.レース番号、レースタイトル、風向、風速の取得
  着 艇 登番 　選　手　名　　ﾓｰﾀｰ ﾎﾞｰﾄ 展示 進入 ｽﾀｰﾄﾀｲﾐﾝｸ ﾚｰｽﾀｲﾑ 差し　　　←５.決まり手の取得
-------------------------------------------------------------------------------
  01  3 2841 吉　田　　　　稔 70   75  6.82   3    0.14     1.50.6←６.選手結果の取得
  ...略...
  06  5 3856 水　野　　暁　広 43   21  6.79   5    0.24      .  . ←６.選手結果の取得

        単勝     3         2150  ←７.ここからレース結果の取得
        複勝     3          170  1          120  
        ２連単   3-1       1560  人気     6 
        ２連複   1-3        290  人気     2 
        拡連複   1-3        170  人気     3 
                 2-3        140  人気     2 
                 1-2        120  人気     1 
        ３連単   3-1-2     3360  人気    12 
        ３連複   1-2-3      260  人気     1 ←７.ここまでレース結果の取得


  2～12Rは省略←４～７を繰り返す。
24KEND
22KBGN←１～７を繰り返す
...略
05KEND
FINALK
```

１～３の開催場や節間タイトルなどの全体的な情報と、４～７のレース毎の情報に分けることができる。

１～３の間と４～７の間は、それぞれ、行数で管理することが望ましそう。
欠場の場合はどのような記載になるかは動かしてから考える。

## コーディング
### 文字列は基本的に使いたくない。
たくさんのデータを拾いたいのでなるべく文字列で保持したくない。
レースタイトル等は仕方ないが、複数の要素しかないも文字列は数値で管理したいため、
以下の通り、Enumやintへの変換を行った

- 風向き

```
public enum WindDirection { None, N, NE, E, ES, S, SW, W, WN };
public static WindDirection WindDirectionNumber(string str)
{
    switch (str)
    {
        case "無風　": return WindDirection.None;
        case "北　　": return WindDirection.N;
        case "北東　": return WindDirection.NE;
        case "東　　": return WindDirection.E;
        case "南東　": return WindDirection.ES;
        case "南　　": return WindDirection.S;
        case "南西　": return WindDirection.SW;
        case "西　　": return WindDirection.W;
        case "北西　": return WindDirection.WN;
        default: return WindDirection.None;
    }
}
```
３文字固定で検索を行うようにした。

- 決まり手

```
public enum Kimarite { Nige, Makuri, Sashi, Makurisashi, Megumare, Nuki, Huseiritsu }

public static Kimarite KimariteNumber(string str)
{
    switch (str)
    {
        case "逃げ　　　": return Kimarite.Nige;
        case "差し　　　": return Kimarite.Sashi;
        case "まくり　　": return Kimarite.Makuri;
        case "まくり差し": return Kimarite.Makurisashi;
        case "抜き　　　": return Kimarite.Nuki;
        case "恵まれ　　": return Kimarite.Megumare;
        case "　　　　　": return Kimarite.Huseiritsu;
        default: return Kimarite.Nige;
    }
}
```
- 着順のフライング、失格、欠場
フライングや欠場の場合は数値ではなく特定の英数字になる。
単純にParseではフライングなのか欠場なのか判断できないため、
以下の通り、Parse失敗時は着順がつかなかった理由を判別できるようにした。

```
        public static int ResultNumber(string str)
        {
            string[] resultList =
            {
                "F ","L ","K0","K1",
                "S0","S1","S2"
            };

            int rtn;
            if (int.TryParse(str, out rtn) == false)
            {
                for (int i = 0; i < resultList.Length; i++)
                {
                    if (str == resultList[i])
                    {
                        return i * (-1);
                    }
                }
                return resultList.Length * (-1);
            }
            return rtn;
        }
```

### 文字列から数値への変換
``Parse``は変換ができないと例外が発生するため、``TryParse``を使用し、
汎用の関数を用意した。

```
        static public int IntParse(string str)
        {
            int rtn = 0;
            if(!int.TryParse(str,out rtn))
            {
                rtn = 0;
            }
            return rtn;
        }

        static public double DoubleParse(string str)
        {
            double rtn = 0.0;
            if (!double.TryParse(str, out rtn))
            {
                rtn = 0;
            }
            return rtn;
        }
```

## 実際にリードして読み込む
今までの内容を踏まえてリード部分を作成した。

```
public void Read(string file)
{
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
    using (var sr = new StreamReader(file, Encoding.GetEncoding("shift_jis")))
    {
        InternodeTitle internodeTitle = new InternodeTitle();
        RaceTitle raceTitle;
        RaceInfo raceInfo = new RaceInfo();
        RaceResult raceResult = new RaceResult();
        Race raceBase = new Race();
        Race race;
        int inLineInfo = -9999;
        int inLinePlayer = -9999;
        int place = 0;
        bool entryfirst = false;
        bool nextKakurenSecond = false;
        bool nextKakurenThired = false;
        while (sr.EndOfStream == false)
        {
            var line = sr.ReadLine();

            if (line.Contains("KBGN"))
            {   // １.開催場コードの取得
                place = Lib.IntParse(line.Replace("KBGN", ""));
                inLineInfo = 0;
            }
            else if (inLineInfo == 5) // 文字列を含むかどうかで拾うのがむつかしい
            {   // ２.節間タイトルを取得
                // https://csharp-ref.com/program_linq-lambda.html
                string title = _StringExtraction(line, 10, 50);
                if (InternodeTitles.Exists(x => x.Title == title))
                {
                    internodeTitle = InternodeTitles.OrderBy(x => x.Start).FirstOrDefault();
                    entryfirst = false;
                }
                else
                {
                    internodeTitle = new InternodeTitle();
                    internodeTitle.Title = title;
                    entryfirst = true;
                    InternodeTitles.Add(internodeTitle);
                }
            }
            else if (inLineInfo == 7)// 文字列を含むかどうかで拾うのがむつかしい
            {   // ３.節間日、開催年月日の取得
                raceBase.Place = place;
                raceBase.InDay = Lib.IntParse(_StringExtraction(line, 5, 2));
                DateTime dateTime = new DateTime(Lib.IntParse(_StringExtraction(line, 19, 4)),
                                                    Lib.IntParse(_StringExtraction(line, 24, 2)),
                                                    Lib.IntParse(_StringExtraction(line, 27, 2)));
                raceBase.Date = dateTime;
                if (entryfirst)
                    internodeTitle.Start = dateTime;
                internodeTitle.End = dateTime;

            }
            else if (line.Contains("H1800m"))
            {   // ４.レース番号、レースタイトル、風向、風速の取得
                race = raceBase.Clone();
                race.RaceNum = Lib.IntParse(_StringExtraction(line, 2, 2));
                // レースタイトル
                var title = _StringExtraction(line, 12, 12);
                if (RaceTitles.Exists(x => x.Title == title))
                {
                    raceTitle = RaceTitles.Where(x => x.Title == title).FirstOrDefault();
                }
                else
                {
                    raceTitle = new RaceTitle() { Title = title };
                    RaceTitles.Add(raceTitle);
                }
                // レース情報
                raceInfo = new RaceInfo();
                raceResult = new RaceResult();
                raceInfo.WindDirectionSet(_StringExtraction(line, 59, 6));
                raceInfo.WindMetor = Lib.IntParse(_StringExtraction(line, 65, 2));
                raceInfo.RaceTitle = raceTitle;
                raceInfo.Race = race;
                raceInfo.RaceResult = raceResult;
                raceInfo.InternodeTitle = internodeTitle;
            }
            else if (line.Contains(" 着 艇 登番 　選　手　名　　ﾓｰﾀｰ ﾎﾞｰﾄ 展示 進入 ｽﾀｰﾄﾀｲﾐﾝｸ ﾚｰｽﾀｲﾑ"))
            {   // ５.決まり手の取得
                raceInfo.KimariteSet(_StringExtraction(line, 66, 10));
                inLinePlayer = -1; // この次の行が --------------だから
            }
            else if (1 <= inLinePlayer && inLinePlayer <= 6)// 文字列を含むかどうかで拾うのがむつかしい
            {   // ６.選手結果の取得
                PlayerResult adder = new PlayerResult();
                adder.RaceInfo = raceInfo;
                adder.Number = Lib.IntParse(_StringExtraction(line, 8, 4));
                adder.Result = PlayerResult.ResultNumber(_StringExtraction(line, 2, 3));
                adder.Wakuban = Lib.IntParse(_StringExtraction(line, 6, 1));
                adder.Cource = Lib.IntParse(_StringExtraction(line, 46, 1));
                adder.Motor = Lib.IntParse(_StringExtraction(line, 30, 2));
                adder.Boat = Lib.IntParse(_StringExtraction(line, 34, 3));
                adder.Tenji = Lib.DoubleParse(_StringExtraction(line, 39, 4));
                adder.Start = Lib.DoubleParse(_StringExtraction(line, 51, 4));
                PlayerResults.Add(adder);
            }
            else if (6 < inLinePlayer && !line.Contains("不成立"))
            {   // ←７.レース結果の取得
                if (line.Contains("単勝"))
                {   // ここの特払い、傾向つかめれば統一したい。
                    if (line.Contains("特払い"))
                    {
                        raceResult.Tansho.First = -1;
                        raceResult.Tansho.Yen = Lib.IntParse(_StringExtraction(line, 27, 4));
                    }
                    else
                    {
                        raceResult.Tansho.First = Lib.IntParse(_StringExtraction(line, 17, 1));
                        raceResult.Tansho.Yen = Lib.IntParse(_StringExtraction(line, 25, 6));
                    }

                }
                if (line.Contains("複勝"))
                {
                    raceResult.Fukusho1.First = Lib.IntParse(_StringExtraction(line, 17, 1));
                    raceResult.Fukusho1.Yen = Lib.IntParse(_StringExtraction(line, 25, 6));
                    raceResult.Fukusho2.First = Lib.IntParse(_StringExtraction(line, 33, 1));
                    raceResult.Fukusho2.Yen = Lib.IntParse(_StringExtraction(line, 41, 6));
                }
                if (line.Contains("２連単") || line.Contains("２連複") || line.Contains("各連複") ||
                    nextKakurenSecond || nextKakurenThired)
                {
                    ResultTwo res = line.Contains("２連単") ? raceResult.NirenTan :
                                    line.Contains("２連複") ? raceResult.NirenFuku :
                                    line.Contains("各連複") ? raceResult.Kakuren1 :
                                    nextKakurenSecond ? raceResult.Kakuren2 : raceResult.Kakuren3;

                    res.First = Lib.IntParse(_StringExtraction(line, 17, 1));
                    res.Second = Lib.IntParse(_StringExtraction(line, 19, 1));
                    res.Yen = Lib.IntParse(_StringExtraction(line, 25, 6));
                    res.Popler = Lib.IntParse(_StringExtraction(line, 40, 3));
                    // 各連複の２．３番目は取れないので
                    nextKakurenSecond = res == raceResult.Kakuren1 ? true : false;
                    nextKakurenThired = res == raceResult.Kakuren2 ? true : false;
                }
                if (line.Contains("３連単") || line.Contains("３連複"))
                {
                    ResultThree res = line.Contains("３連単") ? raceResult.SanrenTan : raceResult.SanrenFuku;

                    res.First = Lib.IntParse(_StringExtraction(line, 17, 1));
                    res.Second = Lib.IntParse(_StringExtraction(line, 19, 1));
                    res.Thired = Lib.IntParse(_StringExtraction(line, 21, 1));
                    res.Yen = Lib.IntParse(_StringExtraction(line, 25, 6));
                    res.Popler = Lib.IntParse(_StringExtraction(line, 40, 3));
                    if (res == raceResult.SanrenFuku)
                        inLinePlayer = -9999;
                }
            }
            inLinePlayer++;
            inLineInfo++;
        }
    }
}
```
思ったより見にくい。保守するなら工夫が必要かも。

## 結果
例のファイルを読み込んで、結果を出力するようにしてみた。
結果を一部抜粋。

```
第６回夜の本命・波乱決定戦　　　　　　　　　　　　,一　般　　　,24,2019,10,01,5,1,0,2150,3,0,170,3,0,120,1,6,1560,3,1,2,290,1,3,0,0,0,0,0,0,0,0,0,0,0,0,12,3360,3,1,2,1,260,1,2,3,2,8,1,2841,1,3,3,70,75,6.82,0.14
第１３回多摩川蛭子カップ外向発売所開設２周年記念　,蛭子選抜戦　,5,2019,10,01,1,12,0,430,2,0,130,2,0,470,3,13,3480,2,3,9,2990,2,3,0,0,0,0,0,0,0,0,0,0,0,0,50,14740,2,3,4,11,2640,2,3,4,2,4,2,3909,1,2,2,49,167,6.72,0.19
```
## コード
作成したコードをGithubに上げました。
https://github.com/TamanegiTarao/BoatRaceResultReader/tree/master

## まとめ
ようやく、結果の読み込みが行えるようになった。
多くのファイルを読み込んでくと、排他処理追加する必要が出てきそうな感じがする。

今後は、細かい処理のと、スタート展示の情報が競争成績のファイルには存在していないため、
スタート展示のスクレイピングについてもやっていきたいと思う。

次回
C# .NetCoreで、ボートレース結果の解析　読み込みその2
https://qiita.com/TamanegiTarao/items/ca2d15ed550d87da6ed2