// =============================================================================
//  number
//  Copyright (c) 2005 - 2013 kei-n
//  URL : http://ehobby.jp
// =============================================================================
/**
 * 数値を扱うモジュール.
 */
module dworld.util.number;

import std.string;

/** 16進数の基数 (16).	*/
immutable int HEX = 16;

/** 10進数の基数 (10).	*/
immutable int DEC = 10;

/**  8進数の基数 (8).	*/
immutable int OCT =  8;

/**  2進数の基数 (2).	*/
immutable int BIN =  2;

/**
 * 数値変換エラー発生時の Exception クラス.
 */
class NumberFormatException : Exception
{
	this(string msg, string file = __FILE__, int line = __LINE__, Throwable next = null)
	{
		super(msg, file, line, next);
	}
}


/** 数値変換定義. */
private int[char] NUMBER_CHARS;
static this()
{
	string[] tmp = [ "0123456789ABCDEF", "0123456789abcdef" ];
	for (int i = 0; i < tmp.length; i++) {
		for (int j = 0; j < tmp[i].length; j++) {
			NUMBER_CHARS[tmp[i][j]] = j;
		}
	}
}


/**
 * 文字列を long 型の数値に変換します.
 *
 * Params:
 *	strVal = 数値文字列
 * Return:
 *	変換後の値
 */
long longValue(string strNum)
{
	NumberAnalyzer analyzer = new NumberAnalyzer(strNum);
	return analyzer.getValue();
}
unittest
{
	assert(0       == longValue("0"));
	assert(0       == longValue("0b0"));
	assert(0       == longValue("0x00"));
	assert(0       == longValue("0x_0000_0000"));
	assert(-1      == longValue("-1"));
	assert(-0b0101 == longValue("-0b0101"));
	assert(-0xFC   == longValue("-0xFC"));
	assert(-83     == longValue("-0123"));
	assert(1       == longValue("1"));
	assert(0b0101  == longValue("0b0101"));
	assert(0xFC    == longValue("0xFC"));
	assert(83      == longValue("0123"));
	assert(0xb123  == longValue("0b123H"));
	assert(1000    == longValue("1,000"));
	assert(1000    == longValue("1000UL"));

}


/**
 * 指定された文字を数値に変換します.
 * 変換できない場合 -1 を返します.
 *
 * Params:
 *	c     = 変換する文字
 *	radix = 進数
 * Return:
 *	変換後の数値
 */
int intValue(char c, int radix = DEC)
{
	if (c in NUMBER_CHARS)
	{
		int tmpVal = NUMBER_CHARS[c];
		if (tmpVal < radix)
		{
			return tmpVal;
		}
	}
	return -1;
}
unittest
{
	assert(intValue('A')       ==  -1);
	assert(intValue('A', HEX)  == 0xA);
	assert(intValue('8', OCT)  ==  -1);
	assert(intValue('Z', DEC)  ==  -1);
}


/**
 * 数値解析用クラス.
 */
class NumberAnalyzer
{
	private
	{
		/** 空白文字列.					*/
		static string SPACE_STRINGS   = " \t_";

		/** 数値末尾に付く付加情報文字.	*/
		static string ADDINFO_STRINGS = "ulULhH";

		/** 解析する文字列.				*/
		string numStr;

		/** 解析位置.					*/
		int    pos;

		/** 符号.						*/
		int    neg;

		/** 進数.						*/
		int    radix;

	}

	/**
	 * 指定された数値文字列を解析する NumberAnalyzer を構築します.
	 *
	 * Params:
	 *	number = 解析する数値文字列
	 */
	public
	this(string number)
	{
		this.numStr	= strip(number);
		this.pos	= 0;
		this.neg    = 1;
		this.radix	= DEC;		// デフォルト10進数
		if (this.numStr.length == 0)
		{
			throw new NumberFormatException("invalid format : " ~ number);
		}
	}
	unittest
	{
		NumberAnalyzer analyzer = new NumberAnalyzer("\t0x12345 ");
		assert(analyzer.numStr == "0x12345");
		assert(analyzer.pos    == 0        );
		assert(analyzer.neg    == 1        );
		assert(analyzer.radix  == DEC      );
		try {
			analyzer = new NumberAnalyzer("");
		} catch (NumberFormatException e) {
			// OK
		}
	}


	/**
	 * 解析後の値を返します.
	 *
	 * Return:
	 *	解析後の値
	 */
	public
	long getValue()
	{
		// 符号解析
		analysisNegative();

		// 進数解析
		analysisAdic();

		// 数値解析
		long value = analysisNumber();

		// 符号を付与
		value *= this.neg;

		return value;
	}
	unittest
	{
		NumberAnalyzer analyzer = new NumberAnalyzer("0b_0010_0001");
		long           value    = analyzer.getValue();
		assert(value == 0b00100001);
	}


	/**
	 * 符号解析.
	 * 負の符号の場合, neg を -1 に設定し, this.pos を 1 進めます.
	 * 正の符号の場合, neg を  1 に設定し, this.pos を 1 進めます.
	 * 符号なしの場合, neg を  1 に設定します.
	 */
	private
	void analysisNegative()
	{
		char c = this.numStr[this.pos];
		switch (c)
		{
			case '-': this.neg = -1; this.pos++; break;
			case '+': this.neg =  1; this.pos++; break;
			default:  this.neg =  1;
		}
	}
	unittest
	{

		// case '+'
		NumberAnalyzer analyzer = new NumberAnalyzer("+1");
		analyzer.analysisNegative();
		assert(analyzer.neg == 1);

		// case '-'
		analyzer = new NumberAnalyzer("-1");
		analyzer.analysisNegative();
		assert(analyzer.neg == -1);

		// default
		analyzer = new NumberAnalyzer("1");
		analyzer.analysisNegative();
		assert(analyzer.neg == 1);
	}


	/**
	 * 進数解析.
	 */
	private
	void analysisAdic()
	{
		if ((this.pos + 1) >= this.numStr.length)
		{	// 残り 1文字 => 進数を表すものなしなので10進数と判断する
			this.radix = DEC;
			return;
		}

		analysisAdicHead();					// 先頭文字を確認し, 進数を判定
		analysisAdicTail();					// 末尾文字を確認し, 進数を判定
	}
	unittest
	{
		NumberAnalyzer analyzer = new NumberAnalyzer("7");
		analyzer.radix = HEX;
		analyzer.analysisAdic();
		assert(analyzer.radix == DEC);

		analyzer = new NumberAnalyzer("07");
		analyzer.radix = HEX;
		analyzer.analysisAdic();
		assert(analyzer.radix == OCT);
	}


	/**
	 * 先頭2文字より, 進数を判定します.
	 * 進数判定後, 適宜次の読み取り位置を進めます.
	 */
	private
	void analysisAdicHead()
	{	char c = this.numStr[this.pos];
		if (c != '0')
		{	// 先頭 0以外は10進数
			this.radix = DEC;
			return;
		}

		this.pos++;
		c = this.numStr[this.pos];
		switch (c)
		{
			case 'x': case 'X':	// 16進数
				this.radix = HEX;
				this.pos++;
				break;
			case 'b': case 'B':	//  2進数
				this.radix = BIN;
				this.pos++;
				break;
			default:			//  8進数
				this.radix = OCT;
		}
	}
	unittest
	{
		// DEC
		NumberAnalyzer analyzer = new NumberAnalyzer("111");
		analyzer.radix = HEX;
		analyzer.analysisAdicHead();
		assert(analyzer.radix == DEC);

		// HEX
		analyzer = new NumberAnalyzer("0x7");
		analyzer.analysisAdicHead();
		assert(analyzer.radix == HEX);

		// HEX
		analyzer = new NumberAnalyzer("0X7");
		analyzer.analysisAdicHead();
		assert(analyzer.radix == HEX);

		// BIN
		analyzer = new NumberAnalyzer("0b7");
		analyzer.analysisAdicHead();
		assert(analyzer.radix == BIN);

		// BIN
		analyzer = new NumberAnalyzer("0B7");
		analyzer.analysisAdicHead();
		assert(analyzer.radix == BIN);

		// OCT
		analyzer = new NumberAnalyzer("07");
		analyzer.analysisAdicHead();
		assert(analyzer.radix == OCT);

	}


	/**
	 * 末尾の文字より進数を判定します.
	 */
	private
	void analysisAdicTail()
	{	// 最後の文字が h, Hか確認
		char c = this.numStr[this.numStr.length - 1];
		if ((c == 'h') || (c == 'H'))
		{
			if (this.radix == BIN)
			{	// 0B000001 H といった場合, 2進数とみなしていた
				// Bを数値として処理するため pos を戻す.
				// 8進数の場合は, 特に戻す必要なし.
				this.pos--;
			}
			this.radix = HEX;
		}
	}
	unittest
	{
		NumberAnalyzer analyzer = new NumberAnalyzer("100");
		analyzer.analysisAdicTail();
		assert(analyzer.radix == DEC);

		analyzer = new NumberAnalyzer("100h");
		analyzer.analysisAdicTail();
		assert(analyzer.radix == HEX);

		analyzer = new NumberAnalyzer("100H");
		analyzer.pos   = 2;
		analyzer.radix = BIN;
		analyzer.analysisAdicTail();
		assert(analyzer.pos   == 1);
		assert(analyzer.radix == HEX);
	}


	/**
	 * 数値を解析し, long 型で返します.
	 */
	private
	long analysisNumber()
	{
		long value = 0;
		for (; this.pos < this.numStr.length; this.pos++)
		{
			char c = this.numStr[this.pos];
			if ((c == '_') || (c == ','))
			{	// _, は読み飛ばす
				continue;
			}
			if (indexOf(ADDINFO_STRINGS, c) >= 0)
			{	// 末尾付加文字を見つけたら終了
				break;
			}
			if (c == '.')
			{	// ドットを見つけても終了
				break;
			}
			if (c in NUMBER_CHARS)
			{
				int tmpVal = NUMBER_CHARS[c];
				if (this.radix <= tmpVal)
				{
					throw new NumberFormatException("invalid character '" ~ c ~ "'");
				}
				value *= this.radix;
				value += tmpVal;
			}
			else
			{
				throw new NumberFormatException("invalid character '" ~ c ~ "'");
			}
		}
		return value;
	}
	unittest
	{
		NumberAnalyzer analyzer = new NumberAnalyzer("1,000");
		long           value    = analyzer.analysisNumber();
		assert(value == 1000);

		analyzer = new NumberAnalyzer("1000UL");
		value    = analyzer.analysisNumber();
		assert(value == 1000);

		analyzer = new NumberAnalyzer("1000.12");
		value    = analyzer.analysisNumber();
		assert(value == 1000);

		try {
			analyzer       = new NumberAnalyzer("0b2");
			analyzer.radix = BIN;
			analyzer.analysisNumber();
		} catch (NumberFormatException e) {
			// OK
		}

		try {
			analyzer = new NumberAnalyzer("0xX");
			analyzer.analysisNumber();
		} catch (NumberFormatException e) {
			// OK
		}
	}

}



