    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.model;
    017
    018import java.util.concurrent.ConcurrentMap;                                              // 6.4.3.4 (2016/03/11)
    019import java.util.concurrent.ConcurrentHashMap;                                  // 6.4.3.1 (2016/02/12) refactoring
    020
    021import org.apache.poi.ss.usermodel.BuiltinFormats;                              // 6.2.0.0 (2015/02/27)
    022import org.apache.poi.xssf.model.StylesTable;                                   // 6.2.0.0 (2015/02/27)
    023import org.apache.poi.xssf.usermodel.XSSFCellStyle;                             // 6.2.0.0 (2015/02/27)
    024
    025import java.util.Locale;                                                                                // 6.2.0.0 (2015/02/27)
    026import java.util.Date;                                                                                  // 6.2.0.0 (2015/02/27)
    027import java.text.DateFormat;                                                                    // 6.2.0.0 (2015/02/27)
    028import java.text.SimpleDateFormat;                                                              // 6.2.0.0 (2015/02/27)
    029import org.apache.poi.ss.usermodel.DateUtil;                                    // 6.2.0.0 (2015/02/27)
    030import org.apache.poi.ss.util.NumberToTextConverter;                    // 6.2.0.0 (2015/02/27)
    031import org.apache.poi.hssf.record.ExtendedFormatRecord;                 // 6.2.0.0 (2015/02/27)
    032import org.apache.poi.hssf.record.FormatRecord;                                 // 6.2.0.0 (2015/02/27)
    033import org.apache.poi.hssf.record.NumberRecord;                                 // 6.2.0.0 (2015/02/27)
    034
    035/**
    036 * POI による、Excel(xlsx)の読み取りクラスです。
    037 *
    038 * xlsx形式のEXCELを、イベント方式でテキストデータを読み取ります。
    039 * このクラスでは、XSSF(.xlsx)形式のファイルを、TableModelHelper を介したイベントで読み取ります。
    040 * TableModelHelperイベントは、openGion形式のファイル読み取りに準拠した方法をサポートします。
    041 * ※ openGion形式のEXCELファイルとは、#NAME 列に、カラム名があり、#で始まる
    042 *    レコードは、コメントとして判断し、読み飛ばす処理の事です。
    043 *
    044 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
    045 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model),クラス名変更(ExcelReader_XSSF → EventReader_XLSX)
    046 * @og.group ファイル入力
    047 *
    048 * @version  6.0
    049 * @author   Kazuhiko Hasegawa
    050 * @since    JDK7.0,
    051 */
    052public final class ExcelStyleFormat {
    053        /** このプログラムのVERSION文字列を設定します。   {@value} */
    054        private static final String VERSION = "6.4.3.3 (2016/03/04)" ;
    055
    056        // 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    057        private static final String[] DATE_TYPE = { "yyyyMMdd" , "yyyyMMddHHmmss" , "HHmmss" };
    058        // 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
    059        private static final DateFormat[] DT_FORMAT = new DateFormat[DATE_TYPE.length];                 // 6.4.1.1 (2016/01/16) dtFormat → DT_FORMAT refactoring
    060        private final StylesTable                       stylesTable ;
    061
    062        // 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    063        // private final List<Integer>                  extFmtIdx = new ArrayList<>();  // ExtendedFormatRecord のｱﾄﾞﾚｽ(順番) の順番に、FormatIndexを設定する。
    064        /** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
    065        private final ConcurrentMap<Integer,String> fmtStrMap = new ConcurrentHashMap<>();      // FormatIndex をキーに、Format文字列 を管理
    066        /** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
    067        private final ConcurrentMap<Integer,Integer> extFmtIdxMap = new ConcurrentHashMap<>();  // ExtendedFormatRecord のｱﾄﾞﾚｽ(順番) をキーに、FormatIndexを設定する。
    068        private int   extFmtCnt ;
    069
    070        /**
    071         * XSL系 コンストラクター
    072         *
    073         * XSL 処理では、HSSFListener のイベント処理のうち、NumberRecord の値取得に
    074         * 必要な内部処理を、実行します。
    075         * 具体的には、ExtendedFormatRecord レコードから、FormatIndex と 並び順を
    076         * 管理するMapと、FormatRecord レコードから、IndexCode と フォーマット文字列を
    077         * 管理するMap を作成し、NumberRecordレコードの XFIndex から、ExtendedFormatRecord を
    078         * 経由して、FormatRecord のフォーマット文字列 を取得し、日付フォーマットの場合は、
    079         * 日付文字列に、それ以外は、数字文字列に変換する手助けを行います。
    080         *
    081         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    082         */
    083        public ExcelStyleFormat() {
    084                stylesTable = null;
    085        }
    086
    087        /**
    088         * XSLX系 コンストラクター
    089         *
    090         * StylesTable は、日付型をはじめとする、EXCELのスタイルのフォーマットを管理しています。
    091         * XSLX形式のEXCELをパースする場合に、このコンストラクタを使用して、StylesTableオブジェクトを
    092         * 設定します。
    093         *
    094         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    095         *
    096         * @param       styles StylesTableオブジェクト
    097         */
    098        public ExcelStyleFormat( final StylesTable styles ) {
    099                stylesTable = styles;
    100        }
    101
    102        /**
    103         * XSL系 ExtendedFormatRecordレコードの設定。
    104         *
    105         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    106         *
    107         * @param       extFmtRec ExtendedFormatRecordレコード
    108         */
    109        public void addExtFmtRec( final ExtendedFormatRecord extFmtRec ) {
    110                final short fmtIdx = extFmtRec.getFormatIndex();
    111                final short xfType = extFmtRec.getXFType();
    112                // Listに ｱﾄﾞﾚｽ(順番) の順番に、FormatIndexを設定する。
    113        //      extFmtIdx.add( Integer.valueOf( fmtIdx ) );
    114
    115                // タイプを判別して、ｱﾄﾞﾚｽ(順番)をキーに、Mapに登録することで、データ件数を削減します。
    116                if( xfType == ExtendedFormatRecord.XF_CELL ) {
    117                        //                              ｱﾄﾞﾚｽ(順番)               FormatIndex
    118                        extFmtIdxMap.put( Integer.valueOf( extFmtCnt ),Integer.valueOf( fmtIdx ) );
    119        //              System.out.println( "fmtIdx=[" + fmtIdx + "] , xfType=[" + xfType + "] , CNT=" + extFmtCnt );
    120                }
    121                extFmtCnt++;
    122        }
    123
    124        /**
    125         * XSL系 FormatRecordレコードの設定。
    126         *
    127         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    128         *
    129         * @param       fmtRec FormatRecordレコード
    130         */
    131        public void addFmtRec( final FormatRecord fmtRec ) {
    132                final int    idxc = fmtRec.getIndexCode();
    133                final String fmt  = fmtRec.getFormatString();
    134
    135                // IndexCode をキーに、Format文字列を登録する。
    136                fmtStrMap.put( Integer.valueOf( idxc ) , fmt );
    137        //      System.out.println( "fmtRec=[" + idxc + "], fmt=[" + fmt + "]" );
    138        }
    139
    140        /**
    141         * XSLX系 セルスタイル文字列(スタイル番号)から、データフォーマットを取得します。
    142         *
    143         * StylesTable は、日付型をはじめとする、EXCELのスタイルのフォーマットを管理しています。
    144         * XSLX形式のEXCELのフォーマット文字列を取得する場合に、使用します。
    145         *
    146         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    147         *
    148         * @param       cellStyleStr    セルスタイル文字列(スタイル番号)
    149         * @param       val                             endElement時の値文字列
    150         * @return      日付データか、数値データかを判別した結果の文字列
    151         */
    152        public String getNumberValue( final String cellStyleStr , final String val ) {
    153                String fmtStr = null;
    154
    155                if( stylesTable != null && cellStyleStr != null && !cellStyleStr.isEmpty() ) {
    156                        final int stIdx = Integer.parseInt( cellStyleStr );
    157                        final XSSFCellStyle style = stylesTable.getStyleAt( stIdx );
    158                        fmtStr = style.getDataFormatString();
    159
    160                        // 必要かどうか不明。テスト時は、ユーザー定義フォーマットも、上記処理で取得できていた。
    161                        if( fmtStr == null ) {
    162                                final int fmtIdx = style.getDataFormat();
    163                                fmtStr = BuiltinFormats.getBuiltinFormat( fmtIdx );
    164                        }
    165
    166        //              if( fmtStr != null ) {
    167        //                      System.out.println( "style=[" + cellStyleStr + "], stIdx=[" + stIdx + "],  fmtStr=[" + fmtStr + "]" );
    168        //              }
    169                }
    170
    171                return getNumberValue( fmtStr , Double.parseDouble( val ) ) ;
    172        }
    173
    174        /**
    175         * XSL系 Numberレコードから、日付データか、数値データかを判別して返します。
    176         *
    177         * 日付フォーマットの判定処理を #isDateFormat(String) で行い、日付の場合は、
    178         * 各タイプ(日付、日時、時刻)に応じた、文字列を返します。
    179         * 日付フォーマットでない場合は、数字化文字列を返します。
    180         *
    181         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    182         *
    183         * @param       numrec  NumberRecordレコード
    184         * @return      日付データか、数値データかを判別した結果の文字列
    185         */
    186        public String getNumberValue( final NumberRecord numrec ) {
    187                final int xfIdx  = numrec.getXFIndex();                                                         // extFmtCnt の事
    188                final int fmtIdx = extFmtIdxMap.get( Integer.valueOf( xfIdx ) );
    189
    190        //      final String fmtStr = fmtIdx < HSSFDataFormat.getNumberOfBuiltinBuiltinFormats()
    191        //                                                      ? HSSFDataFormat.getBuiltinFormat( (short)fmtIdx )
    192        //                                                      : fmtStrMap.get( Integer.valueOf( fmtIdx ) );
    193
    194                final String fmtStr = fmtIdx < BuiltinFormats.FIRST_USER_DEFINED_FORMAT_INDEX
    195                                                                ? BuiltinFormats.getBuiltinFormat( fmtIdx )
    196                                                                : fmtStrMap.get( Integer.valueOf( fmtIdx ) );
    197
    198        //              if( fmtStr != null ) {
    199        //                      System.out.println( "xfIdx=[" + xfIdx + "], fmtIdx=[" + fmtIdx + "],  fmtStr=[" + fmtStr + "]" );
    200        //              }
    201
    202                return getNumberValue( fmtStr , numrec.getValue() ) ;
    203        }
    204
    205        /**
    206         * フォーマット情報と値から、日付データか、数値データかを判別して返します。
    207         *
    208         * 日付フォーマットの判定処理を #isDateFormat(String) で行い、日付の場合は、
    209         * 各タイプ(日付、日時、時刻)に応じた、文字列を返します。
    210         * 日付フォーマットでない場合は、数字文字列を返します。
    211         *
    212         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    213         * @og.rev 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
    214         *
    215         * @param       fmtStr  フォーマット情報
    216         * @param       val             Numberレコードのデータ
    217         * @return      日付データか、数値データかを判別した結果の文字列
    218         */
    219        public static String getNumberValue( final String fmtStr , final double val ) {
    220                return isDateFormat( fmtStr )
    221                                        ? dateFormat( val )                                                     // 日付
    222                                        : NumberToTextConverter.toText( val ) ;         // 数字
    223        }
    224
    225        /**
    226         * フォーマット文字列から、日付型フォーマットかどうかの判定を行います。
    227         *
    228         * ここでは、日本式のフォーマットや、ユーザー定義の日付フォーマットでも、
    229         * ある程度判定できるように、処理しています。
    230         * 以下の文字列を含む場合は、true を返し、それ以外は、false です。
    231         *   "年","月","日","yy","y/m","m/d","h:m"
    232         * ただし、General(標準) は、除外しています。
    233         *
    234         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    235         * @og.rev 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
    236         * @og.rev 6.4.3.3 (2016/03/04) PMD refactoring. These nested if statements could be combined
    237         *
    238         * @param       fmt     フォーマット文字列
    239         * @return      判定結果 [true:日付型/false:それ以外]
    240         */
    241        private static boolean isDateFormat( final String fmt ) {
    242                return  fmt != null && !fmt.isEmpty() && !"General".equalsIgnoreCase( fmt ) &&
    243                        (       fmt.contains( "年" ) || fmt.contains( "月"  ) || fmt.contains( "日"  ) ||
    244                                fmt.contains( "yy" ) || fmt.contains( "y/m" ) || fmt.contains( "m/d" ) ||
    245                                fmt.contains( "h:m" ) ) ;
    246        }
    247
    248        /**
    249         * 日付型の値を、最適なフォーマットで変換して返します。
    250         *
    251         * 日付データは、(DATE=0,DATETIME=1,TIME=2) に分類できます。
    252         * DATE とは、日付のみの状態で、引数の val は、整数に変換できます。
    253         * その場合、"yyyyMMdd" フォーマットで変換します。
    254         * DATETIME とは、日付＋時刻なので、"yyyyMMddHHmmss" に変換します。
    255         * TIME は、日付情報を持っていないため、"HHmmss" に変換します。
    256         *
    257         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    258         * @og.rev 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
    259         *
    260         * @param       val     日付型の値
    261         * @return      日付型の変換結果
    262         */
    263        public static String dateFormat( final double val ) {
    264                int dtType = 0;         // 日付型の処理(DATE=0,DATETIME=1,TIME=2)
    265                if( val < 1.0d ) {                                                                                              // 日付部が無い → TIME=2
    266                        dtType = 2;
    267                }
    268                // 6.3.9.0 (2015/11/06) Avoid if (x != y) ..; else ..; (PMD)
    269                else if( Double.compare( val , Math.floor( val ) ) == 0 ) {             // 整数(Long 相当) → DATE=0
    270                        dtType = 0;
    271                }
    272                else {                                                                                                                  // 整数でない → DATETIME=1
    273                        dtType = 1;
    274                }
    275
    276                DateFormat dtfmt = DT_FORMAT[dtType];                   // 各タイプ別にキャッシュしている。
    277                if( dtfmt == null ) {
    278                        dtfmt = new SimpleDateFormat( DATE_TYPE[dtType] , Locale.JAPAN );               // 初めての場合は新規作成
    279                        DT_FORMAT[dtType] = dtfmt;
    280                }
    281
    282                final Date dt = DateUtil.getJavaDate( val );
    283                return dtfmt.format( dt );
    284        }
    285}