続　全銀固定長データ～複数件データを読む（Power Query）

前回の記事では単データのみに対応していました。結局、複数件のデータにも挑戦しましたので、書き残しておきます。

前回の記事：[全銀固定長データを1件だけ読んでみた](https://qiita.com/tanuki_phoenix/items/0b05d2e162872c040b18)

##複数件の固定長データとは（筆者の理解）

全銀固定長データは下記の4区分で構成されています。最初の1文字目でどの区分か判別できるように設計され、それぞれ120字を1単位とします。

- ヘッダーレコード（1始まり）
- データレコード（2始まり）
- トレーラーレコード（8始まり）
- エンドレコード（9始まり）
 
調べたところ、複数件となる場合でも1ファイルの中に納めるものの、その納め方について下記の2タイプが存在することが分かりました。

1. データレコードだけを繰り返すタイプ
2. ヘッダレコードからトレーラーレコードまでの3区分のセットを繰り返すタイプ（通称：マルチファイル）

本記事では、読もうとしているデータが2タイプのうち、どちらなのか判別し、どちらでも読めるよう、作ってみました。

##クエリ全体の構成
- 材料1：固定長データのテキスト
- 材料2：桁指定マスタ（前回と同じ）
- カスタム関数（サブ）：桁指定マスタに従ってテキストのリストをテーブルで返すもの（ファイルパーサ）
- カスタム関数（メイン）：固定長データが要件どおりか、マルチファイルか判別の上、ファイルパーサを実行する（fx_ファイル読取）

##コード
###固定長データ
前回の記事と異なり、複数件のデータを入れています。テスト用に各タイプ1個ずつ作りみました。
また、作成の都合上、改行コードを入れてます。したがって、変換する過程で改行を除去してから変換していきます。
※詳細エディタに貼ると文字列になるはずです。

```table:固定長データ_1
let
    ソース = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMjQyNFCAg/d797zfN+/93jXv93W93zf//b6O9/sa3u+b/X7vZgX8wMDCwNDAwNgYXdgAXcTQ0sDAyARDoYKCEVHaEUZYmLzf1/x+7973+1re7933ft/c9/umAd2p8X7vNuwuBANDEIHXJ0qxOtFKo44ZdQwJjrFAqIMx8KqnFrCkiy2YQCk2FgA=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type text) meta [Serialized.Text = true]) in type table [列1 = _t]),
    カスタム1 = Text.Combine(ソース[列1])
in
    カスタム1

```

```table:固定長データ_2
let
    ソース = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("7ZExCgIxEEWvElJZWEyyW2TPEnIXQQvxADaiXsBOsHLmSv8KroG4sBuChVjNK0IYJjM/vBit847MB/ATcgHfIAfIFbKHbCAn8N20oUCOqOvmZZpX3EDk+0WjMTato/VfzZjmhB6yBTNkBxbIGXIcw67Aj3rMjHsfze/kMGFqLpfmo1+hTqqok4w6KaiTKuoko04Kw1+2LLEpvQA=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type text) meta [Serialized.Text = true]) in type table [列1 = _t]),
    変更された型 = Table.TransformColumnTypes(ソース,{{"列1", type text}}),
    カスタム1 = Text.Combine(変更された型[列1])
in
    カスタム1
```

###桁指定シート（前回の記事の再掲）

```table:桁指定シート
let
    ソース = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("lZTbToNAEIbfhWsuoPUQn4X0XQgbBZSG1thSbUxKbS0apU1MNFmxPswyHK76Ci6tAZp2YbkggeX7d2bnn1lFEQjSCQqI9gsWBuNKEAWZPgSNCEIEqdk/9Ea0j+2LKXRERYg9H4x5sSYKrTpJ/sUfJbKWyToI14/pJAgxLoeTpYZi6HWp4oRTFjnzjOaD4RKl12riWuUEm2p3+cmn3LLobgnYKYdsN9Q2DplOeqnep97HY5/fPyoE+wnwIh68gv1FRed1whAboN1nIWpRVvP+rx7wLKeq+b1SsVCWI9U839bmDfxMQzyL7HFkqnkla3I/5hcLPuoRG3bAHuZj1Zaqs9/2AU0ldbvF7DLp4Sp5tveGvfqU7gL8ab62Cfr1MQ40txx57W4GSwf/gbPZkvcRvSYT14tnuBbOm7669KyGNzIyw16OjQn0jMQzwu/PaLCi+Bm3pHCtxaEpBlfiyYpxFs0jiGJmxfbyBRvs/AE=", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type text) meta [Serialized.Text = true]) in type table [項目名 = _t, 桁数 = _t, 区分 = _t]),
    変更された型 = Table.TransformColumnTypes(ソース,{{"項目名", type text}, {"桁数", Int64.Type}, {"区分", type text}})
in
    変更された型
```

### カスタム関数（サブ）
読み取った結果をテーブルで返します。
引数の「対象リスト」というのは、読み取る区分名のテキストをリストにしたもので、M言語なら、

```
{"ヘッダーレコード","データレコード"}
```
というようなものです。複数件データを読む場合、4区分をまとめて使う場面がないので、区分を指定し、その区分の桁マスタで分割します。

```query:ファイルパーサ
(対象リスト as list,固定長のリスト as list)=>
let
    桁指定 =Table.SelectRows( 桁指定シート, each List.Contains(対象リスト,[区分]) ),
    分割引数セット =[分け方指示 = List.Generate(()=>[index=0,累計桁数=0],
                                           each [index]<=List.Count(桁指定[桁数]),
                                           each [index=[index]+1,累計桁数=[累計桁数]+桁指定[桁数]{[index]}],
                                           each [累計桁数]
                             ),

                   //分け方指示とぴったり同じ数の列で入るのだが、最後に空列ができないとエラーになる。
                   名前リスト = List.Transform(Table.ToRecords(桁指定),
                                              each Text.Range(_[区分],0,3)&"_"&_[項目名]
                               )&{"ダミー"}
                   ],
    リストの各要素の分割 = Table.FromList(固定長のリスト,
                                        Splitter.SplitTextByPositions(分割引数セット[分け方指示],false),
                                        分割引数セット[名前リスト]),
    ダミー列を削除 = Table.RemoveColumns(リストの各要素の分割,{"ダミー"})
in
    ダミー列を削除
```

### カスタム関数（メイン）（fx_ファイル読取）
既述のサブ関数「ファイルパーサ」が作成されていることを前提にしています。

```query:fx_ファイル読取
(Source as text)=>
let
    //改行の入れ方は数パターンはあれど、cr,lfの2種をそれぞれ除去すれば、全パターンを消せる。
    改行削除 = Text.Remove(Source,{"#(cr)","#(lf)"}),
    ファイルパターン = List.Alternate( Text.ToList(改行削除),120-1,1,1),//ファイル種類の判別用。

    ファイル評価 = if Number.Mod(Text.Length(改行削除),120) > 0 then "文字数エラー：パターン不正です"
        else
        //3区分目の最初の文字でファイル型を識別する
        if ファイルパターン{2}="8" then 
            //マルチファイルパターンとの一致を確認
            if ファイルパターン =List.Repeat( List.Range({"1","2","8"},0,3),
                                             (List.Count(ファイルパターン)-1)/3 )
                               &{"9"} then
                let
                マルチリスト =Splitter.SplitTextByRepeatedLengths(360)(改行削除),
                マルチ完成物 =[主データ =ファイルパーサ({"ヘッダーレコード","データレコード","トレーラーレコード"},
                                                     List.RemoveLastN(マルチリスト,1)
                                        ),
                              エンド =ファイルパーサ({"エンドレコード"},{List.Last(マルチリスト)})
                             ]
                in
                マルチ完成物
            else
                "マルチファイル：パターン不正です"
        else
            //非マルチファイルパターンとの一致を確認
            if ファイルパターン ={"1"}
                                &List.Repeat( {"2"},(List.Count(ファイルパターン)-3) )
                                &{"8","9"} then
                let
                その他リスト =Splitter.SplitTextByRepeatedLengths(120)(改行削除),
                その他完成物=[ヘッダ   =ファイルパーサ( {"ヘッダーレコード"},  {その他リスト{0}} ),
                             データ   =ファイルパーサ( {"データレコード"},  List.Range(その他リスト,1,List.Count(その他リスト)-3) ),
                             トレーラ =ファイルパーサ( {"トレーラーレコード"},List.Range(その他リスト,List.Count(その他リスト)-2,1) ),
                             エンド   =ファイルパーサ( {"エンドレコード"},{List.Last(その他リスト)})
                            ]
                in
                その他完成物
            else
                "非マルチファイル：パターン不正です。"
in
    ファイル評価
```

##実行
###実行方法
数式バーに下記のようなコードを貼ります。

```
=fx_ファイル読取(固定長データ_1)
```

###結果
引数を「固定長データ_1」にした場合（タイプ1の結果）
区分別の1フィールドを割当、それぞれにテーブルが格納されています。データレコードのフィールドには無事、複数件データが入りました。（1行1データ）
![タイプ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370992/ea6486ea-5c74-01d1-5fc5-4d6cab399408.png)

引数を「固定長データ_2」にした場合（タイプ2：マルチファイルの結果）
主データには、ヘッダレコード、データレコード、トレーラーレコードの3つをまとめてパースして得たテーブルが入ります。こちらも、複数件データが入ています。
![タイプ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370992/f40171cb-eb2e-707e-5022-adc20701d578.png)

###おまけ：関数解説など
###Splitter.SplitTextByRepeatedLengths()
今回取り組む際に、@PowerBIxyz さんにSplitter関数の使い方を教わりましたので、早速使いました。

```query:例
Splitter.SplitTextByRepeatedLengths(4)(Text.Combine({"A".."Z"}))
```

結果
![splitter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370992/a7db93ce-c10b-ef82-8f8f-dc1bbcc0d3be.png)



引数を入れて、ようやく関数になるものです。試してはいないですが、他のSplitter関数群や、Combiner関数群、Comparer関数群でも同様の使い方ができると思われます。

###List.Alternate()
なかなか使いづらいので、念のため。

```query:例
List.Alternate({1..1000},119,1,1)
```
結果
![alternate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370992/6668bd83-d1eb-19fe-3eea-c04e05ae3daf.png)

リストを0始まりで数えた1番目の要素から、「119個飛ばして、その次の1個は残す」――というのをリストの終わりまで繰り返しているわけです。