/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.plugin.table;

import java.util.Calendar;									// 7.0.1.3 (2018/11/12)

import org.opengion.hayabusa.db.AbstractTableFilter;
import org.opengion.hayabusa.db.DBTableModel;

import org.opengion.fukurou.util.ErrorMessage;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.HybsDateUtil;				// 7.0.1.3 (2018/11/12)
import org.opengion.fukurou.system.DateSet;					// 7.0.1.3 (2018/11/12)

/**
 * TableFilter_KEY_BREAK は、TableFilter ｲﾝﾀｰﾌｪｰｽを継承した、DBTableModel 処理用の
 * 実装ｸﾗｽです。
 *
 * ここでは、指定のｶﾗﾑに対して、ｷｰﾌﾞﾚｲｸが発生したときのﾃﾞｰﾀ処理方法を指定できます。
 * 主として、ｸﾞﾙｰﾋﾟﾝｸﾞ処理を行うのですが、ｿｰﾄされﾃﾞｰﾀの並び順で、ｷｰﾌﾞﾚｲｸするため、
 * 同一ｷｰが存在していても、並び順が離れている場合、別のｷｰとしてﾌﾞﾚｲｸします。
 *
 * GROUP_KEY  : ｷｰﾌﾞﾚｲｸの判定を行うｶﾗﾑを、CSV形式で設定します。
 * OUT_TYPE   : 出力するﾃﾞｰﾀのﾀｲﾌﾟを指定します。
 *                first : 最初のﾃﾞｰﾀ(ﾌﾞﾚｲｸ直後のﾃﾞｰﾀ)を出力します。(初期値)
 *                last  : 最後のﾃﾞｰﾀ(ﾌﾞﾚｲｸ直前のﾃﾞｰﾀ)を出力します。
 *                range : 最初のﾃﾞｰﾀと最後のﾃﾞｰﾀを出力します。
 *
 * firstは、ｷｰﾌﾞﾚｲｸ時のﾃﾞｰﾀを残します。つまり、ｷｰの最初に現れたﾃﾞｰﾀです。
 * lastは、ｷｰﾌﾞﾚｲｸの直前のﾃﾞｰﾀを残します。これは、同一ｷｰの最後のﾃﾞｰﾀということになります。
 * rangeは、firstと、last つまり、同値ｷｰの最初と最後のﾃﾞｰﾀを残します。
 *
 * もし、ｷｰが、1行だけの場合、firstも、lastも、同じ行を指すことになります。
 * その場合、rangeは、その1行だけになります(2行出力されません)。
 *
 * 例：機種と日付と、状況Fがあったとして、日付、機種、状況F でｿｰﾄし、機種をｸﾞﾙｰﾌﾟｷｰ、
 * 状況Fをﾌﾞﾚｲｸｷｰとすれば、日付の順に、機種の中で、状況Fがﾌﾞﾚｰｸしたときのみ、
 * ﾃﾞｰﾀを残す、ということが可能になります。7.0.0.1 (2018/10/09) Delete
 *
 * OUT_TYPE に、lastか、range を指定した場合のみ、最大、最小、平均、中間、個数の集計処理が行えます。
 * これらの設定は、指定のｶﾗﾑのﾃﾞｰﾀに反映されます。
 * MIN_CLM  : ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最小値をﾃﾞｰﾀに書き込みます。
 * MAX_CLM  : ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最大値をﾃﾞｰﾀに書き込みます。
 * AVG_CLM  : ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの平均値をﾃﾞｰﾀに書き込みます。
 * MID_CLM  : ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最小値と最大値の中間の値をﾃﾞｰﾀに書き込みます。
 * DIF_CLM  : ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最大値から最小値を引いた値(差)をﾃﾞｰﾀに書き込みます。8.0.1.2 (2021/11/19)
 * CNT_CLM  : ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑのﾃﾞｰﾀ件数をﾃﾞｰﾀに書き込みます。
 *
 * これらのｶﾗﾑの値は、数値で表現できるもので無ければなりません。
 * 例えば、20180101000000 のような、日付でも数字のみなら、OKです。
 *
 * 8.0.1.2 (2021/11/19) DIF_CLM 差分計算
 *  8桁か14桁で、先頭"20"の場合は、日付型と判定します。
 *  その場合、8桁は、経過日数を返し、14桁は、MM/dd HH:mm 形式で返します。
 *
 * ﾊﾟﾗﾒｰﾀは、tableFilterﾀｸﾞの keys, vals にそれぞれ記述するか、BODY 部にCSS形式で記述します。
 *
 * @og.formSample
 * ●形式：
 *		① &lt;og:tableFilter classId="KEY_BREAK"
 *				keys="GROUP_KEY,OUT_TYPE"
 * 				vals='"CLM5,CLM6....",first' /&gt;
 *
 *		② &lt;og:tableFilter classId="KEY_BREAK" &gt;
 *				 {
 *					GROUP_KEY : CLM5,CLM6....	;
 *					OUT_TYPE  : first			;
 *				 }
 *		   &lt;/og:tableFilter&gt;
 *
 * @og.rev 6.7.9.1 (2017/05/19) 新規追加
 * @og.rev 7.0.0.1 (2018/10/09) ｸﾞﾙｰﾌﾟで、まとめる処理を止めます。
 * @og.rev 7.0.1.1 (2018/10/22) ﾛｼﾞｯｸ見直し
 *
 * @version	6.7  2017/05/19
 * @author	Kazuhiko Hasegawa
 * @since	JDK1.8,
 */
public class TableFilter_KEY_BREAK extends AbstractTableFilter {
	/** このﾌﾟﾛｸﾞﾗﾑのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "8.5.0.0 (2023/04/21)" ;

	/** 8.0.1.2 (2021/11/19) 日単位変換係数 */
	private static final int MILLIS_OF_DAY = 1000 * 60 * 60 * 24;

	/**
	 * ﾃﾞﾌｫﾙﾄｺﾝｽﾄﾗｸﾀｰ
	 *
	 * @og.rev 6.7.9.1 (2017/05/19) 新規追加
	 * @og.rev 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加
	 * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のｷｰﾜｰﾄﾞ追加
	 * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のｷｰﾜｰﾄﾞ追加
	 */
	public TableFilter_KEY_BREAK() {
		super();
		initSet( "GROUP_KEY"	, "ｷｰﾌﾞﾚｲｸの判定を行うｶﾗﾑを、CSV形式で設定します。" );
		initSet( "OUT_TYPE"	, "出力するﾃﾞｰﾀのﾀｲﾌﾟを指定[first/last/range]を指定します。(初期値:first 最初のﾃﾞｰﾀ)"	);
		initSet( "MIN_CLM"	, "ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最小値をﾃﾞｰﾀに書き込みます。"	);
		initSet( "MAX_CLM"	, "ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最大値をﾃﾞｰﾀに書き込みます。"	);
		initSet( "AVG_CLM"	, "ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの平均値をﾃﾞｰﾀに書き込みます。"	);
		initSet( "MID_CLM"	, "ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最小値と最大値の中間の値をﾃﾞｰﾀに書き込みます。"	);
		initSet( "DIF_CLM"	, "ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑの最大値から最小値を引いた値(差)をﾃﾞｰﾀに書き込みます。"	);	// 8.0.1.2 (2021/11/19)
		initSet( "CNT_CLM"	, "ｷｰﾌﾞﾚｲｸ時に、指定のｶﾗﾑのﾃﾞｰﾀ件数をﾃﾞｰﾀに書き込みます。"	);
	}

	/**
	 * DBTableModel処理を実行します。
	 *
	 * @og.rev 6.7.9.1 (2017/05/19) 新規追加
	 * @og.rev 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加
	 * @og.rev 7.0.1.1 (2018/10/22) ﾛｼﾞｯｸ見直し
	 * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のｷｰﾜｰﾄﾞ追加
	 * @og.rev 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ｾﾞﾛ文字列をｾｯﾄします。
	 * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のｷｰﾜｰﾄﾞ追加
	 * @og.rev 8.1.2.1 (2022/03/25) OUT_TYPE="first" 時に最小,最大,平均,件数の集計ができるように機能追加
	 * @og.rev 8.5.0.0 (2023/04/21) OUT_TYPE="first" 時に range の処理を誤って行っていた。
	 *
	 * @return 処理結果のDBTableModel
	 */
	public DBTableModel execute() {
		final DBTableModel table  = getDBTableModel();
		final DBTableModel rtnTbl = table.newModel();				// 削除ではなく、追加していきます。
		final int rowCnt = table.getRowCount();
		if( rowCnt == 0 ) { return rtnTbl; }						// 7.0.1.3 (2018/11/12) row<=rowCnt を追加したので、0件なら即終了

		final String[]	brkClms	= StringUtil.csv2Array( getValue( "GROUP_KEY" ) );

		// 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加
		final String	outType = StringUtil.nval( getValue( "OUT_TYPE" ), "first" ) ;

		final boolean	useFirst = "first".equalsIgnoreCase( outType ) || "range".equalsIgnoreCase( outType );	// firstかrange時に使用
		final boolean	useLast  = "last".equalsIgnoreCase( outType )  || "range".equalsIgnoreCase( outType ) ;	// lastかrange 時に使用

		// 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加(useLast=true のときのみ使用)
		final int	minClmNo = table.getColumnNo( getValue( "MIN_CLM" ), false ) ;	// ｶﾗﾑが存在しなければ、-1
		final int	maxClmNo = table.getColumnNo( getValue( "MAX_CLM" ), false ) ;	// ｶﾗﾑが存在しなければ、-1
		final int	avgClmNo = table.getColumnNo( getValue( "AVG_CLM" ), false ) ;	// ｶﾗﾑが存在しなければ、-1
		final int	midClmNo = table.getColumnNo( getValue( "MID_CLM" ), false ) ;	// 7.0.1.3 (2018/11/12) ｶﾗﾑが存在しなければ、-1
		final int	difClmNo = table.getColumnNo( getValue( "DIF_CLM" ), false ) ;	// 8.0.1.2 (2021/11/19) ｶﾗﾑが存在しなければ、-1
		final int	cntClmNo = table.getColumnNo( getValue( "CNT_CLM" ), false ) ;	// ｶﾗﾑが存在しなければ、-1

		final int[] brkClmNo = new int[brkClms.length];				// ﾌﾞﾚｲｸｷｰｶﾗﾑの番号

		for( int i=0; i<brkClms.length; i++ ) {
			brkClmNo[i] = table.getColumnNo( brkClms[i],false );	// ｶﾗﾑが存在しなければ、-1
		}

		// 7.0.0.1 (2018/10/09) 最小,最大,平均,件数 を集計するためのｷｰﾜｰﾄﾞ追加(useLast=true のときのみ使用)
		double	minData = Double.POSITIVE_INFINITY ;		// 仮数部の桁数の限界は15桁なので、日付型(14桁)は、処理できる。
		double	maxData = Double.NEGATIVE_INFINITY ;
		double	total   = 0.0 ;
		int		cntData = 0 ;
		boolean	isLong	= true;								// ﾃﾞｰﾀに、少数点以下をつけるかどうかの判定です。
		double	midMin	= Double.POSITIVE_INFINITY ;
		double	midMax	= Double.NEGATIVE_INFINITY ;
		double	difMin	= Double.POSITIVE_INFINITY ;		// 8.0.1.2 (2021/11/19)
		double	difMax	= Double.NEGATIVE_INFINITY ;		// 8.0.1.2 (2021/11/19)

		String oldBlkKeys = null;							// 前回ﾌﾞﾚｲｸｷｰの値

		String[] oldData = null;
		// 7.0.1.3 (2018/11/12) 最後のﾃﾞｰﾀの処理を行うために、row<=rowCnt と1回余計に回します。
		for( int row=0; row<=rowCnt; row++ ) {
			final String[] data = row == rowCnt ? null : table.getValues( row );	// row<=rowCnt の影響
			try {
				final String brkKeys = getKeys( brkClmNo , data );		// ﾌﾞﾚｰｸｷｰ(data==nullの場合、ｾﾞﾛ文字列)
				if( !brkKeys.equalsIgnoreCase( oldBlkKeys ) ) {			// ｷｰﾌﾞﾚｲｸ
					// 初回必ずﾌﾞﾚｲｸするので、row==0 時は処理しない。
					if( row>0 ) {
						// 7.2.4.0 (2020/05/11) MIN_CLMとMAX_CLMが不定の場合は、ｾﾞﾛ文字列をｾｯﾄします。
						if( minClmNo >= 0 ) {
							if( minData == Double.POSITIVE_INFINITY ) {
								oldData[minClmNo] = "";		// 7.2.4.0 (2020/05/11)
							}
							else {
								oldData[minClmNo] = isLong ? String.valueOf( Math.round( minData ) ) : String.valueOf( minData ) ;
							}
						}
						if( maxClmNo >= 0 ) {
							if( maxData == Double.NEGATIVE_INFINITY ) {
								oldData[maxClmNo] = "";		// 7.2.4.0 (2020/05/11)
							}
							else {
								oldData[maxClmNo] = isLong ? String.valueOf( Math.round( maxData ) ) : String.valueOf( maxData ) ;
							}
						}
						if( avgClmNo >= 0 ) { oldData[avgClmNo] = String.format( "%.3f", total/cntData ); }
						if( midClmNo >= 0 ) { oldData[midClmNo] = getMiddle( midMin,midMax ); }
						if( difClmNo >= 0 ) { oldData[difClmNo] = getDifference( difMin,difMax ); }	// 8.0.1.2 (2021/11/19)
						if( cntClmNo >= 0 ) { oldData[cntClmNo] = String.valueOf( cntData ); }

						minData = Double.POSITIVE_INFINITY ;
						maxData = Double.NEGATIVE_INFINITY ;
						total   = 0.0 ;
						midMin  = Double.POSITIVE_INFINITY ;
						midMax  = Double.NEGATIVE_INFINITY ;
						difMin  = Double.POSITIVE_INFINITY ;		// 8.0.1.2 (2021/11/19)
						difMax  = Double.NEGATIVE_INFINITY ;		// 8.0.1.2 (2021/11/19)

						// 8.1.2.1 (2022/03/25) OUT_TYPE="first" 時に最小,最大,平均,件数の集計ができるように機能追加
			//			if( useLast ) {
							// useFirst=true で、cntData == 1 の場合は、First行は削除します(1件を2件に増やさない)。
			//				if( useFirst ) {
							if( useFirst && useLast ) {		// 8.5.0.0 (2023/04/21) range の場合の処理
								final int rCnt = rtnTbl.getRowCount();
								if( cntData == 1 ) {						// 1行しかない場合は、First行は削除します(1件を2件に増やさない)
									rtnTbl.removeValue( rCnt-1 );
								}
								else {
									// useLast && useFirst ⇒ range 指定の処理。 前のﾃﾞｰﾀ=First行に、最大、最小等のﾃﾞｰﾀを反映させます。
									final String[] fstData = rtnTbl.getValues( rCnt-1 );
									if( minClmNo >= 0 ) { fstData[minClmNo] = oldData[minClmNo]; }
									if( maxClmNo >= 0 ) { fstData[maxClmNo] = oldData[maxClmNo]; }
									if( avgClmNo >= 0 ) { fstData[avgClmNo] = oldData[avgClmNo]; }
									if( midClmNo >= 0 ) { fstData[midClmNo] = oldData[midClmNo]; }
									if( difClmNo >= 0 ) { fstData[difClmNo] = oldData[difClmNo]; }	// 8.0.1.2 (2021/11/19)
									if( cntClmNo >= 0 ) { fstData[cntClmNo] = oldData[cntClmNo]; }
								}
							}
			//				rtnTbl.addColumnValues( oldData );			// ﾌﾞﾚｲｸした一つ前＝最後のﾃﾞｰﾀ
			//			}

						if( useLast ) {
							rtnTbl.addColumnValues( oldData );			// ﾌﾞﾚｲｸした一つ前＝最後のﾃﾞｰﾀ
						}

						if( row == rowCnt ) { break; }					// 最後のﾃﾞｰﾀの処理を行うために、row<=rowCnt と1回余計に回します。
					}

					if( useFirst ) {									// useLast=true で、cntData == 1 の場合は、登録しません
						rtnTbl.addColumnValues( data );					// ﾌﾞﾚｲｸ時のﾃﾞｰﾀを登録します。
					}

					oldBlkKeys = brkKeys;
					cntData    = 0 ;
				}
				oldData = data;											// 一つ前のﾃﾞｰﾀ
				cntData++;				// 毎回、ｶﾗﾑのある無しを判定するより、早そうなので常にｶｳﾝﾄしておきます。

				// ﾌﾞﾚｲｸ時も集計処理は行います。
				if( minClmNo >= 0 && !StringUtil.isNull( data[minClmNo] ) ) {
					if( isLong && data[minClmNo].indexOf( '.' ) >= 0 ) { isLong = false; }			// 一度、false になると、戻らない。
					minData = Math.min( minData, Double.parseDouble( data[minClmNo] ) );
				}
				if( maxClmNo >= 0 && !StringUtil.isNull( data[maxClmNo] ) ) {
					if( isLong && data[maxClmNo].indexOf( '.' ) >= 0 ) { isLong = false; }			// 一度、false になると、戻らない。
					maxData = Math.max( maxData, Double.parseDouble( data[maxClmNo] ) );
				}
				if( avgClmNo >= 0 && !StringUtil.isNull( data[avgClmNo] ) ) {
					total += Double.parseDouble( data[avgClmNo] );
				}
				if( midClmNo >= 0 && !StringUtil.isNull( data[midClmNo] ) ) {
					final double mid = Double.parseDouble( data[midClmNo] );
					midMin = Math.min( midMin, mid );
					midMax = Math.max( midMax, mid );
				}
				if( difClmNo >= 0 && !StringUtil.isNull( data[difClmNo] ) ) {		// 8.0.1.2 (2021/11/19)
					final double dif = Double.parseDouble( data[difClmNo] );
					difMin = Math.min( difMin, dif );
					difMax = Math.max( difMax, dif );
				}
			}
			catch( final RuntimeException ex ) {	// そのまま、継続して処理を行う。
				// 6.5.0.1 (2016/10/21) ErrorMessage をまとめるのと、直接 Throwable を渡します。
				makeErrorMessage( "TableFilter_KEY_BREAK Error",ErrorMessage.NG )
					.addMessage( row+1,ErrorMessage.NG,"KEY_BREAK" , StringUtil.array2csv( data ) )
					.addMessage( ex );
			}
		}

		return rtnTbl;
	}

	/**
	 * 最小値と最大値の中間の値の文字列を作成します。
	 *
	 * 特殊系で、8桁か、14桁の場合、日付文字として中間の日付を求めます。
	 *
	 * @og.rev 7.0.1.3 (2018/11/12) MID_CLM(最小値と最大値の中間の値)のｷｰﾜｰﾄﾞ追加
	 *
	 * @param	min	最小値
	 * @param	max	最大値
	 * @return	中間の値の文字列
	 */
	private String getMiddle( final double min , final double max ) {
		final String minStr = String.valueOf( Math.round( min ) );		// 14桁の場合、2.0181103000000E13 見たいな表記になるため。
		final String maxStr = String.valueOf( Math.round( max ) );
		final int minLen = minStr.length();

		final String midStr ;
		// 2000 年問題!? 先頭が "20" の場合は、日付型と判定する。
		if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 )
				&& minStr.startsWith("20") && maxStr.startsWith("20") ) {
			final Calendar minCal = HybsDateUtil.getCalendar( minStr );
			final Calendar maxCal = HybsDateUtil.getCalendar( maxStr );
			final long     midTim = ( maxCal.getTimeInMillis() + minCal.getTimeInMillis() ) / 2 ;

			if( minLen == 8 ) {
				midStr = DateSet.getDate( midTim , "yyyyMMdd" );
			}
			else {	// 14桁しかありえない
				midStr = DateSet.getDate( midTim , "yyyyMMddHHmmss" );
			}
		}
		else {
			midStr = String.format( "%.3f", ( max + min ) / 2.0 );		// 日付型でなければ、minStr,maxStr は使わないので。
		}

		return midStr;
	}

	/**
	 * 最大値から最小値を引いた値(差)の文字列を作成します。
	 *
	 * 特殊系で、8桁か、14桁の場合、日付文字として経過日数を求めます。
	 *
	 * @og.rev 8.0.1.2 (2021/11/19) DIF_CLM(最大値から最小値を引いた値(差))のｷｰﾜｰﾄﾞ追加
	 *
	 * @param	min	最小値
	 * @param	max	最大値
	 * @return	最大値から最小値を引いた値(差)の文字列
	 */
	private String getDifference( final double min , final double max ) {
		final String minStr = String.valueOf( Math.round( min ) );		// 14桁の場合、2.0181103000000E13 見たいな表記になるため。
		final String maxStr = String.valueOf( Math.round( max ) );
		final int minLen = minStr.length();

		final String midStr ;
		// 2000 年問題!? 先頭が "20" の場合は、日付型と判定する。
		if( minLen == maxStr.length() && ( minLen == 8 || minLen == 14 )
				&& minStr.startsWith("20") && maxStr.startsWith("20") ) {
			final Calendar minCal = HybsDateUtil.getCalendar( minStr );
			final Calendar maxCal = HybsDateUtil.getCalendar( maxStr );

			final long difTim = maxCal.getTimeInMillis() - minCal.getTimeInMillis() ;

			if( minLen == 8 ) {
				midStr = String.format( "%d", (int)difTim/MILLIS_OF_DAY );
			}
			else {	// 14桁しかありえない
				midStr = DateSet.getDate( difTim , "MM/dd HH:mm" );
			}
		}
		else {
			midStr = String.format( "%.3f", max - min );	// 日付型でなければ、minStr,maxStr は使わないので。
		}

		return midStr;
	}

	/**
	 * ｷｰの配列ｱﾄﾞﾚｽと、1行分のﾃﾞｰﾀ配列から、ｷｰとなる文字列を作成します。
	 *
	 * @og.rev 6.7.9.1 (2017/05/19) 新規追加
	 * @og.rev 7.0.1.3 (2018/11/12) 最後のﾃﾞｰﾀの処理を行うために、row<=rowCnt と1回余計に回す対応
	 *
	 * @param	clms	ｷｰの配列ｱﾄﾞﾚｽ
	 * @param	rowData	1行分のﾃﾞｰﾀ配列
	 * @return	ｷｰとなる文字列
	 */
	private String getKeys( final int[] clms , final String[] rowData ) {
		if( rowData == null ) { return ""; }					// rowData がnull の場合は、ｷｰﾌﾞﾚｲｸとなる

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		// 7.2.9.4 (2020/11/20) PMD:This for loop can be replaced by a foreach loop
		for( final int clm : clms ) {
			if( clm >= 0 ) {
				buf.append( rowData[clm] ).append( ':' );
			}
		}

		return buf.toString();
	}
}
