ぽにょろん

思いついたこととメモ

NPOIでセルの値を文字列で取得する(C#)

忘れたいシリーズということで、ブログに残すことで頭から追い出したいというトピックです。

目次

TL;DR

  • Excelファイルをprogrammableに扱いたいということ自体が辛み
  • CellTypeを見て、参照プロパティを切り替える

検証環境

  • .NET Framework 4.8
  • DotNetCore.NPOI -Version 1.2.1
  • LINQPad5 v5.37.06(AnyCPU)

NPOIとは

OfficeファイルやOOXMLフォーマットのファイルを読み書きするライブラリです。
POIdotnetバージョンのようです。

Install-Package

nugetにdotnet core対応バージョンがあるので、特に理由がなければそれを使いましょう。

Install-Package DotNetCore.NPOI

ファイル読み込み

ここでの対象は、xlsxのみです。
書き込む必要がなければ、Readオンリーがおすすめです。

XSSFWorkbook LoadXSSFWorkBook(string path)
{
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        return new XSSFWorkbook(fs);
    }
}

セルの値を文字列化

NPOIの場合、入力値を指定されているデータ型で取ることが可能です。
そのため、それぞれに合ったプロパティを参照する必要があります。
大きな注意点がいくつかあります。

  • 数値と時刻を明確に切り離せないため、入力しているFormatに応じてロジックを作る必要がある
  • 関数が埋め込まれている場合には単純にデータが取り出せない(ケースがある)
  • NPOIで使われているビルドインフォーマットが英語基準
    • CultureInfo等での切り替えはない
    • 特に日付回りで、日本語環境とは書式が違うので注意

記事執筆時点の私が使ってる変換ロジックは以下の通りになりました。
汎用的には使えない可能性もありますが、少なくとも私のユースケースではこれで十分でした。

string GetValueString(ICell cell, IFormulaEvaluator eval = default, Func<ICell, CellType> condition = default)
{
    if (cell == null) return default;
    if (condition == null) condition = c => c.CellType;

    switch (condition(cell))
    {
        case CellType.String:
            return cell.StringCellValue;
        case CellType.Formula:
            if (cell.CachedFormulaResultType == CellType.Formula)
            {
                eval = eval ?? cell.Sheet.Workbook.GetCreationHelper().CreateFormulaEvaluator();
                eval.EvaluateInCell(cell);
            }
            return GetValueString(cell, eval, c => c.CachedFormulaResultType);
        case CellType.Numeric:
            if (IsBuildinDateFormat(cell.CellStyle.DataFormat))
            {
                return cell.DateCellValue.ToString(GetBuildinFormatString(cell.CellStyle));
            }
            return cell.NumericCellValue.ToString();
        case CellType.Boolean:
            return cell.BooleanCellValue.ToString();
        default:
            return default;
    }
}

string GetBuildinFormatString(ICellStyle style)
{
    switch (style.DataFormat)
    {
        case 14: return "yyyy/M/d"; //yyyy/m/d
        case 22: return "yyyy/M/d H:mm"; //yyyy/m/d h:mm
        case 55: return "yyyy年M月"; //yyyy年m月
        case 56: return "M月d日"; //m月d日
        case 57: return "yyyy.M.d"; //[$-411]ge.m.d
        case 58: return "yyyy年M月d日"; //[$-411]ggge年m月d日
     default: return style.GetDataFormatString();
    }
}


bool IsBuildinDateFormat(int formatIndex)
    => (55 <= formatIndex && formatIndex <= 58) || DateUtil.IsInternalDateFormat(formatIndex);

xlsxの入力値を全部参照

上記、メソッドを用意したうえで、LINQPad上で次の通り実行すると、
指定xlsx内に記載された値が文字列として表示される。

void Main()
{
    var path = @"D:\test.xlsx";
    var book = LoadXSSFWorkBook(path);
    foreach (IRow row in book.GetSheetAt(0))
        foreach (var cell in row)
        {
            GetValueString(cell).Dump();
        }
}

困ったときは

DotNetCore.NPOIはソースコードGithub*1にあるので、素直にそれを読みましょう。
また、基本的に考え方やAPI設計が同様のため、オリジナルのJava版や多言語での例や実装を参考にすることも可能です。