
secondsPerMinute      =   60;
secondsPerHour        =   60 * secondsPerMinute;
secondsPerDay         =   24 * secondsPerHour;
millisecondsPerMinute = 1000 * secondsPerMinute;
millisecondsPerHour   = 1000 * secondsPerHour;
millisecondsPerDay    = 1000 * secondsPerDay;

//==============================================================================
// Id


nextId = 1; // not `0`: means "no id"

function nextIdString()  { return '_' + nextId++; }
function correctId   (o) { return o.id || (o.id = nextId++); }


// Id
//==============================================================================
// Type


voidType    = 0; // noType anyType typeLess
booleanType = 1;
integerType = 2;
floatType   = 3;
stringType  = 4;
dateType    = 5;
arrayType   = 6;
recordType  = 7;


// Type
//==============================================================================
// Keyboard


backspaceKey         = 8;
tabKey               = 9;
keypad5Key           = 12; // Num Lock off
enterKey             = 13;
shiftKey             = 16;
ctrlKey              = 17;
altKey               = 18;
pauseKey             = 19;
capsLockKey          = 20;
escapeKey            = 27;
spaceKey             = 32; // everything on the keyboard is a key: the spacebar is the space key
pageUpKey            = 33;
pageDownKey          = 34;
endKey               = 35;
homeKey              = 36;
arrowLeftKey         = 37;
arrowUpKey           = 38;
arrowRightKey        = 39;
arrowDownKey         = 40;
printScreenKey       = 44;
insertKey            = 45;
deleteKey            = 46;
closeParenthesisKey  =
digit0Key            = 48;
exclamationMarkKey   =
digit1Key            = 49;
atKey                =
digit2Key            = 50;
octothorpeKey        =
digit3Key            = 51;
dollarSignKey        =
digit4Key            = 52;
percentKey           =
digit5Key            = 53;
caretKey             =
digit6Key            = 54;
ampersandKey         =
digit7Key            = 55;
timesKey             =
starKey              =
digit8Key            = 56;
openParenthesisKey   =
digit9Key            = 57;
aKey                 = 65; // 'A'.charCodeAt(0)
bKey                 = 66; // ...
cKey                 = 67;
dKey                 = 68;
eKey                 = 69;
fKey                 = 70;
gKey                 = 71;
hKey                 = 72;
iKey                 = 73;
jKey                 = 74;
kKey                 = 75;
lKey                 = 76;
mKey                 = 77;
nKey                 = 78;
oKey                 = 79;
pKey                 = 80;
qKey                 = 81;
rKey                 = 82;
sKey                 = 83;
tKey                 = 84;
uKey                 = 85;
vKey                 = 86;
wKey                 = 87;
xKey                 = 88;
yKey                 = 89; // ...
zKey                 = 90; // 'Z'.charCodeAt(0)
keypad0Key           = 96;
keypad1Key           = 97;
keypad2Key           = 98;
keypad3Key           = 99;
keypad4Key           = 100;
keypad5Key           = 101; // Num Lock on
keypad6Key           = 102;
keypad7Key           = 103;
keypad8Key           = 104;
keypad9Key           = 105;
keypadTimesKey       = 106;
keypadPlusKey        = 107;
keypadMinusKey       = 109;
keypadDotKey         = 110;
keypadDividedByKey   = 111;
f1Key                = 112;
f2Key                = 113;
f3Key                = 114;
f4Key                = 115;
f5Key                = 116;
f6Key                = 117;
f7Key                = 118;
f8Key                = 119;
f9Key                = 120;
f10Key               = 121;
f11Key               = 122;
f12Key               = 123;
numLockKey           = 144; 
scrollLockKey        = 145;
nonBreakingSpace     = '\u00A0'; // 160; unicode for `&nbsp`: `document.createTextNode(nonBreakingSpace)`
colonKey             =
semicolonKey         = 186;
plusKey              =
equalsKey            = 187;
lessThanKey          =
leftAngleBracketKey  =
commaKey             = 188;
underscoreKey        =
minusKey             =
hyphenKey            =
dashKey              = 189;
greaterThanKey       =
rightAngleBracketKey =
dotKey               =
periodKey            = 190;
questionMarkKey      =
dividedByKey         =
slashKey             = 191;
tildeKey             =
backQuoteKey         = 192;
openBraceKey         =
openBracketKey       = 219;
verticalBarKey       =
backslashKey         = 220;
closeBraceKey        =
closeBracketKey      = 221;
singleQuoteKey       =
quoteKey             = 222;

shiftModifier        = 256;
ctrlModifier         = 512;
altModifier          = 1024;

keyString            = [];

// size
createKeyString();

function createKeyString()
{
	keyString[backspaceKey]       = 'Backspace';
	keyString[tabKey]             = 'Tab';
	keyString[keypad5Key]         = 'Keypad 5'; // Num Lock off
	keyString[enterKey]           = 'Enter';
	keyString[shiftKey]           = 'Shift';
	keyString[ctrlKey]            = 'Ctrl';
	keyString[altKey]             = 'Alt';
	keyString[pauseKey]           = 'Pause';
	keyString[capsLockKey]        = 'Caps Lock';
	keyString[escapeKey]          = 'Escape';
	keyString[spaceKey]           = 'Spacebar';
	keyString[pageUpKey]          = 'Page Up';
	keyString[pageDownKey]        = 'Page Down';
	keyString[endKey]             = 'End';
	keyString[homeKey]            = 'Home';
	keyString[arrowLeftKey]       = 'Left Arrow';
	keyString[arrowUpKey]         = 'Up Arrow';
	keyString[arrowRightKey]      = 'Right Arrow';
	keyString[arrowDownKey]       = 'Down Arrow';
	keyString[printScreenKey]     = 'Print Screen';
	keyString[insertKey]          = 'Insert';
	keyString[deleteKey]          = 'Delete';
	keyString[digit0Key]          = '0';
	keyString[digit1Key]          = '1';
	keyString[digit2Key]          = '2';
	keyString[digit3Key]          = '3';
	keyString[digit4Key]          = '4';
	keyString[digit5Key]          = '5';
	keyString[digit6Key]          = '6';
	keyString[digit7Key]          = '7';
	keyString[digit8Key]          = '8';
	keyString[digit9Key]          = '9';
	keyString[aKey]               = 'A';
	keyString[bKey]               = 'B';
	keyString[cKey]               = 'C';
	keyString[dKey]               = 'D';
	keyString[eKey]               = 'E';
	keyString[fKey]               = 'F';
	keyString[gKey]               = 'G';
	keyString[hKey]               = 'H';
	keyString[iKey]               = 'I';
	keyString[jKey]               = 'J';
	keyString[kKey]               = 'K';
	keyString[lKey]               = 'L';
	keyString[mKey]               = 'M';
	keyString[nKey]               = 'N';
	keyString[oKey]               = 'O';
	keyString[pKey]               = 'P';
	keyString[qKey]               = 'Q';
	keyString[rKey]               = 'R';
	keyString[sKey]               = 'S';
	keyString[tKey]               = 'T';
	keyString[uKey]               = 'U';
	keyString[vKey]               = 'V';
	keyString[wKey]               = 'W';
	keyString[xKey]               = 'X';
	keyString[yKey]               = 'Y';
	keyString[zKey]               = 'Z';
	keyString[keypad0Key]         = 'Keypad 0';
	keyString[keypad1Key]         = 'Keypad 1';
	keyString[keypad2Key]         = 'Keypad 2';
	keyString[keypad3Key]         = 'Keypad 3';
	keyString[keypad4Key]         = 'Keypad 4';
	keyString[keypad5Key]         = 'Keypad 5'; // Num Lock on
	keyString[keypad6Key]         = 'Keypad 6';
	keyString[keypad7Key]         = 'Keypad 7';
	keyString[keypad8Key]         = 'Keypad 8';
	keyString[keypad9Key]         = 'Keypad 9';
	keyString[keypadTimesKey]     = 'Keypad *';
	keyString[keypadPlusKey]      = 'Keypad +';
	keyString[keypadMinusKey]     = 'Keypad -';
	keyString[keypadDotKey]       = 'Keypad .';
	keyString[keypadDividedByKey] = 'Keypad /';
	keyString[f1Key]              = 'F1';
	keyString[f2Key]              = 'F2';
	keyString[f3Key]              = 'F3';
	keyString[f4Key]              = 'F4';
	keyString[f5Key]              = 'F5';
	keyString[f6Key]              = 'F6';
	keyString[f7Key]              = 'F7';
	keyString[f8Key]              = 'F8';
	keyString[f9Key]              = 'F9';
	keyString[f10Key]             = 'F10';
	keyString[f11Key]             = 'F11';
	keyString[f12Key]             = 'F12';
	keyString[numLockKey]         = 'Num Lock'; 
	keyString[scrollLockKey]      = 'Scroll Lock';
	keyString[nonBreakingSpace]   = 'Non-Breaking Space';
	keyString[semicolonKey]       = ';';
	keyString[equalsKey]          = '=';
	keyString[commaKey]           = ',';
	keyString[dashKey]            = '-';
	keyString[periodKey]          = '.';
	keyString[slashKey]           = '/';
	keyString[backQuoteKey]       = '`';
	keyString[openBracketKey]     = '[';
	keyString[backslashKey]       = '\\';
	keyString[closeBracketKey]    = ']';
	keyString[quoteKey]           = "'";

	var i = keyString.length;

	while (i--)
	{
		if (keyString[i])
			keyString[i | shiftModifier] = 'Shift+' + keyString[i];
	}
}

function isChangeKey(iValue)
{
	return backspaceKey === iValue
	 ||    escapeKey    === iValue // clears input
	 ||    spaceKey     === iValue
	 ||    isBetween(iValue, insertKey,    keypadDividedByKey) // `insertKey`: Shift+Insert is paste
	 ||    isBetween(iValue, semicolonKey, quoteKey);
}

function isCharKey(iValue)
{
	return isBetween(iValue, digit0Key,    keypadDividedByKey)
	 ||    isBetween(iValue, semicolonKey, quoteKey);
}

function notBinaryKey (iValue) { return isCharKey(iValue) &&  correctBinaryKey(iValue) < 0; }
function notDecimalKey(iValue) { return isCharKey(iValue) &&  correctDigitKey (iValue) < 0; }
function notHexKey    (iValue) { return isCharKey(iValue) && !correctHexKey   (iValue); }

function correctBinaryKey(iValue)
{
	return digit0Key === iValue ? 0
	 :     digit1Key === iValue ? 1
	 :     -1;
}

function correctDigitKey(iValue)
{
	return isBetween(iValue, digit0Key,  digit9Key)  ? iValue - digit0Key
	 :     isBetween(iValue, keypad0Key, keypad9Key) ? iValue - keypad0Key
	 :     -1;
}

function correctHexKey(iValue)
{
	return '' +
	(
		   isBetween(iValue, digit0Key,  digit9Key)  ? iValue - digit0Key
		 : isBetween(iValue, keypad0Key, keypad9Key) ? iValue - keypad0Key
		 : isBetween(iValue, aKey,       fKey)       ? keyString[iValue]
		 : ''
	);
}


// Keyboard
//==============================================================================
// Number


// yes, `Number.prototype.abs = function(n) { return Math.abs(this); };`  is preferrable, but slower
// when you create wikiscript, use `iTooTall.abs()` which converts to javascript `Math.abs(iTooTall)` or `abs(iTooTall); function abs(n) { return Match.abs(n); }`
function abs             (n)                 { return n < 0 ? -n : n; } // Math.abs(n) slower
function max             (m, n)              { return m < n ? n : m }
function min             (m, n)              { return m < n ? m : n; }  // Math.min(m, n) slower
function sign            (n)                 { return n < 0 ? -1 : 1; }
//function sign0         (n)                 { return n < 0 ? -1 : 0 < n ? 1 : 0; }

floor   = Math.floor;
ceiling = Math.ceil;
random  = Math.random;
round   = Math.round;
power   = Math.pow;

//function round         (n)                 { return Math.round(n); }
//function power         (n, nExponent)      { return Math.pow(n, nExponent); }

function roundToString2(n)                 { return decimalPlaces(round(n * 100) / 100, 2); }
function roundTo2      (n)                 { return round(n * 100) / 100; }
function roundToI      (n, iDecimalPlaces) { var iMagnitude = power(10, iDecimalPlaces); return round(n * iMagnitude) / iMagnitude; }

roundTo_powersOf10 = [1, 10, 100, 1000, 10000];

function roundTo(n, iDecimalPlaces)
{
// debug {
	if (isNotBetween(iDecimalPlaces, 0, roundTo_powersOf10.length - 1)) errorIn('Increase roundTo_powersOf10 array size', 'roundTo');
// debug }

	var iMagnitude = roundTo_powersOf10[iDecimalPlaces];
	return round(n * iMagnitude) / iMagnitude;
}


function mod0            (i, iMod) { i = i % iMod; return i < 0 ? i + iMod : i; } // speed (only one %)
//function mod0          (i, iMod) { return (i % iMod + iMod) % iMod; }           // size; 2 === mod0(-1, 3), -1 === -1 % 3
function randomBoolean   ()        { return random() < 0.5; }
function randomInteger   (i, j)    { return floor((j - i + 1) * random() + i); }
function randomInteger0  (i)       { return floor((i + 1) * random()); } // randomInteger(0, i)
function randomInteger0To(i)       { return floor(i * random()); }       // upto but not including


function between         (n, nMin, nMax) { return n < nMin ? nMin : nMax < n ? nMax : n; }
function isBetween       (n, nMin, nMax) { return nMin <= n && n <= nMax; }
function isNotBetween    (n, nMin, nMax) { return n < nMin || nMax < n; }

// Number.prototype.between      = function(nMin, nMax) { var n = Number(this); return n < nMin ? nMin : nMax < n ? nMax : n; }; // `Number(this)`: want `'number' === typeof returnValue`
// Number.prototype.isBetween    = function(nMin, nMax) { return nMin <= this && this <= nMax; };
// Number.prototype.isNotBetween = function(nMin, nMax) { return this <  nMin || nMax <  this; };
// Number.prototype.isNotInteger = function()           { return round(this) !== this; };
Number.prototype.toPercent    = function()           { return round(100 * this); };

function number_percent        (i, iMin, iMax)         { return (i - iMin) / (iMax - iMin); }
function integer_percent       (i, iMin, iMax)         { return integer_range    (i, iMin, iMax, 100); }
// function integer_percentDiff(i, iDiff)              { return integer_rangeDiff(i, iDiff,      100); }
function integer_range         (i, iMin, iMax, iRange) { return iMin + round(i * (iMax - iMin) / iRange); }

//function integer_percent128      (i, iMin, iMax) { return      ((i + 16) * (iMax - iMin) >> 7) + iMin; } // 16: 128/8 or 128/(2*diff): adds 0.5
//function integer_percent128_floor(i, iMin, iMax) { return      (i * (iMax - iMin) >> 7)  + iMin; }

function integer_elementRange(x, e, iMin, iMax)
{
// debug {
	if (isNotBetween(x, 0, e.offsetWidth - 1)) error('Outside range');
// debug }

	return integer_range(x, e.offsetWidth - 1, iMin, iMax);
}

/*
function integer_rangeDiff(x, iDiff, iMaxX)
{
// debug {
	if (isNotBetween(x, 0, iMaxX)) error('Outside range');
// debug }

	return round(x * iDiff / iMaxX);
}
*/

function digits2      (i)          { return i < 10 ?                      '0' + i : i + ''; }
function digits3      (i)          { return i < 10 ? '00' + i : i < 100 ? '0' + i : i + ''; }
function digits       (i, iCount)  { return string_digits(i + '', iCount); }
function string_digits(si, iCount) { return string_repeatShort('0', iCount - si.length) + si; }

/*
Number.prototype.digits = function(iDigitCount) { return '0'.repeatShort(iDigitCount - (this + '').length) + this; };

function string_digits(si, iCount) // 30% faster than `string_repeat(`
{
	var iPadding = iCount - si.length;

	return iPadding < 1   ?        si // not `!iPadding`: may be < 0
	 :     1 === iPadding ?  '0' + si
	 :     2 === iPadding ? '00' + si
	 : '00' + string_repeat('0', iPadding - 2) + si;
}
*/

Number.prototype.digitsNegative = function(iDigitCount)
{
	return (this < 0 ? '-' : '')
	 + '0'.repeat(iDigitCount - (this + '').replace(/-|\./g, '').length)
	 + this.abs();
};

function decimalPlaces(n, iValue)
{
	var sn = n + '',
		i  = sn.indexOf('.');

	return i < 0
	 ? sn + '.' + string_repeatShort('0', iValue)
	 : sn       + string_repeatShort('0', iValue - sn.length + i + 1); // - (sn.length - i - 1)
}

Number.prototype.thousandsSeparator = function() // adds commas at every thousand's place
{
	var sThisWithCommas = '',
		sThis           = this + '',
		i               = sThis.length;

	while (0 < (i -= 3))
		sThisWithCommas = ',' + sThis.slice(i, i + 3) + sThisWithCommas;

	return sThis.slice(0, i + 3) + sThisWithCommas;
};


// base
//--------------------------------------

gasMonth3  = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
gasWeekday = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

gasHex    = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
ghiHex    = { 0:0, 1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7, 8:8, 9:9, A:10, B:11, C:12, D:13, E:14, F:15 };
gasBase64 = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_']; // '+', '/']; // http://en.wikipedia.org/wiki/Base64
ghiBase64 = { A:0, B:1, C:2, D:3, E:4, F:5, G:6, H:7, I:8, J:9, K:10, L:11, M:12, N:13, O:14, P:15, Q:16, R:17, S:18, T:19, U:20, V:21, W:22, X:23, Y:24, Z:25, a:26, b:27, c:28, d:29, e:30, f:31, g:32, h:33, i:34, j:35, k:36, l:37, m:38, n:39, o:40, p:41, q:42, r:43, s:44, t:45, u:46, v:47, w:48, x:49, y:50, z:51, 0:52, 1:53, 2:54, 3:55, 4:56, 5:57, 6:58, 7:59, 8:60, 9:61, '-':62, _:63 };

/*
function fromHexCode(iValue)
{
	return iValue < 58
	 ? iValue - 48
	 : iValue - 55;
}
*/

/*
	// fromHexCode is slower!

	ie   mozilla
	------------
	469  625
	484  656
	344  750
	344  766

	var s     = '0123456789ABCDEF',
		iSize = 100000,
		iTime;

	i = iSize; iTime = time(); while (i--) fromHexCode(s.charCodeAt(0));  log(time() - iTime);
	i = iSize; iTime = time(); while (i--) fromHexCode(s.charCodeAt(15)); log(time() - iTime);
	i = iSize; iTime = time(); while (i--) ghiHex[s.charAt(0)];           log(time() - iTime);
	i = iSize; iTime = time(); while (i--) ghiHex[s.charAt(15)];          log(time() - iTime);
*/

function integer_toBinaryIf(i) { return i < 2147483648 ? integer_toBinary(i) : '0'; }
function integer_toHexIf   (i) { return i < 2147483648 ? integer_toHex   (i) : '0'; }

function integer_toBinary  (i) { var sValue = gasHex[i & 1];  while (i >>= 1) sValue = gasHex[i & 1]  + sValue; return sValue; }
function integer_toHex     (i) { var sValue = gasHex[i & 15]; while (i >>= 4) sValue = gasHex[i & 15] + sValue; return sValue; }

function binary_toDecimal(s)
{
	var i      = s.length,
		iPlace = 0,
		iValue = 0;

	while (i--)
		iValue += (s.charCodeAt(i) - digit0Key) << iPlace++;

	return iValue;
}

function hex_toDecimal(s) // assumes upperCase
{
	var i      = s.length,
		iPlace = 0,
		iValue = 0;

	while (i--)
	{
		iValue += ghiHex[s.charAt(i)] << iPlace;
		iPlace += 4;
	}

	return iValue;
}

/*
function integer_toHex(i) { return integer_toBase(i, 16); }

function integer_toBase(i, iBase) // assumes `2 <= iBase <= 16`
{
	var sNumber =
			i
			 ? ''
			 : '0';

	while (i)
	{
		sNumber = gasFromBase10ToUptoBase16[i % iBase] + sNumber;

		i = floor(i / iBase);
	}

	return sNumber;
}
*/


// format
//--------------------------------------

function formatInteger(i) // adds commas at every thousand's place
{
	var siReturn = '',
		si       = i + '',
		i        = si.length;

	while (0 < (i -= 3))
		siReturn = ',' + si.slice(i, i + 3) + siReturn;

	return si.slice(0, i + 3) + siReturn;
}

// function formatCurrencyNumber(nValue) { return formatCurrency(nValue + ''); }

function formatCurrency(snValue)
{
	var n         = Number(snValue.replace(/[^-.0-9]/g, '')), // || 0, // ` || 0`: replaces `if (isNaN(n)) n = 0`
		bNegative = n <= -0.005,
		nCents    = round(100 * (n = abs(n))) % 100;

	return (bNegative ? '-$' : '$')
	 + formatInteger(floor(n + 0.005)) // ` + 0.005`: so 0.995 becomes 1 and -0.005 becomes 0
	 + '.' + digits2(nCents);
}


// statistics
//--------------------------------------

function RunningAverage()
{
	this.count =
	this.total = 0;

	this.add     = function(iValue) { this.total += iValue; this.count++; }; // `this.average = (this.average * this.count + iValue) / ++this.count;` wasteful
	this.average = function(iValue) { return this.total / this.count; };
	this.per     = function(iValue) { return this.count / this.total; };
}


// sequence
//--------------------------------------

function sequence0(       iTo) { var i = iTo + 1,         aiSequence = new Array(i); while (i--) aiSequence[i] = i;         return aiSequence; }
function sequence (iFrom, iTo) { var i = iTo + 1 - iFrom, aiSequence = new Array(i); while (i--) aiSequence[i] = i + iFrom; return aiSequence; }

/* unused
function sequenceDifference(iStart, iEnd, iDifference)
{
	var aiSequence = [iStart];

// debug {
	if (!iDifference) errorIn('Expected non-zero iDifference', 'sequenceDifference');
// debug }

	if (iStart < iEnd)
	{
// debug {
		if (iDifference < 0) errorIn('iDifference is the wrong sign', 'sequenceDifference');
// debug }

		while (iStart < iEnd)
			aiSequence.push(iStart += iDifference);
	}
	else
	{
// debug {
		if (0 < iDifference) errorIn('iDifference is the wrong sign', 'sequenceDifference');
// debug }

		while (iEnd < iStart)
			aiSequence.push(iStart += iDifference);
	}

	return aiSequence;
}
*/

/*
function sequenceDifference(iStart, iEnd, iDifference)
{
	var aiSequence = [iStart];
	
	while (iStart != iEnd)
		aiSequence.push(iStart += iDifference);

	return aiSequence;
}
*/

/*
function characterSequence_test()
{
	var iDigitCount = 3,
		asRange     = ['a', 'a', '0', '1'],
		oSequence   = new CharacterSequence(asRange),
		iSize       = characterSequence_size(iDigitCount, oSequence.aiRange) + 13,
		asValue     = new Array(iSize),
		i           = 0;

	while (i < iSize)
		asValue[i++] = oSequence.next();
	
	log(asValue);
	log(characterSequence(iDigitCount, asRange));
}
*/

function characterSequence(iDigitCount, asRange)
{
	var aiRange      = characterSequence_aiRange(asRange),
		aiValue      = array(iDigitCount, aiRange[0]),
		aiValueRange = array(iDigitCount, 1),
		iSize        = characterSequence_size(iDigitCount, aiRange),
		asValue      = new Array(iSize),
		i            = 0;

	while (i < iSize)
	{
		asValue[i++] = String.fromCharCode.apply(this, aiValue);
		characterSequence_nextValue(aiValue, aiRange, aiValueRange);
	}

	return asValue;
}

function characterSequence_aiRange(asRange)
{
	var i       = asRange.length,
		aiRange = new Array(i);

	while (i--)
		aiRange[i] = asRange[i].charCodeAt(0);

	return aiRange;
}

function characterSequence_size(iDigitCount, aiRange)
{
	var i     = aiRange.length,
		iSize = i >> 1; // save ` + 1` in loop below

	while (i)
		iSize += aiRange[--i] - aiRange[--i];

	return power(iSize, iDigitCount);
}

function characterSequence_nextValue(aiValue, aiRange, aiValueRange)
{
	var i = aiValue.length;

	while (i--)
	{
		if (aiValue[i] < aiRange[aiValueRange[i]])
			return aiValue[i]++; // quick exit

		if (aiValueRange[i] + 1 < aiRange.length)
			return aiValue[i] = aiRange[(aiValueRange[i] += 2) - 1];

		aiValueRange[i] = 1;
		aiValue     [i] = aiRange[0];
	}
	
	return 0;
}



function CharacterSequence(asRange)
{
	this.aiRange      = characterSequence_aiRange(asRange);
	this.aiValue      = [this.aiRange[0]];
	this.aiValueRange = [1];
}

CharacterSequence.prototype.next = function()
{
	var sValue = String.fromCharCode.apply(this, this.aiValue);
	this.nextValue();
	return sValue;
};

CharacterSequence.prototype.nextValue = function()
{
	var i = this.aiValue.length;

	while (i--)
	{
		if (this.aiValue[i] < this.aiRange[this.aiValueRange[i]])
			return this.aiValue[i]++; // quick exit

		if (this.aiValueRange[i] + 1 < this.aiRange.length)
			return this.aiValue[i] = this.aiRange[(this.aiValueRange[i] += 2) - 1];

		this.aiValueRange[i] = 1;
		this.aiValue     [i] = this.aiRange[0];
	}

	this.aiValue     .unshift(this.aiRange[0]);
	this.aiValueRange.unshift(1);
};


// temperature
//--------------------------------------

function fahrenheitToCelsius(iValue) { return (iValue - 32) * 5 / 9; }
function celsiusToFahrenheit(iValue) { return 9 * iValue / 5 + 32; }


// Number
//==============================================================================
// String


// extract
//--------------------------------------

function string_chop    (s) { return s.slice(0, -1); }
//function string_last  (s) { return s.slice(-1); }
//function string_lastCode(s) { return s.charCodeAt(s.length - 1); }

/*
String.prototype.ends        = function() { return this.charAt(0) + this.slice(-1); };
String.prototype.chop        = function() { return this.slice(0, -1); };
String.prototype.chopEnds    = function() { return this.slice(1, -1); };
String.prototype.first       = function() { return this.charAt(0); };
String.prototype.last        = function() { return this.slice(-1); };
*/


// info
//--------------------------------------

String.prototype.countCharRE        = function(sreChar)   { return this.countCharRECorrect(new RegExp('[^' + sreChar + ']+', 'g')); };
String.prototype.countCharRECorrect = function(reNotChar) { return this.replace(reNotChar, '').length; };

String.prototype.count = function(sValue) // faster than countCharRE only if few matches (< 5)
{
	var iCount = 0,
		i      = this.find(sValue) + 1;

	while (i)
	{
		iCount++;
		i = this.findFrom(sValue, i) + 1;
	}
	
	return iCount;
};

function string_startsWithOneOf(s, asValue) { var i = asValue.length; while (i--) if (string_startsWith(s, asValue[i])) return 1; return 0; } // true; false
function string_startsWith     (s, sValue)  { return sValue === s.slice(0, sValue.length); }
function string_endsWith       (s, sValue)  { return sValue === s.slice(-sValue.length); }

function string_has            (s, sValue)  { return -1 < s.indexOf(sValue); }

String.prototype.find              =
String.prototype.findFrom          = String.prototype.indexOf;
String.prototype.has               = function(sValue)   { return -1 < this.indexOf(sValue); };
String.prototype.hasNot            = function(sValue)   { return this.indexOf(sValue) < 0; };

String.prototype.find_ignoreCase   = function(sreValue) { return      this.search(new RegExp(sreValue, 'i')); };
String.prototype.has_ignoreCase    = function(sreValue) { return -1 < this.find_ignoreCase(sreValue); };
String.prototype.hasNot_ignoreCase = function(sreValue) { return      this.find_ignoreCase(sreValue) < 0; };
String.prototype.hasRE             = function(re)       { return -1 < this.search(re); };

function hasWord             (s, sValue) { return wordRegExp(sValue).test(s); }
// function hasWord          (s, sValue) { return array_has(s.split(' '), sValue); }; // size (maybe speed also); assumes this.trim_collapseSpace()
function wordRegExp          (sValue)    { return new RegExp('\\b' + sValue + '\\b'); } // does not handle `-` (eg: `top-left` finds word `top`)
function wordSpaceRegExp     (sValue)    { return new RegExp('^' + sValue + '( |$)| ' + sValue + '(?= |$)'); }

/* removeWord cases:
# (no match)     none    return string as-is
# match          only    remove word, left with empty string
# match ...      first   remove word and right whitespace
# ... match ...  middle  remove word and left whitespace
# ... match      last    remove word and left whitespace
*/
function removeWord(s, sValue) { return s.replace(wordSpaceRegExp(sValue), ''); }

String.prototype.findCase = function(sValue, bCaseSensitive)
{
	return bCaseSensitive
	 ? this.find           (sValue)
	 : this.find_ignoreCase(sValue);
};

String.prototype.findElseLength = function(sValue, iFrom)
{
	var i = this.find(sValue, iFrom);
	
	return i < 0
	 ? this.length
	 : i;
};

String.prototype.hasOneOf_ignoreCase = function(asValue)
{
	var i = asValue.length;

	while (i--)
	{
		if (this.hasNot_ignoreCase(asValue[i])) // .correctRE()
			return 0; // false
	}

	return 1; // true
};


// correct
//--------------------------------------

// function plural(iCount, sSingular, sPlural) { return iCount + ' ' + (1 === iCount ? sSingular : sPlural); }

function plurals(iCount, sSingular)          { return iCount + ' ' + (1 === iCount ? sSingular : sSingular + 's'); }
function plural (iCount, sSingular, sPlural) { return iCount + ' ' + (1 === iCount ? sSingular : sPlural); }

function pluralValue(iCount, sSingular, sPlural) // `sPlural` optional
{
	return 1 === iCount
	 ? sSingular
	 : sPlural || sSingular + 's';
}

String.prototype.addPathIf               = function(sPath)   { return string_startsWith(this, '/') || this.has('://') ? this + '' : sPath + this; };

/* just use encodeURIComponent()
String.prototype.correctFileName         = function()        { return encodeURIComponent(this); }; // this.replace(/&/g, '%26').replace(/#/g, '%23'); }; // [http://dev/#1.gif http://dev/1#.gif] invalid, at least in <img src="...">

String.prototype.correctHttpFileName =
	ie
	 ? function() { return this.replace(/&/g, '%26'); }
	 : String.prototype.correctFileName;
*/

function collapseSpace          (s) { return s.replace(/ {2,}/g, ' '); };
function trim                   (s) { return s.replace(/^ +| +$/g, ''); }
function trim_collapseSpace     (s) { return s.replace(/^ +| +$|( ){2,}/g, '$1'); }
function trimWhitespace         (s) { return s.replace(/^\s+|\s+$/g, ''); }
function trim_collapseWhitepace (s) { return s.replace(/^\s+|\s+$|(\s)+/g, '$1'); }

function fixEnter               (s) { return s.replace(/\r\n|\n/g, '\r'); }; // 13 === enterKey
function fixNewline             (s) { return s.replace(/\r\n|\r/g, '\n'); }; // unix:\n  mac:\r  windows:\r\n
function nameToScript           (s) { return s.toLowerCase().replace(/ (\w)/g, function(sMatch, sChar) { return sChar.toUpperCase() }); } // 'FIRST NAME' -> firstName
function dashToScript           (s) { return s.replace(/-(.)/g, function(s, sLowerCase) { return sLowerCase.toUpperCase(); }); }

String.prototype.correctRE               = function()           { return this.replace(/(\(|\)|\[|]|\{|\.|\?|\*|\+|\||\^|\$|\/|\\)/g, '\\$1'); }; // ()[]{.?*+|^$/\
String.prototype.truncate                = function(iLength)    { return iLength < this.length ? this.slice(0, iLength) + '...' : this + ''; };

//String.prototype.charToUpperCase       = function()           { return String.fromCharCode(this.charCodeAt(0) - 32); }; // todo: test if faster than .toUpperCase()
String.prototype.scriptToDash            = function()           { return this.replace(/([A-Z])/g, function(s, sUpperCase) { return '-' + sUpperCase.toLowerCase(); }); };
String.prototype.scriptToSentenceCase    = function()           { return this.charAt(0).toUpperCase() + this.slice(1).replace(/[A-Z]+|[0-9]+/g, function(sMatch) { return ' ' + sMatch.toLowerCase(); }); };
String.prototype.scriptToTitleCase       = function()           { return this.charAt(0).toUpperCase() + this.slice(1).replace(/([A-Z]+|[0-9]+)/g, ' $1'); };
String.prototype.splitEmpty              = function(sSeparator) { return this.length ? this.split(sSeparator) : []; }; // `''.split('anything') === ['']`

function isIntegerType  (v)  { return 'number' === typeof v ? isInteger(v) : 0; }
function isInteger      (n)  { return !isNaN(n) && Infinity !== n && round(n) === n; } // round(Infnity) === Infinity
function isStringInteger(si) { return round(Number(si)) == si && '' !== si; } // `'' !== si`: want `false === isStringInteger('')`
function isStringNumber (si) { return       Number(si)  == si && '' !== si; } // "

String.prototype.mysqlToDate             = function()           { return new Date(this.replace(/-/g, '/')); };

String.prototype.quoteBackslashAmpersand = function()           { return '"' + this.replace(/("|\\)/g, '\\$1').replace(/\r\n|\n|\r/g, '%0A').replace(/&/g, '%26') + '"'; };
String.prototype.quoteStutter            = function()           { return '"' + this.replace(/"/g, '""'); };
String.prototype.unquoteStutter          = function()           { return this.replace(/""/g, '"'); };

// debug {
function ascii(s)
{
	var aiAscii = [],
		i       = -1;

	while (++i < s.length)
		aiAscii.push(s.charCodeAt(i));

	return aiAscii;
}

Array.prototype.asciiToString = function()
{
	var i       = this.length,
		asValue = new Array(i);
	
	while (i--)
		asValue[i] = String.fromCharCode(this[i]);
	
	return asValue.join('');
};
// debug }


// utility
//--------------------------------------

function string_repeatShort(s, iSize)
{
	return iSize < 1   ? '' // not `!iSize`: may be < 0
	 :     1 === iSize ? s
	 :     2 === iSize ? s + s
	 :                   s + s + string_repeat(s, iSize - 2);
}

function string_repeat(s, iSize)
{
// debug {
	if (iSize < 1) return errorIn('iSize < 1: infinite loop', 'string_repeat');
// debug }

	var sValue = s;

	while (--iSize)
		sValue += s;

	return sValue;
}


// String
//==============================================================================
// Bag


function queue_pop(bag)
{
	var s;

	for (s in bag)
	{
		delete bag[s];
		return s;
	}

	return '';
}


// Bag
//==============================================================================
// Object


function keys     (o)          { var s, as = [];     for (s in o)      as.push(s); return as; }
function ocopy    (o)          { var s, oCopy = {};  for (s in o)      oCopy[s] = o[s]; return oCopy; }
function oempty   (o)          { var s;              for (s in o)      return 0; return 1; } // false; true
function onotEmpty(o)          { var s;              for (s in o)      return 1; return 0; } // true;  false
function ofind    (o, vValue)  { var s;              for (s in o)      if (vValue === o[s]) return s; }
function ojoinBag (bag, sWith) { var s, asWith = []; for (s in bag)    asWith.push(s); return asWith.join(sWith); }
function omergeIn (o, oMerge)  { var s;              for (s in oMerge) o[s] = oMerge[s]; }
function ohasChild(o, oChild)  { while (oChild && o !== oChild) oChild = oChild.parent; return o === oChild; }

/*
function onext    (o)          { var s; for (s in o) return s; }
*/


// Object
//==============================================================================
// Array


function IntegerArrayFromBase64(saBase64, iBitCount)
{
	this.iPer          =
	this.i             = 0;
	this.asBase64      = saBase64.split('');
	this.iBitCount     = iBitCount;
	this.iOriginalPer  = 6 / iBitCount - 1;
	this.iOriginalMask = (1 << iBitCount) - 1;
}

IntegerArrayFromBase64.prototype.next = function()
{
	if (!this.iPer--)
	{
// debug {
	if (this.i === this.asBase64.length) errorIn('Out of bounds', 'IntegerArrayFromBase64_bit.next');
// debug }

		this.iBase64 = ghiBase64[this.asBase64[this.i++]];
		this.iMask   = this.iOriginalMask;
		this.iPer    = this.iOriginalPer;
		this.iShift  = 0;
	}
	else
	{
		this.iMask <<= this.iBitCount;
		this.iShift += this.iBitCount;
	}

	return (this.iBase64 & this.iMask) >> this.iShift;
};

/*
function IntegerArrayFromBase64_bit(saBase64) // speed: no `iBitCount iOriginalPer iOriginalMask iShift`
{
	this.iPer     =
	this.i        = 0;
	this.asBase64 = saBase64.split('');
}

IntegerArrayFromBase64_bit.prototype.next = function()
{
	if (!this.iPer--)
	{
// debug {
	if (this.i === this.asBase64.length) errorIn('Out of bounds', 'IntegerArrayFromBase64_bit.next');
// debug }

		this.iBase64 = this.asBase64[this.i++];
		this.iMask   = 1;
		this.iPer    = 6;
	}
	else
		this.iMask <<= 1;

	return this.iBase64 & this.iMask;
};
*/

function IntegerArrayBase64(iSizeTo, iBitCount)
{
	this.iSizeTo      = iSizeTo;
	this.asBase64     = array0(iSizeTo);
	this.iBitCount    = iBitCount;
	this.iOriginalPer = 6 / iBitCount;

// debug {
	if (!isInteger(this.iOriginalPer)) error('Expected integer, not ' + this.iOriginalPer);
// debug }

	this.reset();
}

IntegerArrayBase64.prototype.reset = function()
{
	this.count   =
	this.iSize   =
	this.iShift  =
	this.iBase64 = 0;
	this.iPer    = this.iOriginalPer;
};

IntegerArrayBase64.prototype.grow = function() { this.asBase64 = this.asBase64.concat(array0(this.iSizeTo <<= 1)); };

IntegerArrayBase64.prototype.add = function(iValue)
{
	if (!--this.iPer)
	{
		if (this.iSizeTo === this.iSize) // this.growIf()
			this.grow();

		this.asBase64[this.iSize++] = gasBase64[this.iBase64 | iValue << this.iShift];

		this.iShift  =
		this.iBase64 = 0;
		this.iPer    = this.iOriginalPer;
	}
	else // if (this.iShift) // size
	{
		this.iBase64 |= iValue << this.iShift;
		this.iShift += this.iBitCount;
	}
/*	else // speed
	{
		this.iBase64 = iValue;
		this.iShift  = this.iBitCount;
	}
*/
	this.count++;
};

IntegerArrayBase64.prototype.toString = function()
{
	return this.iShift
	 ? this.asBase64.slice(0, this.iSize).join('') + gasBase64[this.iBase64]
	 : this.asBase64.slice(0, this.iSize).join('');
};


// query
//--------------------------------------

function array_has    (a, v) { return -1 < array_indexOf(a, v); }
function array_indexOf(a, v) { var i = a.length; while (i--) if (v === a[i]) return i; return -1; }

function array_splitEmpty(s, sSeparator)
{
	return s.length
	 ? s.split(sSeparator)
	 : [];
}

Array.prototype.has      = function(vValue) { return -1 < this.find(vValue); }; 
Array.prototype.hasNot   = function(vValue) { return      this.find(vValue) < 0; }; 
Array.prototype.find     = function(vValue) { var i = this.length; while (i--) if (vValue === this[i]) return i; return -1; };
Array.prototype.last     = function()       { return this[this.length - 1]; };

Array.prototype.only = function() // firstAndOnly
{
// debug {
	if (1 !== this.length)
		error("Expected one value but found " + this.length + ' values.');
// debug }

	return this[0];
};


// operation
//--------------------------------------

// debug {
function array_is0(a) { var i = a.length; while (i--) if ( a[i]) return 0; return 1; }
function array_is1(a) { var i = a.length; while (i--) if (!a[i]) return 0; return 1; }
// debug }

function array           (iSize)        { return new Array(iSize); }
function array0          (iSize)        { var ai = array(iSize); while (iSize--) ai[iSize] = 0;     return ai; }
function array1          (iSize)        { var ai = array(iSize); while (iSize--) ai[iSize] = 1;     return ai; }
function arrayEmptyString(iSize)        { var as = array(iSize); while (iSize--) as[iSize] = '';    return as; }
function arrayOf         (iSize, vFill) { var av = array(iSize); while (iSize--) av[iSize] = vFill; return av; }

function array_fill0(a)            { var i = a.length; while (i--) a[i] = 0; }
function array_fill1(a)            { var i = a.length; while (i--) a[i] = 1; }
function array_fill (a, v)         { var i = a.length; while (i--) a[i] = v; }
//function array_lowerCase(a)      { var i = a.length; while (i--) a[i] = a[i].toLowerCase(); }
//function array_upperCase(a)      { var i = a.length; while (i--) a[i] = a[i].toUpperCase(); }

function array_maxStringSize(a)
{
	var i    = a.length - 1,
		iMax = a[i].length;

	while (i)
	{
		if (iMax < a[--i].length)
			iMax = a[  i].length;
	}

	return iMax;
}

function array_addFirst(a, v)
{
	var i = a.length - 1;

// debug {
	if (i < 0) error('Expected non-empty array');
// debug }

	while (i--)
		a[i + 1] = a[i];
	
	a[0] = v;
}

function array_shuffle(a)
{
	var iSize      = a.length,
		iLast      = iSize - 1,
		abPick     = array0(iSize),
		avShuffled = array0(iSize),
		i          = iSize;

    while (i--)
	{
		j = floor(iSize * random()); // randomInteger0To(iSize);

		if (abPick[j])
		{
			if (random() < 0.5) // randomBoolean()
				while (abPick[j = j === iLast ? 0 : j + 1]);
			else
				while (abPick[j = j ? j - 1 : iSize - 1]);
		}

		abPick    [j] = 1;
		avShuffled[i] = a[j];
	}

	return avShuffled;
}

Array.prototype.addFirst      = function(v)     { this.unshift(v); };      // prepend
Array.prototype.addAt         = function(v, i)  { this.splice(i, 0, v); }; // insert
Array.prototype.merge         = Array.prototype.concat;
Array.prototype.copy          = function()      { return this.slice(); }; // works, altho documentation claims `iStart` (first parameter) is not optional
Array.prototype.decrementFrom = function(i)     { while (i < this.length) this[i++]--; };
Array.prototype.mergeIn       = function(aWith) { var i = -1; while (++i < aWith.length) this.push(aWith[i]); }; // Array.concat returns a new array, it does not modify the source array
Array.prototype.removeIndex   = function(i)     { return this.splice(i, 1)[0]; };


// convert
//--------------------------------------

Array.prototype.toHash            = function()  { var i = this.length, hiIndex = {}; while (i--) hiIndex[this[i]] = i;                           return hiIndex; };
Array.prototype.toNumberIn        = function()  { var i = this.length;               while (i--) this[i]          = Number(this[i]);             return this; };
Array.prototype.scriptToTitleCase = function()  { var i = this.length, asValue = []; while (i--) asValue[i]       = this[i].scriptToTitleCase(); return asValue; };


// utility
//--------------------------------------

function createIncrementArray(i) // iLength
{
	var aiIndex = new Array(i);

	while (i--)
		aiIndex[i] = i;

	return aiIndex;
}


// Array
//==============================================================================
// Php


function phpArrays_split(sa)
{
	return sa.length
	 ? phpArrays_splitCorrect(sa)
	 : [];
}

function phpArrays_splitCorrect(sa)
{
	var aas = sa.split('\2'), // asa
		i   = aas.length;
	
	while (i--)
		aas[i] = aas[i].split('\1');

	return aas;
}

function phpToStringArray(saValue) { return saValue.split('\1').slice(1); }

function phpToIntegerArray(saValue)
{
	var aiValue = saValue.split('\1').slice(1),
		i       = aiValue.length;

	while (i--)
		aiValue[i] = Number(aiValue[i]);

	return aiValue;
}

function phpToRecordArray(saValue, asName, asDefaultValue)
{
	return phpToRecordArrayMap
	(
		saValue,
		asName,

		function(sValue, i, iCountMinus1nt)
		{
			return sValue
			 ? sValue.replace('.', ' ')
			 : asDefaultValue[iCountMinus1 - i]; // todo: abstract away array of columns or array of records or records
		}
	);
}

function phpToRecordArray_namePluralEnd(saValue, asName)
{
	return phpToRecordArrayMap
	(
		saValue,
		['tableName', 'englishName', 'englishNamePlural'],

		function(sValue, i, iCountMinus1, asValue)
		{
			return sValue                ? sValue.replace('.', ' ')
			 :     i < iCountMinus1      ? 'Record'
			 : asValue[iCountMinus1 - 1] ? asValue[iCountMinus1 - 1].replace('.', ' ') + 's'
			 : 'Records';
		}
	);
}

function phpToRecordArrayMap(saValue, asName, fMap)
{
	var asaValue = saValue.split(','),
		i        = asaValue.length,
		arValue  = new Array(i);

	while (i--)
		arValue[i] = phpToRecordArrayMapHelper(asaValue[i].split(' '), asName, fMap);

	return arValue;
}

function phpToRecordArrayMapHelper(asValue, asName, fMap)
{
	var rValue       = {},
		i            = asName.length,
		iCountMinus1 = i - 1;

	while (i--)
		rValue[asName[i]] = fMap(asValue[i], i, iCountMinus1, asValue);

	return rValue;
}


// Php
//==============================================================================
// Date Time


function time                  ()               { return new Date().getTime(); }
function time_toDate           (i)              { return new Date(i * 1000); }
function time_format           ()               { var oDate = new Date(); return digits2(oDate.getHours()) + ':' + digits2(oDate.getMinutes()) + ':' + digits2(oDate.getSeconds()); }

function time_hour             (iHHMM)          { return floor(iHHMM / 100); }
function time_minute           (iHHMM)          { return (iHHMM % 100) * 60 / 100; }

function time_12hour           (iHour)          { return !iHour ? 12   : iHour < 13 ? iHour : iHour - 12; }
function time_suffix           (iHour)          { return                 iHour < 12 ? 'am'  : 'pm'; }

function time_decimalHour      (iHour)          { return 100 * iHour; }
function time_decimalMinute    (       iMinute) { return iMinute * 100 / 60; }
function time_decimalHourMinute(iHour, iMinute) { return time_decimalHour(iHour) + time_decimalMinute(iMinute); }


// Date Time
//==============================================================================
// Utility


function emptyFunction    ()  {}
function returnValue      (v) { return v; }
function returnTrue       ()  { return 1; }
//function returnFalse    ()  { return 0; } // unused
//function mysqlTimeToDate(s) { return new Date(s.slice(0, 4) + '/' + s.slice(4, 6) + '/' + s.slice(6, 8) + ' ' + s.slice(8, 10) + ':' + s.slice(10, 12) + ':' + s.slice(12)); } // yyyymmddhhmmss

function parametersToArray(oParameters) { return Array.prototype.slice.apply(oParameters); } // works!

/*
function parametersToArray(oParameters)
{
	var i        = oParameters.length,
		avReturn = new Array(i);

	while (i--)
		avReturn[i] = oParameters[i];

	return avReturn;
}
*/


// join
//--------------------------------------

function array_deepCopy(a)
{
	var i = a.length,
		v,
		aCopy = [];
	
	while (i--)
	{
		v = a[i];

		aCopy[i] =
			   v instanceof Array  ? array_deepCopy(v)
			 : v instanceof Object ? odeepCopy(v)
			 : v;
	}

	return aCopy;
}

function odeepCopy(o)
{
	var s,
		v,
		oCopy = {};
	
	for (s in o)
	{
		v = o[s];

		oCopy[s] =
			   v instanceof Array  ? array_deepCopy(v)
			 : v instanceof Object ? odeepCopy(v)
			 : v;
	}
	
	return oCopy;
}

// debug {
function d(v) { return deepJoinWithType(v); }

function deepJoinWithType(v) { return deepJoin(v, 1); } // true

function deepJoin(v, bWithType)
{
	return 'object' === typeof v
	 ?
		   v instanceof Array ? v.deepJoin(bWithType)
		 : v instanceof Date  ? v
		 : isElement(v)       ? '(element)'
		 : odeepJoin(v, bWithType)
	 :
		bWithType
		 ?
			'(' + (typeof v) + ')' + v // withType(v)
		 :
			'string' === typeof v
			 ? '"' + v + '"'
			 : v;
}

function odeepJoin(o, bWithType)
{
	var asValue = [],
		sName;
	
	for (sName in o)
		asValue.push(sName + ':' + deepJoin(o[sName], bWithType));
	
	return '{ ' + asValue.join(' ') + ' }';
}

Array.prototype.deepJoin = function(bWithType)
{
	var asValue = [],
		i       = -1;

	while (++i < this.length)
		asValue.push(deepJoin(this[i], bWithType));

	return '[' + asValue.join(' ') + ']';
};

function deepJoinStringOnly(o)
{
	var sName,
		asValue = [];
	
	for (sName in o)
	{
		if ('function' !== typeof o[sName] && sName !== sName.toUpperCase()) // 'object' !== typeof o[sName] &&
			asValue.push(sName + ':' + o[sName]);
	}
	
	return asValue.join(',');
}
// debug }


// Utility
//==============================================================================
// Grid


function Grid(x, y, vClearValue)
{
	this.sizeX      = x;
	this.sizeY      = y;
	this.size       = x * y;
	this.clearValue = vClearValue;
	this.items      = arrayOf(this.size, this.clearValue);
}

Grid.prototype.clear = function() { array_fill(this.items, this.clearValue); };

Grid.prototype.clearIndex = function(i)
{
// debug {
	this.verifyIndex(i);
// debug }

	this.items[i] = this.clearValue;
};

Grid.prototype.clearXY = function(x, y)
{
// debug {
	this.verifyXY(x, y);
// debug }

	this.items[y * this.sizeX + x] = this.clearValue;
};

Grid.prototype.index = function(i)
{
// debug {
	this.verifyIndex(i);
// debug }

	return this.items[i];
};

Grid.prototype.xy = function(x, y)
{
// debug {
	this.verifyXY(x, y);
// debug }

	return this.items[y * this.sizeX + x];
};

Grid.prototype.changeIndex = function(i, v)
{
// debug {
	this.verifyIndex(i);
// debug }

	this.items[i] = v;
};

Grid.prototype.changeXY = function(x, y, v)
{
// debug {
	this.verifyXY(x, y);
// debug }

	this.items[y * this.sizeX + x] = v;
};

// debug {
Grid.prototype.verifyIndex = function(i)
{
	if (isNotBetween(i, 0, this.sizeX * this.sizeY - 1)) error('i out of bounds');
};

Grid.prototype.verifyXY = function(x, y)
{
	if (isNotBetween(x, 0, this.sizeX)) error('x out of bounds');
	if (isNotBetween(y, 0, this.sizeY)) error('y out of bounds');
};
// debug }


// Grid
//==============================================================================