POIUtils.java

package com.gh.mygreen.xlsmapper.util;

import java.awt.Point;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.poi.common.usermodel.HyperlinkType;
import org.apache.poi.hssf.model.InternalSheet;
import org.apache.poi.hssf.model.InternalWorkbook;
import org.apache.poi.hssf.record.DVRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.aggregates.DataValidityTable;
import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.Hyperlink;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.AreaReference;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressBase;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTDataValidation;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTDataValidations;
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gh.mygreen.xlsmapper.CellFormatter;
import com.gh.mygreen.xlsmapper.DefaultCellFormatter;
import com.github.mygreen.cellformatter.lang.ExcelDateUtils;

/**
 * Apache POIとJExcel APIの差を埋めるユーティリティクラス。
 *
 * @version 2.0
 * @author T.TSUCHIE
 *
 */
public class POIUtils {

    private static final Logger logger = LoggerFactory.getLogger(POIUtils.class);

    /** 標準のセルフォーマッター */
    private static CellFormatter defaultCellFormatter = new DefaultCellFormatter();

    /**
     * シートの種類を判定する。
     *
     * @since 2.0
     * @param sheet 判定対象のオブジェクト
     * @return シートの種類。不明な場合はnullを返す。
     * @throws IllegalArgumentException {@literal sheet == null}
     */
    public static SpreadsheetVersion getVersion(final Sheet sheet) {
        ArgUtils.notNull(sheet, "sheet");

        if(sheet instanceof HSSFSheet) {
            return SpreadsheetVersion.EXCEL97;

        } else if(sheet instanceof XSSFSheet) {
            return SpreadsheetVersion.EXCEL2007;
        }

        return null;
    }

    /**
     * シートの最大列数を取得する。
     * <p>{@literal jxl.Sheet.getColumns()}</p>
     * @param sheet シートオブジェクト
     * @return 最大列数
     * @throws IllegalArgumentException {@literal sheet == null.}
     */
    public static int getColumns(final Sheet sheet) {
        ArgUtils.notNull(sheet, "sheet");

        int minRowIndex = sheet.getFirstRowNum();
        int maxRowIndex = sheet.getLastRowNum();
        int maxColumnsIndex = 0;
        for(int i = minRowIndex; i <= maxRowIndex; i++) {
            final Row row = sheet.getRow(i);
            if(row == null) {
                continue;
            }

            final int column = row.getLastCellNum();
            if(column > maxColumnsIndex) {
                maxColumnsIndex = column;
            }
        }

        return maxColumnsIndex;
    }

    /**
     * シートの最大行数を取得する
     *
     * <p>{@literal jxl.Sheet.getRows()}</p>
     * @param sheet シートオブジェクト
     * @return 最大行数
     * @throws IllegalArgumentException {@literal sheet == null.}
     */
    public static int getRows(final Sheet sheet) {
        ArgUtils.notNull(sheet, "sheet");
        return sheet.getLastRowNum() + 1;
    }

    /**
     * シートから任意アドレスのセルを取得する。
     * @since 0.5
     * @param sheet シートオブジェクト
     * @param address アドレス(Point.x=column, Point.y=row)
     * @return セル
     * @throws IllegalArgumentException {@literal sheet == null or address == null.}
     */
    public static Cell getCell(final Sheet sheet, final Point address) {
        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notNull(address, "address");
        return getCell(sheet, address.x, address.y);
    }

    /**
     * シートから任意アドレスのセルを取得する。
     * @since 1.4
     * @param sheet シートオブジェクト
     * @param address セルのアドレス
     * @return セル
     * @throws IllegalArgumentException {@literal sheet == null or address == null.}
     */
    public static Cell getCell(final Sheet sheet, final CellPosition address) {
        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notNull(address, "address");
        return getCell(sheet, address.getColumn(), address.getRow());
    }

    /**
     * シートから任意アドレスのセルを取得する。
     *
     * <p>{@literal jxl.Sheet.getCell(int column, int row)}</p>
     * @param sheet シートオブジェクト
     * @param column 列番号(0から始まる)
     * @param row 行番号(0から始まる)
     * @return セル
     * @throws IllegalArgumentException {@literal sheet == null}
     */
    public static Cell getCell(final Sheet sheet, final int column, final int row) {
        ArgUtils.notNull(sheet, "sheet");

        Row rows = sheet.getRow(row);
        if(rows == null) {
            rows = sheet.createRow(row);
        }

        Cell cell = rows.getCell(column);
        if(cell == null) {
            cell = rows.createCell(column, CellType.BLANK);
        }

        return cell;
    }

    /**
     * 任意の行のセルを全て取得する。
     * <p> {@literal jxl.Seet.getRow(int row)}</p>
     * @param sheet シートオブジェクト
     * @param row 行番号(0から始まる)
     * @return 行レコード(カラムの集合)。
     *         ただし、シートの最大列数以下の場合、空のセルを補完する。
     * @throws IllegalArgumentException {@literal sheet == null}
     */
    public static Cell[] getRow(final Sheet sheet, final int row) {
        ArgUtils.notNull(sheet, "sheet");

        Row rows = sheet.getRow(row);
        if(rows == null) {
            rows = sheet.createRow(row);
        }
        int maxColumn = getColumns(sheet);
        Cell[] cells = new Cell[maxColumn];
        for(int i=0; i < maxColumn; i++) {
            Cell cell = rows.getCell(i);
            if(cell == null) {
                cell = rows.createCell(i, CellType.BLANK);
            }
            cells[i] = cell;
        }

        return cells;
    }

    /**
     * 任意の列のセルを全て取得する。
     * <p> {@literal jxl.Seet.getColumn(int col)}</p>
     * @param sheet
     * @param col 列番号(0から始まる)
     * @return 列レコード(行の集合)。
     *         ただし、シートの最大行数以下の場合、空のセルを補完する。
     * @throws IllegalArgumentException {@literal sheet == null}
     */
    public static Cell[] getColumn(final Sheet sheet, final int col) {
        ArgUtils.notNull(sheet, "sheet");

        int maxRow = getRows(sheet);
        Cell[] cells = new Cell[maxRow];
        for(int i=0; i < maxRow; i++) {
            Row rows = sheet.getRow(i);
            if(rows == null) {
                rows = sheet.createRow(i);

            }

            Cell cell = rows.getCell(col);
            if(cell == null) {
                cell = rows.createCell(col, CellType.BLANK);
            }

            cells[i] = cell;
        }

        return cells;
    }

    /**
     * フォーマッターを指定してセルの値を取得する
     *
     * @param cell
     * @param cellFormatter
     * @return フォーマットした文字列
     * @throws IllegalArgumentException {@literal cell or cellFormatter is null.}
     */
    public static String getCellContents(final Cell cell, final CellFormatter cellFormatter) {
        ArgUtils.notNull(cell, "cell");
        ArgUtils.notNull(cellFormatter, "cellFormatter");

        return cellFormatter.format(cell);

    }

    /**
     * 指定してセルの値が空かどうか判定する。
     * <p>ブランクセルなどの判定は優先的に行う。</p>
     * @param cell
     * @return
     */
    public static boolean isEmptyCellContents(final Cell cell) {
        return isEmptyCellContents(cell, defaultCellFormatter);
    }

    /**
     * フォーマッターを指定してセルの値が空かどうか判定する。
     * <p>ブランクセルなどの判定は優先的に行う。</p>
     * @param cell セル
     * @param cellFormatter セルのフォーマッタ
     * @throws IllegalArgumentException {@literal  sheet == null or cellFormatter == null.}
     * @return
     */
    public static boolean isEmptyCellContents(final Cell cell, final CellFormatter cellFormatter) {
        ArgUtils.notNull(cell, "cell");
        ArgUtils.notNull(cellFormatter, "cellFormatter");

        return getCellContents(cell, cellFormatter).isEmpty();
    }

    /**
     * 指定した書式のインデックス番号を取得する。シートに存在しない場合は、新しく作成する。
     * @param sheet シート
     * @param pattern 作成する書式のパターン
     * @return 書式のインデックス番号。
     * @throws IllegalArgumentException {@literal sheet == null.}
     * @throws IllegalArgumentException {@literal pattern == null || pattern.isEmpty().}
     */
    public static short getDataFormatIndex(final Sheet sheet, final String pattern) {
        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notEmpty(pattern, "pattern");

        return sheet.getWorkbook().getCreationHelper().createDataFormat().getFormat(pattern);

    }

    /**
     * セルに設定されている書式を取得する。
     * @since 1.1
     * @param cell セルのインスタンス。
     * @param cellFormatter セルのフォーマッタ
     * @return 書式が設定されていない場合は、空文字を返す。
     *         cellがnullの場合も空文字を返す。
     *         標準の書式の場合も空文字を返す。
     */
    public static String getCellFormatPattern(final Cell cell, final CellFormatter cellFormatter) {
        if(cell == null) {
            return "";
        }

        String pattern = cellFormatter.getPattern(cell);
        if(pattern.equalsIgnoreCase("general")) {
            return "";
        }

        return pattern;

    }

    /**
     * 指定した範囲のセルを結合する。
     * @param sheet
     * @param startCol
     * @param startRow
     * @param endCol
     * @param endRow
     * @return 結合した範囲のアドレス情報
     * @throws IllegalArgumentException {@literal sheet == null}
     */
    public static CellRangeAddress mergeCells(final Sheet sheet, int startCol, int startRow, int endCol, int endRow) {
        ArgUtils.notNull(sheet, "sheet");

        // 結合先のセルの値を空に設定する
        for(int r=startRow; r <= endRow; r++) {
            for(int c=startCol; c <= endCol; c++) {

                if(r == startRow && c == startCol) {
                    continue;
                }

                Cell cell = getCell(sheet, c, r);
                cell.setBlank();
            }
        }

        final CellRangeAddress range = new CellRangeAddress(startRow, endRow, startCol, endCol);
        sheet.addMergedRegion(range);
        return range;
    }

    /**
     * 指定したセルのアドレスの結合情報を取得する。
     * @since 0.5
     * @param sheet シート情報
     * @param rowIdx 行番号
     * @param colIdx 列番号
     * @return 結合していない場合nullを返す。
     */
    public static CellRangeAddress getMergedRegion(final Sheet sheet, final int rowIdx, final int colIdx) {
        ArgUtils.notNull(sheet, "sheet");

        final int num = sheet.getNumMergedRegions();
        for(int i=0; i < num; i ++) {
            final CellRangeAddress range = sheet.getMergedRegion(i);
            if(range.isInRange(rowIdx, colIdx)) {
                return range;
            }
        }

        return null;
    }

    /**
     * 指定した範囲の結合を解除する。
     * @param sheet
     * @param mergedRange
     * @return 引数で指定した結合が見つからない場合。
     */
    public static boolean removeMergedRange(final Sheet sheet, final CellRangeAddress mergedRange) {
        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notNull(mergedRange, "mergedRange");

        final String mergedAddress = mergedRange.formatAsString(sheet.getSheetName(), true);

        final int num = sheet.getNumMergedRegions();
        for(int i=0; i < num; i ++) {
            final CellRangeAddress range = sheet.getMergedRegion(i);
            final String rangeAddress = range.formatAsString(sheet.getSheetName(), true);
            if(rangeAddress.equals(mergedAddress)) {
                sheet.removeMergedRegion(i);
                return true;
            }
        }

        return false;
    }

    /**
     * 領域の列サイズ(横セル数)を計算します。
     *
     * @since 2.0
     * @param region 領域
     * @return 列サイズ(横セル数)。引数がnullの時は、0を返します。
     */
    public static int getColumnSize(final CellRangeAddress region) {
        if(region == null) {
            return 0;
        }
        return region.getLastColumn() - region.getFirstColumn() + 1;
    }

    /**
     * 領域の行サイズ(行セル数)を計算します。
     *
     * @since 2.0
     * @param region 領域
     * @return 行サイズ(行セル数)。引数がnullの時は、0を返します。
     */
    public static int getRowSize(final CellRangeAddress region) {
        if(region == null) {
            return 0;
        }
        return region.getLastRow() - region.getFirstRow() + 1;
    }

    /**
     * 指定した行の下に行を1行追加する
     * @param sheet
     * @param rowIndex 追加する行数
     * @return 追加した行を返す。
     */
    public static Row insertRow(final Sheet sheet, final int rowIndex) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notMin(rowIndex, 0, "rowIndex");

        // 最終行を取得する
        int lastRow = sheet.getLastRowNum();
        if(lastRow < rowIndex) {
            // データが定義されている範囲害の場合は、行を新たに作成して返す。
            return sheet.createRow(rowIndex);
        }

        sheet.shiftRows(rowIndex, lastRow+1, 1);
        return sheet.createRow(rowIndex);
    }

    /**
     * 指定した行を削除する。
     * <p>削除した行は上に詰める。
     * @since 0.5
     * @param sheet
     * @param rowIndex 削除する行数
     * @return 削除した行
     */
    public static Row removeRow(final Sheet sheet, final int rowIndex) {

        ArgUtils.notNull(sheet, "cell");
        ArgUtils.notMin(rowIndex, 0, "rowIndex");

        final Row row = sheet.getRow(rowIndex);
        if(row == null) {
            // 削除対象の行にデータが何もない場合
            return row;
        }

        sheet.removeRow(row);

        // 上に1つ行をずらす
        int lastRow = sheet.getLastRowNum();
        if(rowIndex +1 > lastRow) {
            return row;
        }

        sheet.shiftRows(rowIndex+1, lastRow, -1);

        return row;
    }


    /**
     * 座標をExcelのアドレス形式'A1'などに変換する
     * @param rowIndex 行インデックス
     * @param colIndex 列インデックス
     * @return
     */
    public static String formatCellAddress(final int rowIndex, final int colIndex) {
        return CellReference.convertNumToColString(colIndex) + String.valueOf(rowIndex+1);
    }

    /**
     * 座標をExcelのアドレス形式'A1'になどに変換する。
     * @param cellAddress セルの位置情報
     * @return
     * @throws IllegalArgumentException address == null.
     */
    public static String formatCellAddress(final Point cellAddress) {
        ArgUtils.notNull(cellAddress, "cellAddress");
        return formatCellAddress(cellAddress.y, cellAddress.x);
    }

    /**
     * セルのアドレス'A1'を取得する。
     * @param cell セル情報
     * @return IllegalArgumentException cell == null.
     */
    public static String formatCellAddress(final Cell cell) {
        ArgUtils.notNull(cell, "cell");
        return CellReference.convertNumToColString(cell.getColumnIndex()) + String.valueOf(cell.getRowIndex()+1);
    }

    /**
     * リンクのアドレスを判定する。
     * @param linkAddress リンクのアドレス(URL)
     * @return 不明な場合は{@link HyperlinkType#NONE}を返す。
     * @throws IllegalArgumentException linkAddress が空文字の場合。
     */
    public static HyperlinkType judgeLinkType(final String linkAddress) {

        ArgUtils.notEmpty(linkAddress, "linkAddress");

        if(linkAddress.matches(".*![\\p{Alnum}]+")) {
            // !A1のアドレスを含むかどうか
            return HyperlinkType.DOCUMENT;

        } else if(linkAddress.matches("[\\p{Alpha}]+[0-9]+")) {
            // A1の通常のアドレスの形式
            return HyperlinkType.DOCUMENT;

        } else if(linkAddress.matches(".+@.+")) {
            // @を含むかどうか
            return HyperlinkType.EMAIL;

        } else if(linkAddress.matches("[\\p{Alpha}]+://.+")) {
            // プロトコル付きかどうか
            return HyperlinkType.URL;

        } else if(linkAddress.matches(".+\\.[\\p{Alnum}]+")) {
            // 拡張子付きかどうか
            return HyperlinkType.FILE;

        } else {
            return HyperlinkType.NONE;
        }

    }

    /**
     * 入力規則の範囲を更新する。
     * @since 0.5
     * @param sheet シート
     * @param oldRegion 更新対象の範囲。
     * @param newRegion 新しい範囲。
     * @return true:更新完了。false:指定した範囲を持つ入力規則が見つからなかった場合。
     */
    public static boolean updateDataValidationRegion(final Sheet sheet,
            final CellRangeAddressList oldRegion, final CellRangeAddressList newRegion) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notNull(oldRegion, "oldRegion");
        ArgUtils.notNull(newRegion, "newRegion");

        if(sheet instanceof XSSFSheet) {

            final List<String> oldSqref = convertSqref(oldRegion);

            try {
                final XSSFSheet xssfSheet = (XSSFSheet) sheet;
                Field fWorksheet = XSSFSheet.class.getDeclaredField("worksheet");
                fWorksheet.setAccessible(true);
                CTWorksheet worksheet = (CTWorksheet) fWorksheet.get(xssfSheet);

                CTDataValidations dataValidations = worksheet.getDataValidations();
                if(dataValidations == null) {
                    return false;
                }

                for(int i=0; i < dataValidations.getCount(); i++) {
                    CTDataValidation dv = dataValidations.getDataValidationArray(i);

                    // 規則の範囲を比較し、同じならば範囲を書き換える。
                    @SuppressWarnings("unchecked")
                    List<String> sqref = new ArrayList<>(dv.getSqref());
                    if(equalsSqref(sqref, oldSqref)) {
                        List<String> newSqref = convertSqref(newRegion);
                        dv.setSqref(newSqref);

                        // 設定し直す
                        dataValidations.setDataValidationArray(i, dv);
                        return true;
                    }

                }

                return false;

            } catch(Exception e) {
                throw new RuntimeException("fail update DataValidation's Regsion.", e);
            }

        } else if(sheet instanceof HSSFSheet) {

            final HSSFSheet hssfSheet = (HSSFSheet) sheet;
            try {
                Field fWorksheet = HSSFSheet.class.getDeclaredField("_sheet");
                fWorksheet.setAccessible(true);
                InternalSheet worksheet = (InternalSheet) fWorksheet.get(hssfSheet);

                DataValidityTable dvt = worksheet.getOrCreateDataValidityTable();

                // シート内の入力規則のデータを検索して、一致するものがあれば書き換える。
                final AtomicBoolean updated = new AtomicBoolean(false);
                RecordVisitor visitor = new RecordVisitor() {

                    @Override
                    public void visitRecord(final Record r) {
                        if (!(r instanceof DVRecord)) {
                            return;
                        }

                        final DVRecord dvRecord = (DVRecord) r;
                        final CellRangeAddressList region = dvRecord.getCellRangeAddress();
                        if(equalsRegion(region, oldRegion)) {

                            // 一旦既存の範囲を削除する。
                            while(region.countRanges() != 0) {
                                region.remove(0);
                            }

                            // 新しい範囲を追加する。
                            for(CellRangeAddress newRange : newRegion.getCellRangeAddresses()) {
                                region.addCellRangeAddress(newRange);
                            }

                            updated.set(true);
                            return;
                        }
                    }
                };

                dvt.visitContainedRecords(visitor);

                return updated.get();

            } catch(Exception e) {
                throw new RuntimeException("fail update DataValidation's Regsion.", e);
            }
        } else {
            throw new UnsupportedOperationException("not supported update dava validation's region for type " + sheet.getClass().getName());
        }

    }

    /**
     * CellRangeAddressを文字列形式のリストに変換する。
     * @since 0.5
     * @param region
     * @return
     */
    private static List<String> convertSqref(final CellRangeAddressList region) {

        List<String> sqref = new ArrayList<>();
        for(CellRangeAddress range : region.getCellRangeAddresses()) {
            sqref.add(range.formatAsString());
        }

        return sqref;

    }

    /**
     * 文字列形式のセルの範囲が同じかどうか比較する。
     * @since 0.5
     * @param sqref1
     * @param sqref2
     * @return
     */
    public static boolean equalsSqref(final List<String> sqref1, final List<String> sqref2) {

        if(sqref1.size() != sqref2.size()) {
            return false;
        }

        Collections.sort(sqref1);
        Collections.sort(sqref2);

        final int size = sqref1.size();
        for(int i=0; i < size; i++) {
            if(!sqref1.get(i).equals(sqref2.get(i))) {
                return false;
            }
        }

        return true;

    }

    /**
     * 文字列形式のセルの範囲が同じかどうか比較する。
     * @since 0.5
     * @param region1
     * @param region2
     * @return
     */
    public static boolean equalsRegion(final CellRangeAddressList region1, final CellRangeAddressList region2) {

        return equalsSqref(convertSqref(region1), convertSqref(region2));

    }

    /**
     * テンプレートの入力規則の制約「リスト」を追加する。
     * <p>POI-3.7以上が必要。
     * @param sheet シート
     * @param constraints 制約とするコレクションの中身
     * @param startPosition 開始位置
     * @param endPosition 終了位置
     */
    public static void setupExplicitListConstaint(final Sheet sheet, final Collection<String> constraints,
            final Point startPosition, final Point endPosition) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notEmpty(constraints, "constraints");
        ArgUtils.notNull(startPosition, "startPosition");
        ArgUtils.notNull(endPosition, "endPosition");

        setupExplicitListConstaint(sheet, constraints.toArray(new String[constraints.size()]),
                startPosition, endPosition);
    }

    /**
     * テンプレートの入力規則の制約「リスト」を追加する。
     * <p>POI-3.7以上が必要。
     * @param sheet シート
     * @param constraints 制約とするリストの中身
     * @param startPosition 開始位置
     * @param endPosition 終了位置
     */
    public static void setupExplicitListConstaint(final Sheet sheet, final String[] constraints,
            final Point startPosition, final Point endPosition) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notEmpty(constraints, "constraints");
        ArgUtils.notNull(startPosition, "startPosition");
        ArgUtils.notNull(endPosition, "endPosition");

        final DataValidationHelper helper = sheet.getDataValidationHelper();
        final DataValidationConstraint constraint = helper.createExplicitListConstraint(constraints);
        setupConstaint(sheet, constraint, startPosition, endPosition);

    }

    /**
     * テンプレートの入力規則の制約「リスト」を式形式で追加する。
     * <p>POI-3.7以上が必要。
     * @param sheet シート
     * @param listFormula 入力規則の式('='は含まない)
     * @param startPosition 設定するセルの開始位置
     * @param endPosition 設定するセルの終了位置
     */
    public static void setupFormulaListConstaint(final Sheet sheet, final String listFormula,
            final Point startPosition, final Point endPosition) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notEmpty(listFormula, "listFormula");
        ArgUtils.notNull(startPosition, "startPosition");
        ArgUtils.notNull(endPosition, "endPosition");

        final DataValidationHelper helper = sheet.getDataValidationHelper();
        final DataValidationConstraint constraint = helper.createFormulaListConstraint("=" + listFormula);
        setupConstaint(sheet, constraint, startPosition, endPosition);
    }

    /**
     * 指定した範囲のセルに制約を追加する。
     * <p>POI-3.7以上が必要。
     * @param sheet シート
     * @param constraint 制約
     * @param startPosition 設定するセルの開始位置
     * @param endPosition 設定するセルの終了位置
     */
    public static void setupConstaint(final Sheet sheet, final DataValidationConstraint constraint,
            final Point startPosition, final Point endPosition) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notNull(constraint, "constraint");
        ArgUtils.notNull(startPosition, "startPosition");
        ArgUtils.notNull(endPosition, "endPosition");

        final DataValidationHelper helper = sheet.getDataValidationHelper();

        final CellRangeAddressList region = new CellRangeAddressList(
                startPosition.y, endPosition.y,
                startPosition.x, endPosition.x
                );
        final DataValidation dataValidation = helper.createValidation(constraint, region);
        sheet.addValidationData(dataValidation);
    }

    /**
     * 指定した範囲の名前を登録する。
     * <p>POI-3.7以上が必要。
     * <p>指定した名前が既に存在する場合は、新しい範囲に書き換える。
     * @param sheet シート
     * @param name 名前
     * @param startPosition 設定するセルの開始位置
     * @param endPosition 設定するセルの終了位置
     * @return
     */
    public static Name defineName(final Sheet sheet, final String name,
            final Point startPosition, final Point endPosition) {

        ArgUtils.notNull(sheet, "sheet");
        ArgUtils.notEmpty(name, "name");
        ArgUtils.notNull(startPosition, "startPosition");
        ArgUtils.notNull(endPosition, "endPosition");

        final Workbook workbook = sheet.getWorkbook();
        Name nameObj = workbook.getName(name);
        if(nameObj == null) {
            nameObj = workbook.createName();
            nameObj.setNameName(name);
        }

        final AreaReference areaRef = buildNameArea(sheet.getSheetName(), startPosition, endPosition,
                sheet.getWorkbook().getSpreadsheetVersion());
        nameObj.setRefersToFormula(areaRef.formatAsString());

        return nameObj;

    }

    /**
     * 名前の範囲の形式を組み立てる。
     * <code>シート名!$A$1:$A:$5</code>
     * @param sheetName シート名
     * @param startPosition 設定するセルの開始位置
     * @param endPosition 設定するセルの終了位置
     * @param sheetVersion シートの形式
     * @return
     */
    public static AreaReference buildNameArea(final String sheetName,
            final Point startPosition, final Point endPosition, SpreadsheetVersion sheetVersion) {

        ArgUtils.notEmpty(sheetName, "sheetName");
        ArgUtils.notNull(startPosition, "startPosition");
        ArgUtils.notNull(endPosition, "endPosition");

        final CellReference firstRefs = new CellReference(sheetName, startPosition.y, startPosition.x, true, true);
        final CellReference lastRefs = new CellReference(sheetName, endPosition.y, endPosition.x, true, true);

        return new AreaReference(firstRefs, lastRefs, sheetVersion);
    }

    /**
     * セルの範囲が重複(交錯)しているかどうか判定する。
     * <p>このメソッドは、POI-3.14で追加されたメソッド{@literal Sheet#intersects(...)}と後方互換性を保つためのもの。</p>
     *
     * @param my
     * @param other
     * @return trueの場合、1つでもセルの範囲が重複している。
     */
    public static boolean intersectsRegion(final CellRangeAddressBase my, final CellRangeAddressBase other) {
        return my.getFirstRow() <= other.getLastRow() &&
                my.getFirstColumn() <= other.getLastColumn() &&
                other.getFirstRow() <= my.getLastRow() &&
                other.getFirstColumn() <= my.getLastColumn();
    }

    /**
     * 日時の開始日が1904年かどうか。
     * 通常は、1900年始まり。
     * @param workbook ワークブック
     * @return trueの場合は、1904年始まり。falseの場合は、1900年始まり。
     */
    public static boolean isDateStart1904(final Workbook workbook) {

        if(workbook instanceof HSSFWorkbook) {
            try {
                Method method = HSSFWorkbook.class.getDeclaredMethod("getWorkbook");
                method.setAccessible(true);

                InternalWorkbook iw = (InternalWorkbook) method.invoke(workbook);
                return iw.isUsing1904DateWindowing();

            } catch(NoSuchMethodException | SecurityException e) {
                logger.warn("fail access method HSSFWorkbook.getWorkbook.", e);
                return false;
            } catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                logger.warn("fail invoke method HSSFWorkbook.getWorkbook.", e);
                return false;
            }

        } else if(workbook instanceof XSSFWorkbook) {
            try {
                Method method = XSSFWorkbook.class.getDeclaredMethod("isDate1904");
                method.setAccessible(true);

                boolean value = (boolean) method.invoke(workbook);
                return value;

            } catch(NoSuchMethodException | SecurityException e) {
                logger.warn("fail access method XSSFWorkbook.isDate1904.", e);
                return false;
            } catch(IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                logger.warn("fail invoke method XSSFWorkbook.isDate1904.", e);
                return false;
            }

        } else {
            logger.warn("unknown workbook type.", workbook.getClass().getName());
        }

        return false;
    }

    /**
     * 結合を考慮してセルの罫線(上部)を取得する。
     *
     * @param cell セル
     * @return {@literal BorderStyle}
     * @throws IllegalArgumentException {@literal cell is null.}
     */
    public static BorderStyle getBorderTop(final Cell cell) {

        ArgUtils.notNull(cell, "cell");

        final Sheet sheet = cell.getSheet();
        CellRangeAddress mergedRegion = getMergedRegion(sheet, cell.getRowIndex(), cell.getColumnIndex());

        final Cell target;
        if(mergedRegion == null) {
            // 結合されていない場合
            target = cell;

        } else {
            if(mergedRegion.getFirstRow() == cell.getRowIndex()) {
                // 引数のCellが上部のセルの場合
                target = cell;
            } else {
                target = getCell(sheet, cell.getColumnIndex(), mergedRegion.getFirstRow());
            }

        }

        final CellStyle style = target.getCellStyle();
        if(style == null) {
            return BorderStyle.NONE;
        } else {
            return style.getBorderTop();
        }

    }

    /**
     * 結合を考慮してセルの罫線(下部)を取得する。
     *
     * @param cell セル
     * @return {@literal BorderStyle}
     * @throws IllegalArgumentException {@literal cell is null.}
     */
    public static BorderStyle getBorderBottom(final Cell cell) {

        ArgUtils.notNull(cell, "cell");

        final Sheet sheet = cell.getSheet();
        CellRangeAddress mergedRegion = getMergedRegion(sheet, cell.getRowIndex(), cell.getColumnIndex());

        final Cell target;
        if(mergedRegion == null) {
            // 結合されていない場合
            target = cell;

        } else {
            if(mergedRegion.getLastRow() == cell.getRowIndex()) {
                // 引数のCellが下部のセルの場合
                target = cell;
            } else {
                target = getCell(sheet, cell.getColumnIndex(), mergedRegion.getLastRow());
            }

        }

        final CellStyle style = target.getCellStyle();
        if(style == null) {
            return BorderStyle.NONE;
        } else {
            return style.getBorderBottom();
        }

    }

    /**
     * 結合を考慮してセルの罫線(左部)を取得する。
     *
     * @param cell セル
     * @return {@literal BorderStyle}
     * @throws IllegalArgumentException {@literal cell is null.}
     */
    public static BorderStyle getBorderRight(final Cell cell) {

        ArgUtils.notNull(cell, "cell");

        final Sheet sheet = cell.getSheet();
        CellRangeAddress mergedRegion = getMergedRegion(sheet, cell.getRowIndex(), cell.getColumnIndex());

        final Cell target;
        if(mergedRegion == null) {
            // 結合されていない場合
            target = cell;

        } else {
            if(mergedRegion.getLastColumn() == cell.getColumnIndex()) {
                // 引数のCellが右部のセルの場合
                target = cell;
            } else {
                target = getCell(sheet, mergedRegion.getLastColumn(), cell.getRowIndex());
            }

        }

        final CellStyle style = target.getCellStyle();
        if(style == null) {
            return BorderStyle.NONE;
        } else {
            return style.getBorderRight();
        }

    }

    /**
     * 結合を考慮してセルの罫線(右部)を取得する。
     *
     * @param cell セル
     * @return {@literal BorderStyle}
     * @throws IllegalArgumentException {@literal cell is null.}
     */
    public static BorderStyle getBorderLeft(final Cell cell) {

        ArgUtils.notNull(cell, "cell");

        final Sheet sheet = cell.getSheet();
        CellRangeAddress mergedRegion = getMergedRegion(sheet, cell.getRowIndex(), cell.getColumnIndex());

        final Cell target;
        if(mergedRegion == null) {
            // 結合されていない場合
            target = cell;

        } else {
            if(mergedRegion.getFirstColumn() == cell.getColumnIndex()) {
                // 引数のCellが左部のセルの場合
                target = cell;
            } else {
                target = getCell(sheet, mergedRegion.getFirstColumn(), cell.getRowIndex());
            }

        }

        final CellStyle style = target.getCellStyle();
        if(style == null) {
            return BorderStyle.NONE;
        } else {
            return style.getBorderLeft();
        }

    }

    /**
     * ハイパーリンクを取得する。
     * <p>結合されているセルの場合にも対応。
     * @param cell
     * @return 見つからない場合は、nullを返す。
     * @throws IllegalArgumentException {@literal cell is null.}
     */
    public static Hyperlink getHyperlink(final Cell cell) {

        ArgUtils.notNull(cell, "cell");

        Hyperlink link = cell.getHyperlink();
        if(link != null) {
            return link;
        }

        final Sheet sheet = cell.getSheet();
        CellRangeAddress mergedRange = getMergedRegion(sheet, cell.getRowIndex(), cell.getColumnIndex());
        if(mergedRange == null) {
            return null;
        }

        for(Hyperlink item : sheet.getHyperlinkList()) {
            if(item.getFirstRow() == mergedRange.getFirstRow()
                    && item.getFirstColumn() == mergedRange.getFirstColumn()) {
                return item;
            }

        }

        return null;

    }

    /**
     * {@literal 1900-01-01 00:00:00.000}の時間(単位はミリ秒)。
     * <p>Excelは設定により、1900年始まりか1904年始まりか指定できるため、その基準値として利用する。
     */
    public static final long MILLISECONDS_19000101_END = ExcelDateUtils.parseDate("1900-01-01 23:59:54.999").getTime();

    /**
     * セルに日時を設定する。
     * <p>1900年1月0日となる経過時間指定の場合は、POIのバグにより設定できあいため、数値として設定する。</p>
     *
     * @param cell 設定するセル
     * @param date セルに設定する日時
     * @param dateStart1904 1904年始まりの設定のシートかどうか
     */
    public static void setCellValueAsDate(Cell cell, Date date, boolean dateStart1904) {

        ArgUtils.notNull(cell, "cell");
        ArgUtils.notNull(date, "date");

        if(dateStart1904) {
            // 1904年始まりの場合は、そのまま設定する
            cell.setCellValue(date);

        } else {

            long timemills = date.getTime();
            if(timemills <= MILLISECONDS_19000101_END) {
                // 1900年1月0日の場合は、数値に変換してから設定する
                // タイムゾーンを除去する
                Date strip = new Date(date.getTime() + TimeZone.getDefault().getRawOffset());
                double num = ExcelDateUtils.convertExcelNumber(strip, dateStart1904);
                cell.setCellValue(num);

            } else {
                cell.setCellValue(date);
            }

        }

    }

}