📰

ExcelProviderとダックタイピングで似て非なるEXCELから共通データを取り出す

2022/05/04に公開

やりたかったこと

表題の通り、似て非なるEXCELが複数ありました。
そこから、共通項を取り出す必要がありました。

似て非なる、とは、

  • 同じヘッダの列が存在するが、列の位置が違う
  • あるファイルには存在するが、あるファイルには存在しないヘッダーがある
  • ヘッダの行も違ったりする

という状態でした。

単純にスクリプトですべてのEXCELをパースして読み取ることもできたのですが、各列の型を把握してパースしたりすると少し面倒なので、F#型プロバイダを使いたくなりました。

型プロバイダの詳細説明は割愛しますが、「ファイル内容から推測して、勝手にclassを生成してくれる機能」という風にとらえてもらえればいいと思います。
型プロバイダがない場合、それぞれのデータフォーマットに応じてクラス定義を行い、データをパースしてオブジェクトにセットする処理を実装する必要がありますが、型プロバイダではそれらがすべてショートカットできます。

ただ、今回の要件では、「似て非なる」ものが複数あるので、型プロバイダを使ってもそれぞれのデータを共通の型として読み込むことができません。似て非なる型が、たくさんできてしまいます。

そこで、F#のダックタイピング機能を使って、必要な値だけ抜き出して用いることとしました。

EXCELの型プロバイダである、ExcelProviderについては以下の記事を参考にさせていただきました。
https://zenn.dev/flipflap/articles/fsharp-typeprovider

F#でダックタイピングを行うにあたっては、以下の記事を参考にさせていただきました。
https://nobuhisa.hatenablog.jp/entry/20180815/p1

実践

ExcelProviderインストール

NuGetでExcelProviderをインストールします。

データの準備

使用するデータは以下としました。
実際にはもっとたくさんファイル・シート・列の種類がありましたが、簡略化しています。
DB1とDB2では、DB1のみ内径が存在します。


型プロバイダによる型生成

EXCELファイルから、型プロバイダにより型を取得します。
ファイル名、シート名、範囲を指定するだけで簡単です。

module DuckTyping

open FSharp.Interop.Excel

type Db1 = ExcelFile<"./Book.xlsx", "DB1", "B4:E8">
type Db2 = ExcelFile<"./Book.xlsx", "DB2", "B4:E8">

上記のように記述するだけで、型が生成されます。
Db1,Db2の行に相当する型を引数にとる関数を作ってみると、インテリセンスでプロパティが確認できます。
初めてこの機能に触る人は、魔法のように感じるのではないでしょうか。

ダックタイピング

続いてダックタイピングです。

正直私もなんでこれで動くのかよくわからないというか、あまり解説できないのでそのあたりは前述の記事などご参照ください。

let inline getOuterDiameter(data: ^a when ^a : (member 外径 : float)) = 
    (^a : (member 外径 : float) data)

記法がとても難解です。
inline を忘れるとうまくいきません。
コンパイル時に解釈できる必要があるためのようです。

これを用いることで、型が違っていたDB1,DB2で同じ関数を通じて外径を取得することができるようになりました。

この例だとあまりわかりませんが、getOuterDiameterがもっと複雑な、数十行の関数だったらと考えるとこのありがたみがわかります。

DB1,DB2それぞれを引数に取る似たような関数は作りたくないですし、現実にはDB1,DB2どころではなく6,7個の型がありました。

まとめ

F#でExcelProviderとDuckTypingを組み合わせてみました。

こういったことは動的型付け言語でしかできないと思っていたので、F#は選択肢からやむなくはずしていたのですが、今後は捗りそうです。
なんだかんだ言っても、列インデックスを意識しなくてよかったり、インテリセンスで各列の値にアクセスできるのは非常に楽です。

今後も使っていきたいと思います。

Discussion