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}