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.plugin.table;
017
018import java.util.Calendar;                                                                      // 7.0.1.3 (2018/11/12)
019
020import org.opengion.hayabusa.db.AbstractTableFilter;
021import org.opengion.hayabusa.db.DBTableModel;
022
023import org.opengion.fukurou.util.ErrorMessage;
024import org.opengion.fukurou.util.StringUtil;
025import org.opengion.fukurou.util.HybsDateUtil;                          // 7.0.1.3 (2018/11/12)
026import org.opengion.fukurou.system.DateSet;                                     // 7.0.1.3 (2018/11/12)
027
028/**
029 * TableFilter_KEY_BREAK は、TableFilter インターフェースを継承した、DBTableModel 処理用の
030 * 実装クラスです。
031 *
032 * ここでは、指定のカラムに対して、キーブレイクが発生したときのデータ処理方法を指定できます。
033 * 主として、グルーピング処理を行うのですが、ソートされデータの並び順で、キーブレイクするため、
034 * 同一キーが存在していても、並び順が離れている場合、別のキーとしてブレイクします。
035 *
036 * GROUP_KEY  : キーブレイクの判定を行うカラムを、CSV形式で設定します。
037 * OUT_TYPE   : 出力するデータのタイプを指定します。
038 *                first : 最初のデータ(ブレイク直後のデータ)を出力します。(初期値)
039 *                last  : 最後のデータ(ブレイク直前のデータ)を出力します。
040 *                range : 最初のデータと最後のデータを出力します。
041 *
042 * firstは、キーブレイク時のデータを残します。つまり、キーの最初に現れたデータです。
043 * lastは、キーブレイクの直前のデータを残します。これは、同一キーの最後のデータということになります。
044 * rangeは、firstと、last つまり、同値キーの最初と最後のデータを残します。
045 *
046 * もし、キーが、1行だけの場合、firstも、lastも、同じ行を指すことになります。
047 * その場合、rangeは、その1行だけになります(2行出力されません)。
048 *
049 * 例:機種と日付と、状況Fがあったとして、日付、機種、状況F でソートし、機種をグループキー、
050 * 状況Fをブレイクキーとすれば、日付の順に、機種の中で、状況Fがブレークしたときのみ、
051 * データを残す、ということが可能になります。7.0.0.1 (2018/10/09) Delete
052 *
053 * OUT_TYPE に、lastか、range を指定した場合のみ、最大、最小、平均、中間、個数の集計処理が行えます。
054 * これらの設定は、指定のカラムのデータに反映されます。
055 * MIN_CLM  : キーブレイク時に、指定のカラムの最小値をデータに書き込みます。
056 * MAX_CLM  : キーブレイク時に、指定のカラムの最大値をデータに書き込みます。
057 * AVG_CLM  : キーブレイク時に、指定のカラムの平均値をデータに書き込みます。
058 * MID_CLM  : キーブレイク時に、指定のカラムの最小値と最大値の中間の値をデータに書き込みます。
059 * DIF_CLM  : キーブレイク時に、指定のカラムの最大値から最小値を引いた値(差)をデータに書き込みます。8.0.1.2 (2021/11/19)
060 * CNT_CLM  : キーブレイク時に、指定のカラムのデータ件数をデータに書き込みます。
061 *
062 * これらのカラムの値は、数値で表現できるもので無ければなりません。
063 * 例えば、20180101000000 のような、日付でも数字のみなら、OKです。
064 *
065 * 8.0.1.2 (2021/11/19) DIF_CLM 差分計算
066 *  8桁か14桁で、先頭"20"の場合は、日付型と判定します。
067 *  その場合、8桁は、経過日数を返し、14桁は、MM/dd HH:mm 形式で返します。
068 *
069 * パラメータは、tableFilterタグの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
070 *
071 * @og.formSample
072 * ●形式:
073 *              ① <og:tableFilter classId="KEY_BREAK"
074 *                              keys="GROUP_KEY,OUT_TYPE"
075 *                              vals='"CLM5,CLM6....",first' />
076 *
077 *              ② <og:tableFilter classId="KEY_BREAK" >
078 *                               {
079 *                                       GROUP_KEY  : CLM5,CLM6....     ;
080 *                                       OUT_TYPE : first                       ;
081 *                               }
082 *                 </og:tableFilter>
083 *
084 * @og.rev 6.7.9.1 (2017/05/19) 新規追加
085 * @og.rev 7.0.0.1 (2018/10/09) グループで、まとめる処理を止めます。
086 * @og.rev 7.0.1.1 (2018/10/22) ロジック見直し
087 *
088 * @version  6.7  2017/05/19
089 * @author   Kazuhiko Hasegawa
090 * @since    JDK1.8,
091 */
092public class TableFilter_KEY_BREAK extends AbstractTableFilter {
093        /** このプログラムのVERSION文字列を設定します。 {@value} */
094        private static final String VERSION = "8.1.2.1 (2022/03/25)" ;
095
096        /** 8.0.1.2 (2021/11/19) 日単位変換係数 */
097        private static final int MILLIS_OF_DAY = 1000 * 60 * 60 * 24;
098
099        /**
100         * デフォルトコンストラクター
101         *
102         * @og.rev 6.7.9.1 (2017/05/19) 新規追加
103         * @og.rev 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加
104         * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のキーワード追加
105         * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のキーワード追加
106         */
107        public TableFilter_KEY_BREAK() {
108                super();
109                initSet( "GROUP_KEY"    , "キーブレイクの判定を行うカラムを、CSV形式で設定します。" );
110                initSet( "OUT_TYPE"     , "出力するデータのタイプを指定[first/last/range]を指定します。(初期値:first 最初のデータ)"        );
111                initSet( "MIN_CLM"      , "キーブレイク時に、指定のカラムの最小値をデータに書き込みます。"   );
112                initSet( "MAX_CLM"      , "キーブレイク時に、指定のカラムの最大値をデータに書き込みます。"   );
113                initSet( "AVG_CLM"      , "キーブレイク時に、指定のカラムの平均値をデータに書き込みます。"   );
114                initSet( "MID_CLM"      , "キーブレイク時に、指定のカラムの最小値と最大値の中間の値をデータに書き込みます。"  );
115                initSet( "DIF_CLM"      , "キーブレイク時に、指定のカラムの最大値から最小値を引いた値(差)をデータに書き込みます。"      );      // 8.0.1.2 (2021/11/19)
116                initSet( "CNT_CLM"      , "キーブレイク時に、指定のカラムのデータ件数をデータに書き込みます。"        );
117        }
118
119        /**
120         * DBTableModel処理を実行します。
121         *
122         * @og.rev 6.7.9.1 (2017/05/19) 新規追加
123         * @og.rev 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加
124         * @og.rev 7.0.1.1 (2018/10/22) ロジック見直し
125         * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のキーワード追加
126         * @og.rev 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ゼロ文字列をセットします。
127         * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のキーワード追加
128         * @og.rev 8.1.2.1 (2022/03/25) OUT_TYPE="first" 時に最小,最大,平均,件数の集計ができるように機能追加
129         *
130         * @return 処理結果のDBTableModel
131         */
132        public DBTableModel execute() {
133                final DBTableModel table  = getDBTableModel();
134                final DBTableModel rtnTbl = table.newModel();                           // 削除ではなく、追加していきます。
135                final int rowCnt = table.getRowCount();
136                if( rowCnt == 0 ) { return rtnTbl; }                                            // 7.0.1.3 (2018/11/12) row<=rowCnt を追加したので、0件なら即終了
137
138                final String[]  brkClms = StringUtil.csv2Array( getValue( "GROUP_KEY" ) );
139
140                // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加
141                final String    outType = StringUtil.nval( getValue( "OUT_TYPE"  ), "first" ) ;
142
143                final boolean   useFirst = "first".equalsIgnoreCase( outType ) || "range".equalsIgnoreCase( outType );  // firstかrange時に使用
144                final boolean   useLast  = "last".equalsIgnoreCase( outType )  || "range".equalsIgnoreCase( outType ) ; // lastかrange 時に使用
145
146                // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加(useLast=true のときのみ使用)
147                final int       minClmNo = table.getColumnNo( getValue( "MIN_CLM"  ), false ) ; // カラムが存在しなければ、-1
148                final int       maxClmNo = table.getColumnNo( getValue( "MAX_CLM"  ), false ) ; // カラムが存在しなければ、-1
149                final int       avgClmNo = table.getColumnNo( getValue( "AVG_CLM"  ), false ) ; // カラムが存在しなければ、-1
150                final int       midClmNo = table.getColumnNo( getValue( "MID_CLM"  ), false ) ; // 7.0.1.3 (2018/11/12) カラムが存在しなければ、-1
151                final int       difClmNo = table.getColumnNo( getValue( "DIF_CLM"  ), false ) ; // 8.0.1.2 (2021/11/19) カラムが存在しなければ、-1
152                final int       cntClmNo = table.getColumnNo( getValue( "CNT_CLM"  ), false ) ; // カラムが存在しなければ、-1
153
154                final int[]    brkClmNo = new int[brkClms.length];                      // ブレイクキーカラムの番号
155
156                for( int i=0; i<brkClms.length; i++ ) {
157                        brkClmNo[i] = table.getColumnNo( brkClms[i],false );    // カラムが存在しなければ、-1
158                }
159
160                // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加(useLast=true のときのみ使用)
161                double  minData  = Double.POSITIVE_INFINITY ;           // 仮数部の桁数の限界は15桁なので、日付型(14桁)は、処理できる。
162                double  maxData  = Double.NEGATIVE_INFINITY ;
163                double  total    = 0.0 ;
164                int             cntData  = 0 ;          //
165                boolean isLong   = true;        // データに、少数点以下をつけるかどうかの判定です。
166                double  midMin   = Double.POSITIVE_INFINITY ;
167                double  midMax   = Double.NEGATIVE_INFINITY ;
168                double  difMin   = Double.POSITIVE_INFINITY ;           // 8.0.1.2 (2021/11/19)
169                double  difMax   = Double.NEGATIVE_INFINITY ;           // 8.0.1.2 (2021/11/19)
170
171                String oldBlkKeys = null;       // 前回ブレイクキーの値
172
173                String[] oldData = null;
174                // 7.0.1.3 (2018/11/12) 最後のデータの処理を行うために、row<=rowCnt と1回余計に回します。
175                for( int row=0; row<=rowCnt; row++ ) {
176                        final String[] data = row == rowCnt ? null : table.getValues( row );            // row<=rowCnt の影響
177                        try {
178                                final String brkKeys = getKeys( brkClmNo , data );              // ブレークキー(data==nullの場合、ゼロ文字列)
179                                if( !brkKeys.equalsIgnoreCase( oldBlkKeys ) ) {                 // キーブレイク
180                                        // 初回必ずブレイクするので、row==0 時は処理しない。
181                                        if( row>0 ) {
182                                                // 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ゼロ文字列をセットします。
183                                                if( minClmNo >= 0 ) {
184                                                        if( minData == Double.POSITIVE_INFINITY ) {
185                                                                oldData[minClmNo] = "";                 // 7.2.4.0 (2020/05/11)
186                                                        }
187                                                        else {
188                                                                oldData[minClmNo] = isLong ? String.valueOf( Math.round( minData ) ) : String.valueOf( minData ) ;
189                                                        }
190                                                }
191                                                if( maxClmNo >= 0 ) {
192                                                        if( maxData == Double.NEGATIVE_INFINITY ) {
193                                                                oldData[maxClmNo] = "";                 // 7.2.4.0 (2020/05/11)
194                                                        }
195                                                        else {
196                                                                oldData[maxClmNo] = isLong ? String.valueOf( Math.round( maxData ) ) : String.valueOf( maxData ) ;
197                                                        }
198                                                }
199                                                if( avgClmNo >= 0 ) { oldData[avgClmNo] = String.format( "%.3f", total/cntData ); }
200                                                if( midClmNo >= 0 ) { oldData[midClmNo] = getMiddle( midMin,midMax ); }
201                                                if( difClmNo >= 0 ) { oldData[difClmNo] = getDifference( difMin,difMax ); }     // 8.0.1.2 (2021/11/19)
202                                                if( cntClmNo >= 0 ) { oldData[cntClmNo] = String.valueOf( cntData ); }
203
204                                                minData = Double.POSITIVE_INFINITY  ;
205                                                maxData = Double.NEGATIVE_INFINITY  ;
206                                                total   = 0.0 ;
207                                                midMin  = Double.POSITIVE_INFINITY  ;
208                                                midMax  = Double.NEGATIVE_INFINITY  ;
209                                                difMin  = Double.POSITIVE_INFINITY  ;           // 8.0.1.2 (2021/11/19)
210                                                difMax  = Double.NEGATIVE_INFINITY  ;           // 8.0.1.2 (2021/11/19)
211
212                                                // 8.1.2.1 (2022/03/25) OUT_TYPE="first" 時に最小,最大,平均,件数の集計ができるように機能追加
213                        //                      if( useLast ) {
214                                                        // useFirst=true で、cntData == 1 の場合は、First行は削除します(1件を2件に増やさない)。
215                                                        if( useFirst ) {
216                                                                final int rCnt = rtnTbl.getRowCount();
217                                                                if( cntData == 1 ) {                                            // 1行しかない場合は、First行は削除します(1件を2件に増やさない)
218                                                                        rtnTbl.removeValue( rCnt-1 );
219                                                                }
220                                                                else {
221                                                                        // useLast && useFirst ⇒ range 指定の処理。 前のデータ=First行に、最大、最小等のデータを反映させます。
222                                                                        final String[] fstData = rtnTbl.getValues( rCnt-1 );
223                                                                        if( minClmNo >= 0 ) { fstData[minClmNo] = oldData[minClmNo]; }
224                                                                        if( maxClmNo >= 0 ) { fstData[maxClmNo] = oldData[maxClmNo]; }
225                                                                        if( avgClmNo >= 0 ) { fstData[avgClmNo] = oldData[avgClmNo]; }
226                                                                        if( midClmNo >= 0 ) { fstData[midClmNo] = oldData[midClmNo]; }
227                                                                        if( difClmNo >= 0 ) { fstData[difClmNo] = oldData[difClmNo]; }          // 8.0.1.2 (2021/11/19)
228                                                                        if( cntClmNo >= 0 ) { fstData[cntClmNo] = oldData[cntClmNo]; }
229                                                                }
230                                                        }
231                        //                              rtnTbl.addColumnValues( oldData );                      // ブレイクした一つ前=最後のデータ
232                        //                      }
233
234                                                if( useLast ) {
235                                                        rtnTbl.addColumnValues( oldData );                      // ブレイクした一つ前=最後のデータ
236                                                }
237
238                                                if( row == rowCnt ) { break; }                                  // 最後のデータの処理を行うために、row<=rowCnt と1回余計に回します。
239                                        }
240
241                                        if( useFirst ) {                                                                        // useLast=true で、cntData == 1 の場合は、登録しません
242                                                rtnTbl.addColumnValues( data );                                 // ブレイク時のデータを登録します。
243                                        }
244
245                                        oldBlkKeys = brkKeys;
246                                        cntData    = 0 ;
247                                }
248                                oldData = data;                                                                                 // 一つ前のデータ
249                                cntData++;                              // 毎回、カラムのある無しを判定するより、早そうなので常にカウントしておきます。
250
251                                // ブレイク時も集計処理は行います。
252                                if( minClmNo >= 0 && !StringUtil.isNull( data[minClmNo] ) ) {
253                                        if( isLong && data[minClmNo].indexOf( '.' ) >= 0 ) { isLong = false; }                  // 一度、false になると、戻らない。
254                                        minData = Math.min( minData, Double.parseDouble( data[minClmNo] ) );
255                                }
256                                if( maxClmNo >= 0 && !StringUtil.isNull( data[maxClmNo] ) ) {
257                                        if( isLong && data[maxClmNo].indexOf( '.' ) >= 0 ) { isLong = false; }                  // 一度、false になると、戻らない。
258                                        maxData = Math.max( maxData, Double.parseDouble( data[maxClmNo] ) );
259                                }
260                                if( avgClmNo >= 0 && !StringUtil.isNull( data[avgClmNo] ) ) {
261                                        total += Double.parseDouble( data[avgClmNo] );
262                                }
263                                if( midClmNo >= 0 && !StringUtil.isNull( data[midClmNo] ) ) {
264                                        final double mid = Double.parseDouble( data[midClmNo] );
265                                        midMin = Math.min( midMin, mid );
266                                        midMax = Math.max( midMax, mid );
267                                }
268                                if( difClmNo >= 0 && !StringUtil.isNull( data[difClmNo] ) ) {           // 8.0.1.2 (2021/11/19)
269                                        final double dif = Double.parseDouble( data[difClmNo] );
270                                        difMin = Math.min( difMin, dif );
271                                        difMax = Math.max( difMax, dif );
272                                }
273                        }
274                        catch( final RuntimeException ex ) {    // そのまま、継続して処理を行う。
275                                // 6.5.0.1 (2016/10/21) ErrorMessage をまとめるのと、直接 Throwable を渡します。
276                                makeErrorMessage( "TableFilter_KEY_BREAK Error",ErrorMessage.NG )
277                                        .addMessage( row+1,ErrorMessage.NG,"KEY_BREAK" , StringUtil.array2csv( data ) )
278                                        .addMessage( ex );
279                        }
280                }
281
282                return rtnTbl;
283        }
284
285        /**
286         * 最小値と最大値の中間の値の文字列を作成します。
287         *
288         * 特殊系で、8桁か、14桁の場合、日付文字として中間の日付を求めます。
289         *
290         * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のキーワード追加
291         *
292         * @param min 最小値
293         * @param max 最大値
294         * @return 中間の値の文字列
295         */
296        private String getMiddle( final double min , final double max ) {
297                final String minStr = String.valueOf( Math.round( min ) );              // 14桁の場合、2.0181103000000E13 見たいな表記になるため。
298                final String maxStr = String.valueOf( Math.round( max ) );
299                final int minLen = minStr.length();
300
301                final String midStr ;
302                // 2000 年問題!? 先頭が "20" の場合は、日付型と判定する。
303                if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 )
304                                && minStr.startsWith("20") && maxStr.startsWith("20") ) {
305                        final Calendar minCal = HybsDateUtil.getCalendar( minStr );
306                        final Calendar maxCal = HybsDateUtil.getCalendar( maxStr );
307                        final long     midTim = ( maxCal.getTimeInMillis() + minCal.getTimeInMillis() ) / 2 ;
308
309                        if( minLen == 8 ) {
310                                midStr = DateSet.getDate( midTim , "yyyyMMdd" );
311                        }
312                        else {  // 14桁しかありえない
313                                midStr = DateSet.getDate( midTim , "yyyyMMddHHmmss" );
314                        }
315                }
316                else {
317                        midStr = String.format( "%.3f", ( max + min ) / 2.0 );          // 日付型でなければ、minStr,maxStr は使わないので。
318                }
319
320                return midStr;
321        }
322
323        /**
324         * 最大値から最小値を引いた値(差)の文字列を作成します。
325         *
326         * 特殊系で、8桁か、14桁の場合、日付文字として経過日数を求めます。
327         *
328         * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のキーワード追加
329         *
330         * @param min 最小値
331         * @param max 最大値
332         * @return 最大値から最小値を引いた値(差)の文字列
333         */
334        private String getDifference( final double min , final double max ) {
335                final String minStr = String.valueOf( Math.round( min ) );              // 14桁の場合、2.0181103000000E13 見たいな表記になるため。
336                final String maxStr = String.valueOf( Math.round( max ) );
337                final int minLen = minStr.length();
338
339                final String midStr ;
340                // 2000 年問題!? 先頭が "20" の場合は、日付型と判定する。
341                if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 )
342                                && minStr.startsWith("20") && maxStr.startsWith("20") ) {
343                        final Calendar minCal = HybsDateUtil.getCalendar( minStr );
344                        final Calendar maxCal = HybsDateUtil.getCalendar( maxStr );
345
346                        final long difTim = maxCal.getTimeInMillis() - minCal.getTimeInMillis() ;
347
348                        if( minLen == 8 ) {
349                                midStr = String.format( "%d", (int)difTim/MILLIS_OF_DAY );
350                        }
351                        else {  // 14桁しかありえない
352                                midStr = DateSet.getDate( difTim , "MM/dd HH:mm" );
353                        }
354                }
355                else {
356                        midStr = String.format( "%.3f", max - min );    // 日付型でなければ、minStr,maxStr は使わないので。
357                }
358
359                return midStr;
360        }
361
362        /**
363         * キーの配列アドレスと、1行分のデータ配列から、キーとなる文字列を作成します。
364         *
365         * @og.rev 6.7.9.1 (2017/05/19) 新規追加
366         * @og.rev 7.0.1.3 (2018/11/12) 最後のデータの処理を行うために、row<=rowCnt と1回余計に回す対応
367         *
368         * @param clms キーの配列アドレス
369         * @param rowData 1行分のデータ配列
370         * @return キーとなる文字列
371         */
372        private String getKeys( final int[] clms , final String[] rowData ) {
373                if( rowData == null ) { return ""; }                                    // rowData がnull の場合は、キーブレイクとなる
374
375                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
376                // 7.2.9.4 (2020/11/20) PMD:This for loop can be replaced by a foreach loop
377                for( final int clm : clms ) {
378                        if( clm >= 0 ) {
379                                buf.append( rowData[clm] ).append( ':' );
380                        }
381                }
382
383                return buf.toString();
384        }
385}