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.5.0.0 (2023/04/21)" ; 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 * @og.rev 8.5.0.0 (2023/04/21) OUT_TYPE="first" 時に range の処理を誤って行っていた。 130 * 131 * @return 処理結果のDBTableModel 132 */ 133 public DBTableModel execute() { 134 final DBTableModel table = getDBTableModel(); 135 final DBTableModel rtnTbl = table.newModel(); // 削除ではなく、追加していきます。 136 final int rowCnt = table.getRowCount(); 137 if( rowCnt == 0 ) { return rtnTbl; } // 7.0.1.3 (2018/11/12) row<=rowCnt を追加したので、0件なら即終了 138 139 final String[] brkClms = StringUtil.csv2Array( getValue( "GROUP_KEY" ) ); 140 141 // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加 142 final String outType = StringUtil.nval( getValue( "OUT_TYPE" ), "first" ) ; 143 144 final boolean useFirst = "first".equalsIgnoreCase( outType ) || "range".equalsIgnoreCase( outType ); // firstかrange時に使用 145 final boolean useLast = "last".equalsIgnoreCase( outType ) || "range".equalsIgnoreCase( outType ) ; // lastかrange 時に使用 146 147 // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加(useLast=true のときのみ使用) 148 final int minClmNo = table.getColumnNo( getValue( "MIN_CLM" ), false ) ; // カラムが存在しなければ、-1 149 final int maxClmNo = table.getColumnNo( getValue( "MAX_CLM" ), false ) ; // カラムが存在しなければ、-1 150 final int avgClmNo = table.getColumnNo( getValue( "AVG_CLM" ), false ) ; // カラムが存在しなければ、-1 151 final int midClmNo = table.getColumnNo( getValue( "MID_CLM" ), false ) ; // 7.0.1.3 (2018/11/12) カラムが存在しなければ、-1 152 final int difClmNo = table.getColumnNo( getValue( "DIF_CLM" ), false ) ; // 8.0.1.2 (2021/11/19) カラムが存在しなければ、-1 153 final int cntClmNo = table.getColumnNo( getValue( "CNT_CLM" ), false ) ; // カラムが存在しなければ、-1 154 155 final int[] brkClmNo = new int[brkClms.length]; // ブレイクキーカラムの番号 156 157 for( int i=0; i<brkClms.length; i++ ) { 158 brkClmNo[i] = table.getColumnNo( brkClms[i],false ); // カラムが存在しなければ、-1 159 } 160 161 // 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのキーワード追加(useLast=true のときのみ使用) 162 double minData = Double.POSITIVE_INFINITY ; // 仮数部の桁数の限界は15桁なので、日付型(14桁)は、処理できる。 163 double maxData = Double.NEGATIVE_INFINITY ; 164 double total = 0.0 ; 165 int cntData = 0 ; 166 boolean isLong = true; // データに、少数点以下をつけるかどうかの判定です。 167 double midMin = Double.POSITIVE_INFINITY ; 168 double midMax = Double.NEGATIVE_INFINITY ; 169 double difMin = Double.POSITIVE_INFINITY ; // 8.0.1.2 (2021/11/19) 170 double difMax = Double.NEGATIVE_INFINITY ; // 8.0.1.2 (2021/11/19) 171 172 String oldBlkKeys = null; // 前回ブレイクキーの値 173 174 String[] oldData = null; 175 // 7.0.1.3 (2018/11/12) 最後のデータの処理を行うために、row<=rowCnt と1回余計に回します。 176 for( int row=0; row<=rowCnt; row++ ) { 177 final String[] data = row == rowCnt ? null : table.getValues( row ); // row<=rowCnt の影響 178 try { 179 final String brkKeys = getKeys( brkClmNo , data ); // ブレークキー(data==nullの場合、ゼロ文字列) 180 if( !brkKeys.equalsIgnoreCase( oldBlkKeys ) ) { // キーブレイク 181 // 初回必ずブレイクするので、row==0 時は処理しない。 182 if( row>0 ) { 183 // 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ゼロ文字列をセットします。 184 if( minClmNo >= 0 ) { 185 if( minData == Double.POSITIVE_INFINITY ) { 186 oldData[minClmNo] = ""; // 7.2.4.0 (2020/05/11) 187 } 188 else { 189 oldData[minClmNo] = isLong ? String.valueOf( Math.round( minData ) ) : String.valueOf( minData ) ; 190 } 191 } 192 if( maxClmNo >= 0 ) { 193 if( maxData == Double.NEGATIVE_INFINITY ) { 194 oldData[maxClmNo] = ""; // 7.2.4.0 (2020/05/11) 195 } 196 else { 197 oldData[maxClmNo] = isLong ? String.valueOf( Math.round( maxData ) ) : String.valueOf( maxData ) ; 198 } 199 } 200 if( avgClmNo >= 0 ) { oldData[avgClmNo] = String.format( "%.3f", total/cntData ); } 201 if( midClmNo >= 0 ) { oldData[midClmNo] = getMiddle( midMin,midMax ); } 202 if( difClmNo >= 0 ) { oldData[difClmNo] = getDifference( difMin,difMax ); } // 8.0.1.2 (2021/11/19) 203 if( cntClmNo >= 0 ) { oldData[cntClmNo] = String.valueOf( cntData ); } 204 205 minData = Double.POSITIVE_INFINITY ; 206 maxData = Double.NEGATIVE_INFINITY ; 207 total = 0.0 ; 208 midMin = Double.POSITIVE_INFINITY ; 209 midMax = Double.NEGATIVE_INFINITY ; 210 difMin = Double.POSITIVE_INFINITY ; // 8.0.1.2 (2021/11/19) 211 difMax = Double.NEGATIVE_INFINITY ; // 8.0.1.2 (2021/11/19) 212 213 // 8.1.2.1 (2022/03/25) OUT_TYPE="first" 時に最小,最大,平均,件数の集計ができるように機能追加 214 // if( useLast ) { 215 // useFirst=true で、cntData == 1 の場合は、First行は削除します(1件を2件に増やさない)。 216 // if( useFirst ) { 217 if( useFirst && useLast ) { // 8.5.0.0 (2023/04/21) range の場合の処理 218 final int rCnt = rtnTbl.getRowCount(); 219 if( cntData == 1 ) { // 1行しかない場合は、First行は削除します(1件を2件に増やさない) 220 rtnTbl.removeValue( rCnt-1 ); 221 } 222 else { 223 // useLast && useFirst ⇒ range 指定の処理。 前のデータ=First行に、最大、最小等のデータを反映させます。 224 final String[] fstData = rtnTbl.getValues( rCnt-1 ); 225 if( minClmNo >= 0 ) { fstData[minClmNo] = oldData[minClmNo]; } 226 if( maxClmNo >= 0 ) { fstData[maxClmNo] = oldData[maxClmNo]; } 227 if( avgClmNo >= 0 ) { fstData[avgClmNo] = oldData[avgClmNo]; } 228 if( midClmNo >= 0 ) { fstData[midClmNo] = oldData[midClmNo]; } 229 if( difClmNo >= 0 ) { fstData[difClmNo] = oldData[difClmNo]; } // 8.0.1.2 (2021/11/19) 230 if( cntClmNo >= 0 ) { fstData[cntClmNo] = oldData[cntClmNo]; } 231 } 232 } 233 // rtnTbl.addColumnValues( oldData ); // ブレイクした一つ前=最後のデータ 234 // } 235 236 if( useLast ) { 237 rtnTbl.addColumnValues( oldData ); // ブレイクした一つ前=最後のデータ 238 } 239 240 if( row == rowCnt ) { break; } // 最後のデータの処理を行うために、row<=rowCnt と1回余計に回します。 241 } 242 243 if( useFirst ) { // useLast=true で、cntData == 1 の場合は、登録しません 244 rtnTbl.addColumnValues( data ); // ブレイク時のデータを登録します。 245 } 246 247 oldBlkKeys = brkKeys; 248 cntData = 0 ; 249 } 250 oldData = data; // 一つ前のデータ 251 cntData++; // 毎回、カラムのある無しを判定するより、早そうなので常にカウントしておきます。 252 253 // ブレイク時も集計処理は行います。 254 if( minClmNo >= 0 && !StringUtil.isNull( data[minClmNo] ) ) { 255 if( isLong && data[minClmNo].indexOf( '.' ) >= 0 ) { isLong = false; } // 一度、false になると、戻らない。 256 minData = Math.min( minData, Double.parseDouble( data[minClmNo] ) ); 257 } 258 if( maxClmNo >= 0 && !StringUtil.isNull( data[maxClmNo] ) ) { 259 if( isLong && data[maxClmNo].indexOf( '.' ) >= 0 ) { isLong = false; } // 一度、false になると、戻らない。 260 maxData = Math.max( maxData, Double.parseDouble( data[maxClmNo] ) ); 261 } 262 if( avgClmNo >= 0 && !StringUtil.isNull( data[avgClmNo] ) ) { 263 total += Double.parseDouble( data[avgClmNo] ); 264 } 265 if( midClmNo >= 0 && !StringUtil.isNull( data[midClmNo] ) ) { 266 final double mid = Double.parseDouble( data[midClmNo] ); 267 midMin = Math.min( midMin, mid ); 268 midMax = Math.max( midMax, mid ); 269 } 270 if( difClmNo >= 0 && !StringUtil.isNull( data[difClmNo] ) ) { // 8.0.1.2 (2021/11/19) 271 final double dif = Double.parseDouble( data[difClmNo] ); 272 difMin = Math.min( difMin, dif ); 273 difMax = Math.max( difMax, dif ); 274 } 275 } 276 catch( final RuntimeException ex ) { // そのまま、継続して処理を行う。 277 // 6.5.0.1 (2016/10/21) ErrorMessage をまとめるのと、直接 Throwable を渡します。 278 makeErrorMessage( "TableFilter_KEY_BREAK Error",ErrorMessage.NG ) 279 .addMessage( row+1,ErrorMessage.NG,"KEY_BREAK" , StringUtil.array2csv( data ) ) 280 .addMessage( ex ); 281 } 282 } 283 284 return rtnTbl; 285 } 286 287 /** 288 * 最小値と最大値の中間の値の文字列を作成します。 289 * 290 * 特殊系で、8桁か、14桁の場合、日付文字として中間の日付を求めます。 291 * 292 * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のキーワード追加 293 * 294 * @param min 最小値 295 * @param max 最大値 296 * @return 中間の値の文字列 297 */ 298 private String getMiddle( final double min , final double max ) { 299 final String minStr = String.valueOf( Math.round( min ) ); // 14桁の場合、2.0181103000000E13 見たいな表記になるため。 300 final String maxStr = String.valueOf( Math.round( max ) ); 301 final int minLen = minStr.length(); 302 303 final String midStr ; 304 // 2000 年問題!? 先頭が "20" の場合は、日付型と判定する。 305 if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 ) 306 && minStr.startsWith("20") && maxStr.startsWith("20") ) { 307 final Calendar minCal = HybsDateUtil.getCalendar( minStr ); 308 final Calendar maxCal = HybsDateUtil.getCalendar( maxStr ); 309 final long midTim = ( maxCal.getTimeInMillis() + minCal.getTimeInMillis() ) / 2 ; 310 311 if( minLen == 8 ) { 312 midStr = DateSet.getDate( midTim , "yyyyMMdd" ); 313 } 314 else { // 14桁しかありえない 315 midStr = DateSet.getDate( midTim , "yyyyMMddHHmmss" ); 316 } 317 } 318 else { 319 midStr = String.format( "%.3f", ( max + min ) / 2.0 ); // 日付型でなければ、minStr,maxStr は使わないので。 320 } 321 322 return midStr; 323 } 324 325 /** 326 * 最大値から最小値を引いた値(差)の文字列を作成します。 327 * 328 * 特殊系で、8桁か、14桁の場合、日付文字として経過日数を求めます。 329 * 330 * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のキーワード追加 331 * 332 * @param min 最小値 333 * @param max 最大値 334 * @return 最大値から最小値を引いた値(差)の文字列 335 */ 336 private String getDifference( final double min , final double max ) { 337 final String minStr = String.valueOf( Math.round( min ) ); // 14桁の場合、2.0181103000000E13 見たいな表記になるため。 338 final String maxStr = String.valueOf( Math.round( max ) ); 339 final int minLen = minStr.length(); 340 341 final String midStr ; 342 // 2000 年問題!? 先頭が "20" の場合は、日付型と判定する。 343 if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 ) 344 && minStr.startsWith("20") && maxStr.startsWith("20") ) { 345 final Calendar minCal = HybsDateUtil.getCalendar( minStr ); 346 final Calendar maxCal = HybsDateUtil.getCalendar( maxStr ); 347 348 final long difTim = maxCal.getTimeInMillis() - minCal.getTimeInMillis() ; 349 350 if( minLen == 8 ) { 351 midStr = String.format( "%d", (int)difTim/MILLIS_OF_DAY ); 352 } 353 else { // 14桁しかありえない 354 midStr = DateSet.getDate( difTim , "MM/dd HH:mm" ); 355 } 356 } 357 else { 358 midStr = String.format( "%.3f", max - min ); // 日付型でなければ、minStr,maxStr は使わないので。 359 } 360 361 return midStr; 362 } 363 364 /** 365 * キーの配列アドレスと、1行分のデータ配列から、キーとなる文字列を作成します。 366 * 367 * @og.rev 6.7.9.1 (2017/05/19) 新規追加 368 * @og.rev 7.0.1.3 (2018/11/12) 最後のデータの処理を行うために、row<=rowCnt と1回余計に回す対応 369 * 370 * @param clms キーの配列アドレス 371 * @param rowData 1行分のデータ配列 372 * @return キーとなる文字列 373 */ 374 private String getKeys( final int[] clms , final String[] rowData ) { 375 if( rowData == null ) { return ""; } // rowData がnull の場合は、キーブレイクとなる 376 377 final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE ); 378 // 7.2.9.4 (2020/11/20) PMD:This for loop can be replaced by a foreach loop 379 for( final int clm : clms ) { 380 if( clm >= 0 ) { 381 buf.append( rowData[clm] ).append( ':' ); 382 } 383 } 384 385 return buf.toString(); 386 } 387}