/**
 * Number Module.
 *
 * License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>
 * Authors: Nomura Kei
 * Copyright: Copyright(c) 2013 Nomura Kei
 */
module jp.ehobby.sal.util.number;

private
{
	import std.string;
	import std.conv;
}


/** 数値変換定義. */
private int[char] NUMBER_CHARS;
private	string[]  NUMBER_STRINGS = [
	"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
	"0123456789abcdefghijklmnopqrstuvwxyz"
];


/**
 * Number Module 初期化.
 */
static this()
{
	for (int arrayIndex = 0; arrayIndex < NUMBER_STRINGS.length; arrayIndex++) {
		for (int charIndex = 0; charIndex < NUMBER_STRINGS[arrayIndex].length; charIndex++) {
			NUMBER_CHARS[NUMBER_STRINGS[arrayIndex][charIndex]] = charIndex;
		}
	}
}


/**
 * 基数を表す.
 */
enum Radix : int
{
	BIN		=  2,	///  2進数の基数
	OCT		=  8,	///  8進数の基数
	DEC		= 10,	/// 10進数の基数
	HEX		= 16	/// 16進数の基数
};


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


/**
 * 指定された数値文字列を long 型に変換して返します.
 *
 * Params:
 *	strNum	= 数値文字列
 *
 * Return:
 *	数値 (long型)
 */
long longValue(string strNum)
{
	NumberAnalyzer analyzer = new NumberAnalyzer(strNum);
	return analyzer.longValue();
}


/**
 * 指定された数値文字列を double 型に変換して返します.
 *
 * Params:
 *	strNum	= 数値文字列
 *
 * Return:
 *	数値 (double型)
 */
double doubleValue(string strNum)
{
	string striped = strip(strNum);
	return parse!(double)(striped);
}


/**
 * 数値変換エラー発生時に throw する Exception クラス.
 */
class NumberFormatException : Exception
{
	/**
	 * コンストラクタ.
	 *
	 * Params:
	 *	msg		= メッセージ
	 *	file	= ファイル
	 *	line	= 行番号
	 *	next	= 次のエラー
	 */
	this(string msg, string file = __FILE__, int line = __LINE__, Throwable next = null)
	{
		super(msg, file, line, next);
	}
}


/**
 * 整数数値解析器.
 */
private
class NumberAnalyzer
{
	private
	{
		static string SPACE_STRINGS		= " \t_";		/// 空白文字列.
		static string ADDINFO_STRINGS	= "ulULhH";		/// 数値末尾に付く付加情報文字.
		string numStr;									/// 解析する文字列.
		int    pos;										/// 解析位置.
		int    neg;										/// 符号.
		int    radix;									/// 進数
	}


	/**
	 * 指定された数値文字列を解析する NumberAnalyzer を構築します.
	 *
	 * Params:
	 *	number	= 解析する数値文字列
	 */
	this(string number)
	{
		this.numStr	= strip(number);
		this.pos    = 0;
		this.neg    = 1;
		this.radix  = Radix.DEC;
		if (this.numStr.length == 0)
		{
			throw new NumberFormatException("invalid format : " ~ number);
		}
	}


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

		// 2. 進数解析
		analysisAdic();

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

		// 4. 符号付与
		value  *= this.neg;
		return value;
	}


	/**
	 * 符号解析.
	 * 負の符号の場合, 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;
		}
	}


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


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

		this.pos++;
		c = this.numStr[this.pos];
		switch (c)
		{
			case 'x': case 'X':	this.radix = Radix.HEX;	this.pos++;	break;	// 16進数
			case 'b': case 'B':	this.radix = Radix.BIN;	this.pos++;	break;	//  2進数
			default:			this.radix = Radix.OCT;						//  8進数
		}
	}


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

		// 前後の空白は無視
		NumberAnalyzer analyzer = new NumberAnalyzer("	12345 \t");
		assert(analyzer.longValue() == 12345);

		// カンマ, アンダーバーは無視
		analyzer = new NumberAnalyzer("1,000,000");
		assert(analyzer.longValue() == 1000000);
		analyzer = new NumberAnalyzer("1_000_000");
		assert(analyzer.longValue() == 1000000);

		// ul, l, UL, L は無視
		analyzer = new NumberAnalyzer("1000ul");
		assert(analyzer.longValue() == 1000);
		analyzer = new NumberAnalyzer("1000l");
		assert(analyzer.longValue() == 1000);
		analyzer = new NumberAnalyzer("1000UL");
		assert(analyzer.longValue() == 1000);
		analyzer = new NumberAnalyzer("1000L");
		assert(analyzer.longValue() == 1000);

		// . 以降は無視
		analyzer = new NumberAnalyzer("1000.12E10");
		assert(analyzer.longValue() == 1000);

		// 2進数で使用可能な文字は 0 or 1 のみ
		analyzer = new NumberAnalyzer("0b1");
		assert(analyzer.longValue() == 0b1);
		analyzer = new NumberAnalyzer("0b2");
		try { analyzer.longValue(); } catch (NumberFormatException e) {}

		// 8進数で使用可能な文字は 0-7 のみ
		analyzer = new NumberAnalyzer("07");
		assert(analyzer.longValue() == 07);
		analyzer = new NumberAnalyzer("08");
		try { analyzer.longValue(); } catch (NumberFormatException e) {}

		// 10進数で使用可能な文字は 0-9 のみ
		analyzer = new NumberAnalyzer("1234567890");
		assert(analyzer.longValue() == 1234567890);
		analyzer = new NumberAnalyzer("123A");
		try { analyzer.longValue(); } catch (NumberFormatException e) {}

		// 16進数で使用可能な文字は 0-9,A-F,a-f のみ
		analyzer = new NumberAnalyzer("0x01ABCDEF_abcdef");
		assert(analyzer.longValue() == 0x01ABCDEF_abcdef);
		analyzer = new NumberAnalyzer("0xG");
		try { analyzer.longValue(); } catch (NumberFormatException e) {}

		// 符号の確認 (+, -, 符号なし)
		analyzer = new NumberAnalyzer("+1");
		assert(analyzer.longValue() == 1);
		analyzer = new NumberAnalyzer("-1");
		assert(analyzer.longValue() == -1);
		analyzer = new NumberAnalyzer("1");
		assert(analyzer.longValue() == 1);

		// 2進数
		analyzer = new NumberAnalyzer("0B_0001_0010");
		assert(analyzer.longValue() == 0B_0001_0010);
		analyzer = new NumberAnalyzer("0b_0101_0111");
		assert(analyzer.longValue() == 0b_0101_0111);

		// 8進数
		analyzer = new NumberAnalyzer("0644");
		assert(analyzer.longValue() == octal!(644));

		// 10進数
		analyzer = new NumberAnalyzer("100");
		assert(analyzer.longValue() == 100);

		// 16進数
		analyzer = new NumberAnalyzer("0XABC");
		assert(analyzer.longValue() == 0XABC);
		analyzer = new NumberAnalyzer("0xdef");
		assert(analyzer.longValue() == 0xdef);
		analyzer = new NumberAnalyzer("100h");
		assert(analyzer.longValue() == 0x100);
		analyzer = new NumberAnalyzer("123H");
		assert(analyzer.longValue() == 0x123);
	}
}

