/*
 * 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.List;
import java.util.ArrayList;
import java.math.BigDecimal;				// 6.9.2.0 (2018/03/05)
import java.math.RoundingMode;				// 6.9.2.0 (2018/03/05)

/**
 * StandardDeviation は、登録されたデータから、標準偏差等の値を求めます。
 *
 * このﾌﾟﾛｸﾞﾗﾑは、０データを無視する特殊な計算をしています。
 * これは、成形条件ﾐﾄﾞﾙｳｴｱが、０をﾃﾞｰﾀなしとして扱っているためです。
 * よって、一般的な標準偏差等の値を求めることは出来ません。
 *
 * ここではデータを追加していき、取り出すときに、計算した値を文字列配列で返します。
 * 作成するカラムは、CNT,SUM,AVG,(STDEVS or STDEVP),COEFF,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S です。
 *
 * CNT(個数),SUM(合計),AVG(平均),
 * STDEVS(標本標準偏差:n-1) または、STDEVP(母標準偏差:n) を、useDEVP(trueで、母標準偏差) で選択します。
 * COEFF(変動係数) は、標準偏差(σ)を算術平均で、割ったものの百分率
 * M3S(～-3σ),M2S(-3σ～-2σ),M1S(-2σ～-σ),M0S(-σ～0),P0S(0～σ),P1S(σ～2σ),P2S(2σ～3σ),P3S(3σ～)
 * FILTERは、1:(-2σ～-σ or σ～2σ) , 2:(-3σ～-2σ or 2σ～3σ) , 3:(～-3σ or 3σ～) のみピックアップします。
 * 初期値の 0 は、フィルターなしです。
 *
 * 6.9.9.2 (2018/09/18)
 *   COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加します。
 *   これは、単位(%)で、指定の値以下の変動係数のﾚｺｰﾄﾞを出力しません。
 *
 * @og.rev 6.7.7.0 (2017/03/31) 新規追加
 * @og.rev 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
 *
 * @version  6.7.7  2017/03/31
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
class StandardDeviation {
	// * このプログラムのVERSION文字列を設定します。 {@value} */
	private static final String VERSION = "7.3.0.0 (2021/01/06)" ;

	// 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する
	private static final String[] ZERO_ARY = new String[0];		// null ではなく長さが0の配列を返すことを検討する

//	public  static final String[] ADD_CLMS = new String[] { "CNT","SUM","AVG","STDEVS","STDEVP","M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" };
	/** 追加カラム列 */
	public  static final String[] ADD_CLMS = new String[] { "CNT","SUM","AVG","STDEV","COEFF","M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" };	// 6.9.3.0 (2018/03/26)
	private static final int      HIST_SU  = 8;		// "M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" の個数

	private final List<Double> data = new ArrayList<>();

	private final int		ftype	;	// フィルタータイプ(0,1,2,3)
	private final boolean	useDEVP	;	// 初期値が、"P" (母標準偏差)
	private final String	format	;	// 初期値が、"%.3f"
	private final double	minCV	;	// 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターする

	private double sum ;
	private double pow ;				// 6.9.2.0 (2018/03/05) 分散の計算方法を変更

	/**
	 * 各種条件を指定した標準偏差計算用のインスタンスを作成します。
	 *
	 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
	 * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
	 *
	 * @param ftype		フィルタータイプ(0,1,2,3)
	 * @param useDEVP	初期値が、"P" (母標準偏差)
	 * @param format	初期値が、"%.3f"
	 * @param minCV		変動係数の最小値(%)
	 */
//	public StandardDeviation( final int ftype , final boolean useDEVP , final String format ) {
	public StandardDeviation( final int ftype , final boolean useDEVP , final String format , final String minCV ) {
		this.ftype	= ftype;
		this.useDEVP= useDEVP;
		this.format	= format;
		this.minCV	= parseDouble( minCV );		// 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターする
	}

	/**
	 * 内部情報を、初期化します。
	 *
	 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
	 *
	 */
	public void clear() {
		data.clear();
		sum = 0d;
		pow = 0d;
	}

	/**
	 * データを追加します。
	 *
	 * 引数の文字列を、double に変換して使用します。
	 * 変換できない場合は、エラーにはなりませんが、警告を出します。
	 * ただし、値が、0.0 の場合は、対象外にします。
	 *
	 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
	 * @og.rev 6.9.2.0 (2018/03/05) 分散の計算方法を変更
	 *
	 * @param strVal	データ
	 */
	public void addData( final String strVal ) {
		final double val = parseDouble( strVal );
		if( val != 0d ) {
			data.add( val );
			sum += val;
			pow += val * val ;		// 6.9.2.0 (2018/03/05)
		}
	}

	/**
	 * データから計算した結果を、文字列に変換して、返します。
	 *
	 * 標準偏差の式を
	 *    σ＝sqrt(Σ(Xi - Xave)^2 / n)
	 * から
	 *    σ＝sqrt(Σ(Xi^2) / n - Xave^2))
	 * に変形します。
	 * 参考：http://imagingsolution.blog107.fc2.com/blog-entry-62.html
	 *
	 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
	 * @og.rev 6.9.2.0 (2018/03/05) 分散の計算方法を変更
	 * @og.rev 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
	 * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
	 * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する
	 *
	 * @return データから計算した結果(無い場合は、長さゼロの配列)
	 * @og.rtnNotNull
	 */
	public String[] getData() {
		final int cnt = data.size();
//		if( cnt == 0 ) { return null; }
		if( cnt == 0 ) { return ZERO_ARY; }		// 7.3.0.0 (2021/01/06)

		final double avg = sum/cnt;				// 平均
	//	double sa1 = 0d;

	//	// 標準偏差の計算のために一度回す
	//	for( final double val : data ) {
	//		sa1 += Math.pow( val - avg , 2 ) ;
	//	}

	//	final double stdevs = cnt==1 ? 0d : Math.sqrt( sa1/(cnt-1) );		// 母集団の標本の標準偏差(標本標準偏差)
	//	final double stdevp = Math.sqrt( sa1/cnt );							// 母集団全ての標準偏差(母標準偏差)

		// 6.9.2.0 (2018/03/05) 分散の計算方法を変更
		final double vari = Math.abs( pow/cnt - avg * avg );					// マイナスはありえない（計算誤差）
		// 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
//		final double stdevp = Math.sqrt( vari );								// 母集団全ての標準偏差(母標準偏差)
//		final double stdevs = cnt==1 ? 0d : Math.sqrt( vari * cnt / (cnt-1) );	// 誤差があるので、掛け算してから、SQRTします。
		final double stdev  = useDEVP ? Math.sqrt( vari )
									  : cnt==1 ? 0d : Math.sqrt( vari * cnt / (cnt-1) );

		// 6.9.3.0 (2018/03/26) 変動係数(標準偏差/平均 の百分率)
		final double coeff = stdev / avg * 100 ;

		// 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
//		if( coeff < minCV ) { return null; }		// minCV より小さい場合は、null(ﾚｺｰﾄﾞを追加しない)
		if( coeff < minCV ) { return ZERO_ARY; }	// minCV より小さい場合は、null(ﾚｺｰﾄﾞを追加しない)

		// 6.9.2.0 (2018/03/05) 毎回計算ではなく固定値を使用します。
//		final double sa2 = useDEVP ? stdevp : stdevs ;						// useDEVP == true の場合、母標準偏差 を使用します。
//		final double SA1 = halfUp( useDEVP ? stdevp : stdevs ) ;			// useDEVP == true の場合、母標準偏差 を使用します。
		final double SA1 = halfUp( stdev ) ;								// useDEVP == true の場合、母標準偏差 を使用します。
		final double SA2 = SA1 * 2 ;										// 2σ
		final double SA3 = SA1 * 3 ;										// 3σ

		// 確率分布の合計グラフを作成するためにもう一度回す
		final int[] dtCnt = new int[HIST_SU];
		for( final double val : data ) {
			final double val2 = halfUp( val - avg );

			// 6.9.2.0 (2018/03/05) 毎回計算ではなく固定値を使用します。
//			if(        0.0d == val2 || cnt == 1      ) { dtCnt[4]++ ; }		//   0  ･･･データが１件の場合
//			else if(                   val2 < -sa2*3 ) { dtCnt[0]++ ; }		// -3σ＜
//			else if( -sa2*3 <= val2 && val2 < -sa2*2 ) { dtCnt[1]++ ; }		// -2σ＜
//			else if( -sa2*2 <= val2 && val2 < -sa2*1 ) { dtCnt[2]++ ; }		// -1σ＜
//			else if( -sa2*1 <= val2 && val2 <  0.0d  ) { dtCnt[3]++ ; }		//   0＜
//			else if(   0.0d <= val2 && val2 <  sa2*1 ) { dtCnt[4]++ ; }		//   0≦
//			else if(  sa2*1 <= val2 && val2 <  sa2*2 ) { dtCnt[5]++ ; }		//  1σ≦
//			else if(  sa2*2 <= val2 && val2 <  sa2*3 ) { dtCnt[6]++ ; }		//  2σ≦
//			else if(  sa2*3 <= val2                  ) { dtCnt[7]++ ; }		//  3σ≦

			// 標準偏差等が０に近い場合の誤差を考慮して、比較順を変更します。
			if( cnt == 1 || 0d == val2 || 0d == SA1 ) { dtCnt[4]++ ; }		//   0  ･･･データが１件、平均との差がゼロ、標準偏差がゼロ
			else if(  0d  <= val2 && val2 <  SA1  ) { dtCnt[4]++ ; }		//   0≦
			else if( -0d  == val2                 ) { dtCnt[3]++ ; }		//   0＜	平均との差がマイナスゼロの場合
			else if( -SA1 <= val2 && val2 <  0d   ) { dtCnt[3]++ ; }		//   0＜
			else if(  SA1 <= val2 && val2 <  SA2  ) { dtCnt[5]++ ; }		//  1σ≦
			else if( -SA2 <= val2 && val2 < -SA1  ) { dtCnt[2]++ ; }		// -1σ＜
			else if(  SA2 <= val2 && val2 <  SA3  ) { dtCnt[6]++ ; }		//  2σ≦
			else if( -SA3 <= val2 && val2 < -SA2  ) { dtCnt[1]++ ; }		// -2σ＜
			else if(  SA3 <= val2                 ) { dtCnt[7]++ ; }		//  3σ≦
			else if(                 val2 < -SA3  ) { dtCnt[0]++ ; }		// -3σ＜
		}

		// 6.7.2.0 (2017/01/16) FILTERパラメータ追加。
		// ここで、フィルター処理を行います。
		final boolean useValue ;
		switch( ftype ) {
			case 1  : useValue = ( dtCnt[0] + dtCnt[1] + dtCnt[2] + dtCnt[5] + dtCnt[6] + dtCnt[7] ) > 0 ; break ;
			case 2  : useValue = ( dtCnt[0] + dtCnt[1] +                       dtCnt[6] + dtCnt[7] ) > 0 ; break ;
			case 3  : useValue = ( dtCnt[0] +                                             dtCnt[7] ) > 0 ; break ;
			default : useValue = true ; break;
		}

		if( useValue ) {
			final String[] vals = new String[ADD_CLMS.length];	// CNT,SUM,AVG,STDEVS,STDEVP,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S の個数

			vals[0]  = String.valueOf( cnt );				// CNT
			vals[1]  = String.format( format , sum );		// SUM
			vals[2]  = String.format( format , avg );		// AVG
			// 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
//			vals[3]  = String.format( format , stdevs );	// STDEVS(標本標準偏差)
//			vals[4]  = String.format( format , stdevp );	// STDEVP(母標準偏差)
			vals[3]  = String.format( format , stdev );		// useDEVP=true で、STDEVP(母標準偏差) , false で、STDEVS(標本標準偏差)
			vals[4]  = String.format( "%.2f" , coeff );		// 6.9.3.0 (2018/03/26) 変動係数は、小数第二位で四捨五入します。
			vals[5]  = String.valueOf( dtCnt[0] );			// M3S
			vals[6]  = String.valueOf( dtCnt[1] );			// M2S
			vals[7]  = String.valueOf( dtCnt[2] );			// M1S
			vals[8]  = String.valueOf( dtCnt[3] );			// M0S
			vals[9]  = String.valueOf( dtCnt[4] );			// P0S
			vals[10] = String.valueOf( dtCnt[5] );			// P1S
			vals[11] = String.valueOf( dtCnt[6] );			// P2S
			vals[12] = String.valueOf( dtCnt[7] );			// P3S

			return vals;
		}
//		return null;
		return ZERO_ARY;		// 7.3.0.0 (2021/01/06)
	}

	/**
	 * 引数の文字列を、double に変換して返します。
	 *
	 * 処理が止まらないように、null や、変換ミスの場合は、ゼロを返します。
	 *
	 * @param	val	変換する元の文字列
	 *
	 * @return	変換後のdouble
	 * @og.rtnNotNull
	 */
	private double parseDouble( final String val ) {
		double rtn = 0.0d;
		if( val != null && !val.trim().isEmpty() ) {
			try {
				rtn = Double.parseDouble( val.trim() );
			}
			catch( final NumberFormatException ex ) {
				final String errMsg = "文字列を数値に変換できません。val=[" + val + "]" + ex.getMessage();
				System.out.println( errMsg );
			}
		}

		return rtn ;
	}

	/**
	 * 引数のdoubleを、少数点３桁で、四捨五入(HALF_UP)します。
	 *
	 * 長い処理式を、短くすることが目的のメソッドです。
	 *
	 * @param	val	変換する元のdouble
	 *
	 * @return	変換後のdouble
	 * @og.rtnNotNull
	 */
	private double halfUp( final double val ) {
		return BigDecimal.valueOf( val ).setScale( 3 , RoundingMode.HALF_UP ).doubleValue();
	}
}
