RowやCellをイテレータで処理する場合は未定義の行やセルに気を付ける


発生した問題

Apache POIを利用するシステム開発プロジェクトでは「指定したシートを読み込み、2次元配列に変換する」というようなAPIを作成することがあります。自分がかかわっているプロジェクトでもそのようなAPIが提供されており、おおよそ以下のように実装されていました (読み込ませるシートはすべて文字列になっている想定)

try (Workbook book = WorkbookFactory.create(Paths.get("target-book.xlsx").toFile())) {
    Sheet sheet = book.getSheet("target-sheet");

    List<List<String>> lists = new ArrayList<>();
    for (Row row : sheet) {
        List<String> list = new ArrayList<>();
        for (Cell cell : row) {
            list.add(cell.getStringCellValue());
        }
        lists.add(list);
    }

    doSomeThing(lists);
}

さてこの実装は少なくともこのAPIを提供したチームが公表していた仕様を満たしていませんでした。たとえばこのAPIを使って以下のようなExcelを読み込ませたとします。

このシートは以下のような二次元配列に変換されます。

[A1, B1, C1, D1, E1], 
[B2, D2],  
[A4, B4, C4, D4, E4]

しかしAPI提供チームがうたっていた仕様では、このExcelシートは以下のように変換されるはずでした。

[A1, B1, C1, D1, E1], 
[  , B2, ,   D2], 
[], 
[A4, B4, C4, D4, E4]

つまり、空の行や空のセルについても適切に処理するというのがAPI仕様でしたが、実装はそのようになっていませんでした。

原因と対策

原因はRowCellの読み込みにイテレータを利用していることです。SheetIteratable<Row>を、 RowIterable<Cell>をそれぞれ実装していますが、これらを利用すると、未定義の行や未定義のセルについては、存在しないものとしてスキップします。上述の例でいうと、3行目やA2セルやA3セルは、Apache POI上、未定義として扱われてしまい、イテレータ読み込みではないものとしてスキップされてしまいます。

こういう空の行や空のセルをうまくハンドリングするには、Sheet.getRowRow.getCellを使う必要があります。今回の場合は以下のように実装を修正しました。

try (Workbook book = WorkbookFactory.create(Paths.get("target-book.xlsx").toFile())) {
    Sheet sheet = book.getSheet("target-sheet");

    List<List<String>> lists = new ArrayList<>();
    for (int i = 0; i <= sheet.getLastRowNum(); i++) {
        Row row = sheet.getRow(i);
        List<String> list = new ArrayList<>();

        int len = (row == null) ? 0 : row.getLastCellNum();
        for (int j = 0; j < len; j++) {
            Cell cell = row.getCell(j);
            list.add((cell == null) ? "" : cell.getStringCellValue());
        }
        lists.add(list);
    }

    doSomeThing(lists);
}

環境情報 (pom.xml抜粋)

<!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.0.0</version>
</dependency>