    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 * CNT_CLM  : キーブレイク時に、指定のカラムのデータ件数をデータに書き込みます。
    060 *
    061 * これらのカラムの値は、数値で表現できるもので無ければなりません。
    062 * 例えば、20180101000000 のような、日付でも数字のみなら、OKです。
    063 *
    064 * パラメータは、tableFilterタグの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
    065 *
    066 * @og.formSample
    067 * ●形式：
    068 *              ① &lt;og:tableFilter classId="KEY_BREAK"
    069 *                              keys="GROUP_KEY,OUT_TYPE"
    070 *                              vals='"CLM5,CLM6....",first' /&gt;
    071 *
    072 *              ② &lt;og:tableFilter classId="KEY_BREAK" &gt;
    073 *                               {
    074 *                                       GROUP_KEY  : CLM5,CLM6....     ;
    075 *                                       OUT_TYPE : first                       ;
    076 *                               }
    077 *                 &lt;/og:tableFilter&gt;
    078 *
    079 * @og.rev 6.7.9.1 (2017/05/19) 新規追加
    080 * @og.rev 7.0.0.1 (2018/10/09) ｸﾞﾙｰﾌﾟで、まとめる処理を止めます。
    081 * @og.rev 7.0.1.1 (2018/10/22) ﾛｼﾞｯｸ見直し
    082 *
    083 * @version  6.7  2017/05/19
    084 * @author   Kazuhiko Hasegawa
    085 * @since    JDK1.8,
    086 */
    087public class TableFilter_KEY_BREAK extends AbstractTableFilter {
    088        /** このプログラムのVERSION文字列を設定します。   {@value} */
    089        private static final String VERSION = "7.2.4.0 (2020/05/11)" ;
    090
    091        /**
    092         * デフォルトコンストラクター
    093         *
    094         * @og.rev 6.7.9.1 (2017/05/19) 新規追加
    095         * @og.rev 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加
    096         * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のｷｰﾜｰﾄﾞ追加
    097         */
    098        public TableFilter_KEY_BREAK() {
    099                super();
    100                initSet( "GROUP_KEY"    , "キーブレイクの判定を行うカラムを、CSV形式で設定します。" );
    101                initSet( "OUT_TYPE"     , "出力するデータのタイプを指定[first/last/range]を指定します。(初期値:first 最初のデータ)"   );
    102                initSet( "MIN_CLM"      , "キーブレイク時に、指定のカラムの最小値をデータに書き込みます。"     );
    103                initSet( "MAX_CLM"      , "キーブレイク時に、指定のカラムの最大値をデータに書き込みます。"     );
    104                initSet( "AVG_CLM"      , "キーブレイク時に、指定のカラムの平均値をデータに書き込みます。"     );
    105                initSet( "MID_CLM"      , "キーブレイク時に、指定のカラムの最小値と最大値の中間の値をデータに書き込みます。"    );
    106                initSet( "CNT_CLM"      , "キーブレイク時に、指定のカラムのデータ件数をデータに書き込みます。"   );
    107        }
    108
    109        /**
    110         * DBTableModel処理を実行します。
    111         *
    112         * @og.rev 6.7.9.1 (2017/05/19) 新規追加
    113         * @og.rev 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加
    114         * @og.rev 7.0.1.1 (2018/10/22) ﾛｼﾞｯｸ見直し
    115         * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のｷｰﾜｰﾄﾞ追加
    116         * @og.rev 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ゼロ文字列をセットします。
    117         *
    118         * @return 処理結果のDBTableModel
    119         */
    120        public DBTableModel execute() {
    121                final DBTableModel table  = getDBTableModel();
    122                final DBTableModel rtnTbl = table.newModel();                           // 削除ではなく、追加していきます。
    123                final int rowCnt = table.getRowCount();
    124                if( rowCnt == 0 ) { return rtnTbl; }                                            // 7.0.1.3 (2018/11/12) row<=rowCnt を追加したので、0件なら即終了
    125
    126                final String[]  brkClms = StringUtil.csv2Array( getValue( "GROUP_KEY" ) );
    127
    128                // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加
    129                final String    outType = StringUtil.nval( getValue( "OUT_TYPE"  ), "first" ) ;
    130
    131                final boolean   useFirst = "first".equalsIgnoreCase( outType ) || "range".equalsIgnoreCase( outType );  // firstかrange時に使用
    132                final boolean   useLast  = "last".equalsIgnoreCase( outType )  || "range".equalsIgnoreCase( outType ) ; // lastかrange 時に使用
    133
    134                // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加(useLast=true のときのみ使用)
    135                final int       minClmNo = table.getColumnNo( getValue( "MIN_CLM"  ), false ) ; // カラムが存在しなければ、-1
    136                final int       maxClmNo = table.getColumnNo( getValue( "MAX_CLM"  ), false ) ; // カラムが存在しなければ、-1
    137                final int       avgClmNo = table.getColumnNo( getValue( "AVG_CLM"  ), false ) ; // カラムが存在しなければ、-1
    138                final int       midClmNo = table.getColumnNo( getValue( "MID_CLM"  ), false ) ; // 7.0.1.3 (2018/11/12) カラムが存在しなければ、-1
    139                final int       cntClmNo = table.getColumnNo( getValue( "CNT_CLM"  ), false ) ; // カラムが存在しなければ、-1
    140
    141                final int[]    brkClmNo = new int[brkClms.length];                      // ブレイクキーカラムの番号
    142
    143                for( int i=0; i<brkClms.length; i++ ) {
    144                        brkClmNo[i] = table.getColumnNo( brkClms[i],false );    // カラムが存在しなければ、-1
    145                }
    146
    147                // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加(useLast=true のときのみ使用)
    148                double  minData  = Double.POSITIVE_INFINITY ;           // 仮数部の桁数の限界は15桁なので、日付型(14桁)は、処理できる。
    149                double  maxData  = Double.NEGATIVE_INFINITY ;
    150                double  total    = 0.0 ;
    151                int             cntData  = 0 ;          //
    152                boolean isLong   = true;        // データに、少数点以下をつけるかどうかの判定です。
    153                double  midMin   = Double.POSITIVE_INFINITY ;
    154                double  midMax   = Double.NEGATIVE_INFINITY ;
    155
    156                String oldBlkKeys = null;       // 前回ブレイクキーの値
    157
    158//              final int rowCnt = table.getRowCount();
    159
    160                String[] oldData = null;
    161                // 7.0.1.3 (2018/11/12) 最後のデータの処理を行うために、row<=rowCnt と1回余計に回します。
    162                for( int row=0; row<=rowCnt; row++ ) {
    163                        final String[] data = row == rowCnt ? null : table.getValues( row );            // row<=rowCnt の影響
    164                        try {
    165                                final String brkKeys = getKeys( brkClmNo , data );              // ブレークキー(data==nullの場合、ゼロ文字列)
    166                                if( !brkKeys.equalsIgnoreCase( oldBlkKeys ) ) {                 // キーブレイク
    167                                        if( row>0 ) {
    168                                                // 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ゼロ文字列をセットします。
    169//                                              if( minClmNo >= 0 ) { oldData[minClmNo] = isLong ? String.valueOf( Math.round( minData ) ) : String.valueOf( minData ) ; }
    170//                                              if( maxClmNo >= 0 ) { oldData[maxClmNo] = isLong ? String.valueOf( Math.round( maxData ) ) : String.valueOf( maxData ) ; }
    171                                                if( minClmNo >= 0 ) {
    172                                                        if( minData == Double.POSITIVE_INFINITY ) {
    173                                                                oldData[minClmNo] = "";                 // 7.2.4.0 (2020/05/11)
    174                                                        }
    175                                                        else {
    176                                                                oldData[minClmNo] = isLong ? String.valueOf( Math.round( minData ) ) : String.valueOf( minData ) ;
    177                                                        }
    178                                                }
    179                                                if( maxClmNo >= 0 ) {
    180                                                        if( maxData == Double.NEGATIVE_INFINITY ) {
    181                                                                oldData[maxClmNo] = "";                 // 7.2.4.0 (2020/05/11)
    182                                                        }
    183                                                        else {
    184                                                                oldData[maxClmNo] = isLong ? String.valueOf( Math.round( maxData ) ) : String.valueOf( maxData ) ;
    185                                                        }
    186                                                }
    187                                                if( avgClmNo >= 0 ) { oldData[avgClmNo] = String.format( "%.3f", total/cntData ); }
    188                                                if( midClmNo >= 0 ) { oldData[midClmNo] = getMiddle( midMin,midMax ); }
    189                                                if( cntClmNo >= 0 ) { oldData[cntClmNo] = String.valueOf( cntData ); }
    190
    191                                                minData = Double.POSITIVE_INFINITY  ;
    192                                                maxData = Double.NEGATIVE_INFINITY  ;
    193                                                total   = 0.0 ;
    194                                                midMin  = Double.POSITIVE_INFINITY  ;
    195                                                midMax  = Double.NEGATIVE_INFINITY  ;
    196
    197                                                if( useLast ) {
    198                                                        // useFirst=true で、cntData == 1 の場合は、First行は削除します(1件を2件に増やさない)。
    199                                                        if( useFirst ) {
    200                                                                final int rCnt = rtnTbl.getRowCount();
    201                                                                if( cntData == 1 ) {                                            // 1行しかない場合は、First行は削除します(1件を2件に増やさない)
    202                                                                        rtnTbl.removeValue( rCnt-1 );
    203                                                                }
    204                                                                else {
    205                                                                        final String[] fstData = rtnTbl.getValues( rCnt-1 );    // 前のデータ=First行に、最大、最小等のデータを反映させます。
    206                                                                        if( minClmNo >= 0 ) { fstData[minClmNo] = oldData[minClmNo]; }
    207                                                                        if( maxClmNo >= 0 ) { fstData[maxClmNo] = oldData[maxClmNo]; }
    208                                                                        if( avgClmNo >= 0 ) { fstData[avgClmNo] = oldData[avgClmNo]; }
    209                                                                        if( midClmNo >= 0 ) { fstData[midClmNo] = oldData[midClmNo]; }
    210                                                                        if( cntClmNo >= 0 ) { fstData[cntClmNo] = oldData[cntClmNo]; }
    211                                                                }
    212                                                        }
    213
    214                                                        rtnTbl.addColumnValues( oldData );                      // ブレイクした一つ前＝最後のデータ
    215                                                }
    216                                                if( row == rowCnt ) { break; }                                  // 最後のデータの処理を行うために、row<=rowCnt と1回余計に回します。
    217                                        }
    218
    219                                        if( useFirst ) {                                                                        // useLast=true で、cntData == 1 の場合は、登録しません
    220                                                rtnTbl.addColumnValues( data );                                 // ブレイク時のデータを登録します。
    221                                        }
    222
    223                                        oldBlkKeys = brkKeys;
    224                                        cntData    = 0 ;
    225                                }
    226                                oldData = data;                                                                                 // 一つ前のデータ
    227                                cntData++;                              // 毎回、カラムのある無しを判定するより、早そうなので常にカウントしておきます。
    228
    229                                // ブレイク時も集計処理は行います。
    230//                              if( useLast ) {
    231                                        if( minClmNo >= 0 && !StringUtil.isNull( data[minClmNo] ) ) {
    232                                                if( isLong && data[minClmNo].indexOf( '.' ) >= 0 ) { isLong = false; }                  // 一度、false になると、戻らない。
    233                                                minData = Math.min( minData, Double.parseDouble( data[minClmNo] ) );
    234                                        }
    235                                        if( maxClmNo >= 0 && !StringUtil.isNull( data[maxClmNo] ) ) {
    236                                                if( isLong && data[maxClmNo].indexOf( '.' ) >= 0 ) { isLong = false; }                  // 一度、false になると、戻らない。
    237                                                maxData = Math.max( maxData, Double.parseDouble( data[maxClmNo] ) );
    238                                        }
    239                                        if( avgClmNo >= 0 && !StringUtil.isNull( data[avgClmNo] ) ) {
    240                                                total += Double.parseDouble( data[avgClmNo] );
    241                                        }
    242                                        if( midClmNo >= 0 && !StringUtil.isNull( data[midClmNo] ) ) {
    243                                                final double mid = Double.parseDouble( data[midClmNo] );
    244                                                midMin = Math.min( midMin, mid );
    245                                                midMax = Math.max( midMax, mid );
    246                                        }
    247//                              }
    248                        }
    249                        catch( final RuntimeException ex ) {    // そのまま、継続して処理を行う。
    250                                // 6.5.0.1 (2016/10/21) ErrorMessage をまとめるのと、直接 Throwable を渡します。
    251                                makeErrorMessage( "TableFilter_KEY_BREAK Error",ErrorMessage.NG )
    252                                        .addMessage( row+1,ErrorMessage.NG,"KEY_BREAK" , StringUtil.array2csv( data ) )
    253                                        .addMessage( ex );
    254                        }
    255                }
    256//              // 一番最後に書込みが行われないので。
    257//              if( useLast ) {
    258//                      if( minClmNo >= 0 ) { oldData[minClmNo] = isLong ? String.valueOf( Math.round( minData ) ) : String.valueOf( minData ) ; }
    259//                      if( maxClmNo >= 0 ) { oldData[maxClmNo] = isLong ? String.valueOf( Math.round( maxData ) ) : String.valueOf( maxData ) ; }
    260//                      if( avgClmNo >= 0 ) { oldData[avgClmNo] = String.format( "%.3f", total/cntData ); }
    261//                      if( midClmNo >= 0 ) { oldData[midClmNo] = getMiddle( midMin,midMax ); }
    262//                      if( cntClmNo >= 0 ) { oldData[cntClmNo] = String.valueOf( cntData ); }
    263//
    264//                      rtnTbl.addColumnValues( oldData );
    265//              }
    266
    267                return rtnTbl;
    268        }
    269
    270        /**
    271         * 最小値と最大値の中間の値の文字列を作成します。
    272         *
    273         * 特殊系で、8桁か、14桁の場合、日付文字として中間の日付を求めます。
    274         *
    275         * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のｷｰﾜｰﾄﾞ追加
    276         *
    277         * @param min 最小値
    278         * @param max 最大値
    279         * @return 中間の値の文字列
    280         */
    281        private String getMiddle( final double min , final double max ) {
    282                final String minStr = String.valueOf( Math.round( min ) );              // 14桁の場合、2.0181103000000E13 見たいな表記になるため。
    283                final String maxStr = String.valueOf( Math.round( max ) );
    284                final int minLen = minStr.length();
    285
    286                final String midStr ;
    287                if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 ) ) {
    288                        final Calendar minCal = HybsDateUtil.getCalendar( minStr );
    289                        final Calendar maxCal = HybsDateUtil.getCalendar( maxStr );
    290                        final long     midTim = ( maxCal.getTimeInMillis() + minCal.getTimeInMillis() ) / 2 ;
    291
    292                        if( minLen == 8 ) {
    293                                midStr = DateSet.getDate( midTim , "yyyyMMdd" );
    294                        }
    295                        else {  // 14桁しかありえない
    296                                midStr = DateSet.getDate( midTim , "yyyyMMddHHmmss" );
    297                        }
    298                }
    299                else {
    300                        midStr = String.format( "%.3f", ( max + min ) / 2.0 );          // 日付型でなければ、minStr,maxStr は使わないので。
    301                }
    302
    303                return midStr;
    304        }
    305
    306        /**
    307         * キーの配列ｱﾄﾞﾚｽと、１行分のデータ配列から、キーとなる文字列を作成します。
    308         *
    309         * @og.rev 6.7.9.1 (2017/05/19) 新規追加
    310         * @og.rev 7.0.1.3 (2018/11/12) 最後のデータの処理を行うために、row<=rowCnt と1回余計に回す対応
    311         *
    312         * @param clms キーの配列ｱﾄﾞﾚｽ
    313         * @param rowData １行分のデータ配列
    314         * @return キーとなる文字列
    315         */
    316        private String getKeys( final int[] clms , final String[] rowData ) {
    317                if( rowData == null ) { return ""; }                                    // rowData がnull の場合は、キーブレイクとなる
    318
    319                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
    320                // 7.2.9.4 (2020/11/20) PMD:This for loop can be replaced by a foreach loop
    321                for( final int clm : clms ) {
    322                        if( clm >= 0 ) {
    323                                buf.append( rowData[clm] ).append( ':' );
    324                        }
    325                }
    326//              for( int i=0; i<clms.length; i++ ) {
    327//                      if( clms[i] >= 0 ) {
    328//                              buf.append( rowData[clms[i]] ).append( ':' );
    329//                      }
    330//              }
    331                return buf.toString();
    332        }
    333}