K345 number methods source

source code
/**
* converts a numeric string to number
* can handle comma|dot|space formatted strings like
* "1,000,000.00" , "1 000 000.00", "1.000.000,00"
*
* @param {String} inStr
*     (formatted) string represenation of a number
* @param {Number} [radix=10]
*     number system of result. defaults is 10 (decimal numeral system)
* @returns {Number|NaN)
*     numeric value or NaN for illegal input
*/
K345.numString2Number = function (inStr, radix) {
	var dot, com, len;

	if (typeof inStr === 'string' && (/^\-?[0-9\s,.-]+$/i).test(inStr)) {
		len = inStr.split(/[,.]/).length;
		dot = inStr.indexOf('.');
		com = inStr.indexOf(',');

		/* remove all whitespace; replace all »,« and ».« with spaces */
		inStr = inStr.replace(/\s/g, '')
			.replace(/[,.]/g, ' ');

		if ((dot > -1 && com > -1) || len === 2) {
			/* de facto: replace last space with "." */
			inStr = inStr.replace(/^(.+) ([^\s]+)$/, '$1.$2');
		}

		if (typeof radix !== 'number' || radix < 2 || radix > 36) {
			radix = 10;
		}
		return parseFloat(
			inStr.replace(/\s/g, ''), /* remove all remaining spaces */
			Math.floor(radix)
		);
	}
	return NaN;
};

/*! docs: http://javascript.knrs.de/k345/numeric/#numinput */
/**
* ###  K345.numericInput  ###
*/
(function () {
	var sysDefs = { /* (en|dis)abled numeral systems defaults */
		dec: {enabled: true, radix: 10},
		hex: {enabled: true, radix: 16},
		oct: {enabled: true, radix: 8},
		bin: {enabled: true, radix: 2},
		sci: {enabled: true},
		exp: {enabled: false},
		rom: {enabled: false},
		boo: {enabled: false},
		num: {enabled: false}
	},
	sysIDs = {
		'0x': 'hex', '0d': 'dec', '0b': 'bin', '0o': 'oct', '#': 'hex'
	},
	rChars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

	/* get string with valid chars for numeral system */
	function getChars(radix) {
		return rChars.substring(0, radix);
	}

	/* validate radix for parseInt and parseFloat */
	function isValidRadix(radix) {
		return (typeof radix === 'number' && radix >= 2 && radix <= 36);
	}

	/* set flags in config object for enabled/disabled numeral systems and add default
	* flags for omitted systems */
	function setSystems(so, defs) {
		var enable_all = false;

		if (!K345.isObject(so)) {
			so = {};
		}
		else if ('all' in so) {
			if (so.all === true) {
				enable_all = true;
			}
			delete so.all;
		}

		Object.forEach(defs, function (item) {
			if (enable_all) {
				so[item] = true;
			}
			else {
				if (item in so) {
					/* convert non boolean values */
					so[item] = Boolean(so[item]);
				}
				else {
					/* add default value if property is missing */
					so[item] = defs[item].enabled;
				}
			}
		});
		return so;
	}

	/* convert strings matching decimal, hexadeimal, octal or binary notation to decimal */
	function convertBinHexOctDec(val, sys) {
		var sInfo, sChar, sName, reg, radix, mch,
			rv = NaN;

		sInfo = val.match(/^(\-?)(0[bdox]|#)/i);
		/* strings prefixed by "0x", "0d", "0o", "0b" or "#" */
		if (sInfo && sInfo.length > 2) {
			sChar = sInfo[2].toLowerCase();
			sName = sysIDs[sChar];
			if (sChar && sName && (sName in sys) && sys[sName] && (sName in sysDefs)) {
				radix = sysDefs[sName].radix;
				if (isValidRadix(radix)) {
					reg = new RegExp(
						'^' + sInfo[0] + '([' + getChars(radix) + ']+)$',
						'i'
					);
					mch = val.match(reg);
					if (mch && mch.length > 1) {
						rv = parseInt(sInfo[1] + mch[1], radix);
					}
				}
			}
		}
		/* decimal notation string containig only 0-9, comma and dot */
		else if (sys.dec && (/^\-?[0-9,.]+$/).test(val)) {
			if (typeof K345.numString2Number === 'function') {
				rv = K345.numString2Number(val, 10);
			}
			else {
				rv = parseFloat(val, 10);
			}
		}
		return rv;
	}

	/* convert roman number to decimal using external function */
	function convertRoman(val) {
		var mul = 1,
			rv = NaN;

		if (typeof K345.rom2dec === 'function') {
			if (val.charAt(0) === '-') {
				val = val.substring(1);
				mul = -1;
			}
			rv = K345.rom2dec(val);
			if (!isNaN(rv)) {
				rv = rv * mul;
			}
		}
		return rv;
	}

	/* convert "power of" or "root of" to decimal */
	function convertPowRoot(val) {
		var mch, op,
			rv = NaN;

		mch = val.match(
			/^(\-?[0-9]+(?:\.[0-9]+)?)?(\^|pow|\*\*||r(?:oo)?t)(\-?[0-9]+(?:\.[0-9]+)?)$/i
		);
		if (mch && mch.length > 2 && !isNaN(mch[3])) {
			op = mch[2];
			if ('√|rt|root'.indexOf(op) > -1) {
				if (typeof mch[1] === 'undefined') {
					mch[1] = 2;
				}
				if (!isNaN(mch[1]) && mch[1] > 0) {
					rv = Math.pow(mch[3], 1 / mch[1]);
				}
			}
			else if (!isNaN(mch[1])) {
				rv = Math.pow(mch[1], mch[3]);
			}

		}
		return (rv === Infinity) ? NaN : rv;
	}

	/* convert custom numeral systems to decimal. base may be 2 to 36 */
	function convertNumSystems(val) {
		var mch, radix, reg,
			rv = NaN;

		mch = val.match(/\[([0-9]+)\]/);
		if (mch && mch.length > 0) {
			radix = Number(mch[1]);
			if (isValidRadix(radix)) {
				reg = new RegExp(
					'^(\\-?[' + getChars(radix) + ']+)',
					'i'
				);
				mch = val.match(reg);
				if (mch && mch.length > 1) {
					rv = parseInt(mch[1], radix);
				}
			}
		}
		return rv;
	}

	/**
	* Convert several types of numeric strings to decimal
	*
	* doc: http://javascript.knrs.de/k345/numeric/#numinput
	*
	* supports by default:
	* - decimal ("13", "0d13");
	* - hexadecimal ("0xA0", "#FF");
	* - octal ("0o27");
	* - binary ("0b101010");
	* - e notation ("2E3", "2000E-5");
	*
	* supports if enabled:
	*  - boolean ("true", true);
	*  - other numeral systems "[value]base" ("[30]7", "[-41]12")
	*
	* supports using external function:
	*  - roman ("MMXIV" "V*MDCCLXVIII") with K345.rom2dec()
	*
	* @param {String} val
	*     numeric string to convert
	* @param {Object} [sys={dec: true, hex: true, oct: true, bin: true, sci: true,
	*                           exp: false, rom: false, boo: false, num: false}]
	*     enabled numberal systems
	* @returns {Number|NaN}
	*     decimal value of numeric string or NaN
	*/
	K345.numericInput = function (val, sys) {
		var rv = NaN,
			ty = typeof val;

		/* enabled/disabled numeral systems*/
		sys = setSystems(sys, sysDefs);

		/* handle non string values for val, */
		if (ty !== 'string') {
			if (sys.dec && ty === 'number') {
				return val;
			}
			else if (sys.boo && ty === 'boolean') {
				return Number(val);
			}
			return rv;
		}

		val = val.replace(/\s/g, '').replace('+', '');

		/* binary, hexadecimal, decimal, octal */
		if (
			(sys.bin || sys.oct || sys.dec || sys.hex) &&
			(/^\-?(?:0[bdox]|#|[0-9,.]+$)/i).test(val)
		) {
			rv = convertBinHexOctDec(val, sys);
		}
		/* scientific E notation (calculator notation) */
		else if (sys.sci && (/^\-?[0-9]+(?:\.[0-9]+)?e\-?[0-9]+$/i).test(val)) {
			rv = Number(val);
		}
		/* exponential and root */
		else if (sys.exp &&
			(/^(?:[0-9.-]+)?(?:\^|pow|\*\*|√|r(?:oo)?t)[0-9.-]+$/i).test(val)
		) {
			rv = convertPowRoot(val);
		}
		/* roman */
		else if (sys.rom && (/^\-?[IJVXLCDM•\*\u2180-\u2183]+$/i).test(val)) {
			rv = convertRoman(val);
		}
		/* convert strings with words "true" or "false" to 0/1 */
		else if (sys.boo && (val === 'true' || val === 'false')) {
			rv = Number(val === 'true');
		}
		/* other numeral systems */
		else if (sys.num && (/^\-?[0-9a-z]+\[[0-9]+\]$/i).test(val)) {
			rv = convertNumSystems(val);
		}
		return rv;
	};
})();

/**
* convert number to/from roman
* supports 3 and 4 char syntax (e.g IX or VIIII => 9, XC or LXXXX => 90 )

NEEDS REWRITE

*
* Römische Zahlen verarbeiten.
* Unterstützt 3- und 4-Zeichen-Syntax (z.B. IX oder VIIII => 9; XC oder LXXXX => 90 usw.)
*/
K345.parseRoman = (function () {
	var rData, synCheck, validChars, decMax, rGroups,
		rTokens = [],
		rValues = [];

	/* Zeichen, die auftreten können und deren Werte. Zwischenwerte werden automatisch
	* generiert. Bei allen Werten > 1000 werden intern Ersatzbuchstaben verwendet, um die
	* Verarbeitung zu vereinfachen. Bei der Ein/Ausgabe werden Unicode-Spezial-Zeichen
	* entsprechend zu diesen Buchstaben gewandelt.
	*
	* Eigenschaften:
	*     tk:  Token; für Werte > 1000 (M) beliebiger unbelegter Buchstabe
	*     val: Zahlenwert , der zu Token gehört
	*     ci:  Zeichen werden zusätzlich als Eingabe bei rom2dec erkannt
	*     cio:  Zeichen wird als Ausgabe bei dec2rom benutzt und bei rom2dec erkannt
	*
	* Sämtliche erforderliche regulären Ausdrücke werde aus diesen Daten automatisch
	* generiert.
	*/
	rData = [
		{tk: 'S', val: 1000000000, cio: 'M•M•M'},
		{tk: 'R', val: 500000000, cio: 'D•M•M'},
		{tk: 'Q', val: 100000000, cio: 'C•M•M'},
		{tk: 'P', val: 50000000, cio: 'L•M•M'},
		{tk: 'N', val: 10000000, cio: 'X•M•M'},
		{tk: 'K', val: 5000000, cio: 'V•M•M'},
		{tk: 'H', val: 1000000, cio: 'M•M'},
		{tk: 'G', val: 500000, cio: 'D•M'},
		{tk: 'F', val: 100000, cio: 'C•M'},
		{tk: 'E', val: 50000, cio: 'L•M'},
		{tk: 'B', val: 10000, cio: 'X•M', ci: '\u2182'},
		{tk: 'A', val: 5000, cio: 'V•M', ci: '\u2181'},
		/* ab hier übliche römisch Zahlzeichen in "tk" */
		{tk: 'M', val: 1000, ci: '\u2180'},
		//{tk: 'D', val: 500, ci: '\u2183'},
		{tk: 'D', val: 500},
		{tk: 'C', val: 100},
		{tk: 'L', val: 50},
		{tk: 'X', val: 10},
		{tk: 'V', val: 5},
		{tk: 'I', val: 1, ci: 'J'}
	];

	/* RegEx für erlaubte Zeichen erzeugen */
	validChars = (function () {
		var ch = '';

		rData.forEach(function (o) {
			if (o.cio) {
				if (o.cio.indexOf('•') === -1 && ch.indexOf(o.cio) === -1) {
					ch += o.cio;
				}
			}
			if (o.ci && ch.indexOf(o.ci) === -1) {
				ch += o.ci;
			}
			if (!o.cio && ch.indexOf(o.tk) === -1) {
				ch += o.tk;
			}
		});
		return new RegExp('^[' + ch  + '•\\(\\)\\*\\s\\-]+$', 'i');
	})();

	/* Mögliche Zeicheneinheiten und deren Werte in den Arrays „rTokens“ und „rValues“
	* abspeichern. Jeder Wert in „rValues“ entspricht der jeweiligen Zeicheneinheit mit
	* gleichem Index im Array „rTokens“ */
	(function () {
		var l = rData.length,
			i;

		for (i = (l % 2); i < l; i += 2) {
			if (rData[i - 1]) {
				rTokens.push(
					rData[i - 1].tk,
					rData[i + 1].tk + rData[i - 1].tk
				);
				rValues.push(
					rData[i - 1].val,
					rData[i - 1].val - rData[i + 1].val
				);
			}
			rTokens.push(
				rData[i].tk,
				rData[i + 1].tk + rData[i].tk
			);
			rValues.push(
				rData[i].val,
				rData[i].val - rData[i + 1].val
			);
		}
		rTokens.push(rData[l - 1].tk);
		rValues.push(rData[l - 1].val);
	})();

	/* RegExp für Zeichengruppen-Erkennung erzeugen */
	rGroups = new RegExp('(' + rTokens.join('|') + ')', 'g');

	/* maximal erlaubter Eingangswert für dec2rom */
	decMax = (5 * rValues[0]) - 1;

	/* RegEx für Test auf gültige Syntax erzeugen */
	synCheck = (function () {
		var i = Math.floor(rTokens.length / 4), /* Je 4 Einheiten bilden eine Gruppe */
			r = '',
			pat = '(§1?§0{0,4}|§0[§2§1])',
			base;

		while (i--) {
			base = 4 * (i + 1);
			r = pat.replace(/§0/g, rTokens[base])
				.replace(/§1/g, rTokens[base - 2])
				.replace(/§2/g, rTokens[base - 4]) + r;
		}
		return new RegExp('^' + rTokens[0] + '{0,4}' + r + '$', 'i');
	})();

	/* Die Zeichenkette zu Großbuchstaben wandeln, Whitespace entfernen, Unicode-Zeichen
	* und Multi-Zeichen-Kombinationen für interne Weiterverarbeitung durch einen einzelnen
	* Buchstaben ersetzen. **/
	function replaceInChars(s) {
		s = s.replace(/[\s\(\)]/g, '').replace(/\*/g, '•').toUpperCase();
		rData.forEach(function (o) {
			if (typeof o.cio === 'string') {
				s = s.replace(new RegExp(o.cio, 'g'), o.tk);
			}
			if (typeof o.ci === 'string') {
				o.ci = o.ci.split('');
			}
			if (Array.isArray(o.ci)) {
				o.ci.forEach(function (ch) {
					s = s.replace(new RegExp(ch, 'g'), o.tk);
				});
			}
		});
		return s;
	}

	/* Interne Zeichen zu gültiger Ausgabe umwandeln */
	function replaceOutChars(s, fmt) {
		s = s.join((fmt) ? ' ' : '');
		rData.forEach(function (o) {
			if (o.cio) {
				s = s.replace(new RegExp(o.tk, 'g'), o.cio);
			}
		});
		if (fmt) {
			s = s.replace(/(•[a-z])([a-z]•)/gi, '$1 $2')
				.replace(/(•[a-z])([a-z])/gi, '$1 $2');
		}
		return s;
	}

	return {
		/**
		* Parsen einer römischen zahl zu einer Dezimalzahl
		*
		* @param {String} romStr
		*     Römische Zahl. Erlaubt sind Kombinationen aus M D C L X V I und
		*     \u2180 (1000), \u2181 (5000) und \u2182 (10000) sowie Leerzeichen zur
		*     lesbareren Gruppierung
		* @returns {Number|NaN}
		*     dezimale Repräsentation der Zeichenkette oder NaN bei ungültigen Werten
		*/
		rom2dec: function (romStr) {
			var fnd;

			/* Testen, ob nur valid Zeichen verwendet werden */
			if (typeof romStr === 'string' && validChars.test(romStr)) {
				/* Zur Vereinfachung der Verarbeitung diverse Zeichen ersetzen */
				romStr = replaceInChars(romStr);
				/* Test auf korrekt Syntax bezüglich Zeichenposition */
				if (synCheck.test(romStr)) {
					/* Array fnd enthält die gültigen Einheiten */
					fnd = romStr.match(rGroups);
					/* Wertzuweisung dieser Einheiten ermitteln und zu Startwert 0 addieren*/
					return fnd.reduce(function (p, c) {
						p += rValues[rTokens.indexOf(c)];
						return p;
					}, 0);
				}
			}
			return NaN;
		},

		/**
		* Umwandlung einer Dezimalzahl zu einer römischen Zahl
		*
		* @param {Number} num
		*     Eine Zahl zwischen 1 und dem errechneten Wert 5 * höchste Zahl - 1
		* @param {Boolean} [grp=false]
		*     Die erzeugte Zeichenkette mit Leerzeichen gruppieren
		* @returns {String}
		*     Repräsentation der Zahl in römischer Schreibweise
		*/
		dec2rom: function (num, grp) {
			var s = [];

			num = Math.floor(num);
			if (num > 0 && num <= decMax) {
				/* Durchläuft solange Array rValues, bis entweder Callback nicht mehr
				* „true“ zurückgibt (Restzahl ist <= 0) oder alle Array-Einträge
				* durchlaufen wurden */
				rValues.every(function (val, i) {
					while (num >= val) {
						s.push(rTokens[i]);
						num -= val;
					}
					return (num > 0);
				});
				return replaceOutChars(s, !!grp);
			}
			return '';
		}
	};
})();
K345.rom2dec = K345.parseRoman.rom2dec;
K345.dec2rom = K345.parseRoman.dec2rom;