multidata
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © faiyaz7283

//@version=5

// @description A library for multi-dimensional data arrays.
library("multidata", overlay=true)

// Imports
import faiyaz7283/tools/9 as tools
import faiyaz7283/printer/6 as p

var ALT_ERROR 		= "Wrong data type is being used to set altVal. The altVal must match the original val type of "
var SET_ERROR 		= "Wrong data type is being used to set value. The value must match the original value type of "
var DK_PUSH_ERROR 	= "Total data key value missmatch. There must be a total of {0} new data key values, {1} provided."


// { Comparison calculation:

__et(orgVal, altVal) =>
	// @function For *INTERNAL* use only.
	tools._bool(orgVal == altVal)

__lte(orgVal, altVal) =>
	// @function For *INTERNAL* use only.
	tools._bool(orgVal <= altVal)

__lt(orgVal, altVal) =>
	// @function For *INTERNAL* use only.
	tools._bool(orgVal < altVal)

__gte(orgVal, altVal) =>
	// @function For *INTERNAL* use only.
	tools._bool(orgVal >= altVal)

__gt(orgVal, altVal) =>
	// @function For *INTERNAL* use only.
	tools._bool(orgVal > altVal)

__change(orgVal, altVal, bool percent) =>
	// @function For *INTERNAL* use only.
	result = orgVal - altVal
	if percent
		result := result / altVal * 100
	result

__changeF(orgVal, altVal, bool percent, _type) =>
	// @function For *INTERNAL* use only.
	_else = _type == 'float' ? '{0,number,currency}' : '{0}'
	format = percent ? "{0,number,0.00}%" : (_type == "timestamp" ? tools._getTimeDifference(tools._int(orgVal), tools._int(altVal)) : _else)
	str.format(format, __change(orgVal, altVal, percent))

// }

// { Custom Key & Value Objects:

// Define `val` - A value storage. Allowed data types are string, float, int, bool.
// stringValue	(string) A string value. Also, color values are converted and stored as string values. (`na` is used if no argument is supplied).
// floatValue	(string) A float value. (`na` is used if no argument is supplied). 
// intValue		(string) An integer value. Both regular integer values as well as unix timestamp values (milliseconds), are stored as integer values. (`na` is used if no argument is supplied). 
// boolValue	(string) A boolean value. (`na` is used if no argument is supplied).
export type val
	string stringValue = na
	float floatValue = na
	int intValue = na
	bool boolValue = na

// Define `kv` - A key value pair storage.
// key 						(string) The key identifier.
// val 						(val) A value represented by the val object. Must contain one of the val properties: val.stringValue, val.floatValue, val.intValue, or val.boolValue.
// altVal 					(val) An alternate value represented by the val object. Must contain one of the val properties: val.stringValue, val.floatValue, val.intValue, or val.boolValue. (`na` is used if no argument is supplied).
// format					(string) A custom string format for the val object. (`na` is used if no argument is supplied).
// changeFormat				(string) A custom string format targeting the change value between original and alt value. (`na` is used if no argument is supplied).
// changePercentFormat		(string) A custom string format targeting the change percent value between original and alt value. (`na` is used if no argument is supplied).
// dataType					(string) The data type for the value. Possible values are string, float, int, bool, color, and timestamp. (`na` is used if no argument is supplied). 
// value					(string) The string version of the original value. (`na` is used if no argument is supplied).
// altValue					(string) The string version of the original alt value. (`na` is used if no argument is supplied).
// formatValue				(string) The formatted version of the original value. If no format is available, just uses string version.. (`na` is used if no argument is supplied). 
// formatAltValue			(string) The formatted version of the alternate value. If no format is available, just uses string version. (`na` is used if no argument is supplied). 
// changeValue				(float) The change between original and alt value. (`na` is used if no argument is supplied).
// changePercentValue		(float) The change percent between original and alt value. (`na` is used if no argument is supplied).
// formatChangeValue		(string) The formatted version of change between original and alt value. (`na` is used if no argument is supplied).
// formatChangePercentValue	(string) The formatted version of change percent between original and alt value. (`na` is used if no argument is supplied).
// etValue					(bool) Comparing 'equal to' between original and alt value. (`na` is used if no argument is supplied).
// ltValue					(bool) Comparing 'less than' between original and alt value. (`na` is used if no argument is supplied).
// lteValue					(bool) Comparing 'less than or equal to' between original and alt value. (`na` is used if no argument is supplied).
// gtValue					(bool) Comparing 'greater than' between original and alt value. (`na` is used if no argument is supplied).
// gteValue					(bool) Comparing 'greater than or equal to' between original and alt value. (`na` is used if no argument is supplied).
// timezone					(string) A custom timezone for the value. Applies only for timestamp data types. (`na` is used if no argument is supplied).
// _tz						(bool) An internal switch to determine if custom timezone is set for this object. (`false` is used if no argument is supplied).
export type kv
	string key
	val val
	val altVal = na
	string format = na
	string changeFormat = na
	string changePercentFormat = na
	string dataType = na
	string value = na
	string altValue = na
	string formatValue = na
	string formatAltValue = na
	float changeValue = na
	float changePercentValue = na
	string formatChangeValue = na
	string formatChangePercentValue = na
	bool etValue = na
	bool ltValue = na
	bool lteValue = na
	bool gtValue = na
	bool gteValue = na
	string timezone = na
	bool _tz = false

// Define `pkv` - A primary key value pair storage.
// primaryKey 	(string) The primary key identifier.
// dataKeys 	(array<string>) An array of data key identifiers.
// dataKeyTypes (array<string>) An array of data types asscociated with the object's data keys.
// kvs 			(array<kv>) An array of kv objects. They must adhere to this objects data keys and data key types.
export type pkv
	string primaryKey
	array<string> dataKeys
	array<string> dataKeyTypes
	array<kv> kvs


// }

// { kv() and alt() methods:

// @function    				The kv() method is utilized to construct a kv object. This method systematically constructs the kv object, ensuring that all of its properties are properly satisfied.
// @param this					(string) A string object, representing a unique key identifier.
// @param val					Any object of data type string, float, integer (including Unix timestamp), boolean and color.
// @param altVal				Any object of data type string, float, integer (including Unix timestamp), boolean and color. (`na` is used if no argument is supplied).
// @param timestamp				(bool) A boolean A boolean flag to indicate if the integer value is of timestamp data type. Use true for timestamp values. This parameter is only available with integer values (includes timestamps); for all other cases, not available. (`na` is used if no argument is supplied).
// @param timezone				(string) A string representing timezone in UTC/GMT notation (e.g., "UTC-5", "GMT+0530") or as an IANA time zone database name (e.g., "America/New_York"). This parameter is only available with integer values (includes timestamps); for all other cases, not available. (`na` is used if no argument is supplied).
// @param format				(string) A custom format. (`na` is used if no argument is supplied).
// @param changeFormat			(string) A custom change format. (`na` is used if no argument is supplied).
// @param changePercentFormat	(string) A custom change percent format. (`na` is used if no argument is supplied).
// @returns  					(kv) The `kv` object.
export method kv(string this, int val, int altVal=na, bool timestamp=na, string timezone=na, string format=na, string changeFormat=na, string changePercentFormat=na) =>
	strVl = tools._str(val)
	strAvl = not na(altVal) ? tools._str(altVal) : na
	_timestamp = not na(timestamp) ? timestamp : false
	_timezone = not na(timezone) ? timezone : syminfo.timezone
	_tz = not na(timezone) ? true : false
	vl = val.new(intValue = val)
	avl = val.new(intValue = altVal)
	vt = _timestamp ? "timestamp" : "int"
	frmtVal = na(format) ? strVl : (_timestamp ? str.format_time(val, format, _timezone) : str.format(format, val))
	frmtAltValue = (not na(altVal) and not na(format)) ? (_timestamp ? str.format_time(altVal, format, _timezone) : str.format(format, altVal)) : strAvl
	float chngValue = na 
	float chngPercentValue = na
	string frmtChngValue = na
	string frmtChngPercentValue = na
	bool etValue = na
	bool ltValue = na
	bool lteValue = na
	bool gtValue = na
	bool gteValue = na
	if not na(altVal)
		chngValue := __change(val, altVal, false)
		chngPercentValue := __change(val, altVal, true)
		if _timestamp
			_cfKeys = not na(changeFormat) ? tools._commaSplit(changeFormat) : array.from('year', 'month', 'week', 'day', 'hour', 'minute', 'second')
			years = _cfKeys.includes('year')
			months = _cfKeys.includes('month') 
			weeks = _cfKeys.includes('week')
			days = _cfKeys.includes('day')
			hours = _cfKeys.includes('hour') 
			minutes = _cfKeys.includes('minute')
			seconds = _cfKeys.includes('second') 
			frmtChngValue := tools._getTimeDifference(val, altVal, years, months, weeks, days, hours, minutes, seconds)
		else
			frmtChngValue := not na(changeFormat) ? str.format(changeFormat, chngValue) : __changeF(val, altVal, false, 'int')
		frmtChngPercentValue := not na(changePercentFormat) ? str.format(changePercentFormat, chngValue) : __changeF(val, altVal, true, 'int')
		etValue := __et(val, altVal)
		ltValue := __lt(val, altVal)
		lteValue := __lte(val, altVal)
		gtValue := __gt(val, altVal)
		gteValue := __gte(val, altVal)
	kv.new(this, vl, avl, format, changeFormat, changePercentFormat, vt, strVl, strAvl, frmtVal, frmtAltValue, chngValue, chngPercentValue, frmtChngValue, frmtChngPercentValue, etValue, ltValue, lteValue, gtValue, gteValue, timezone, _tz)

export method kv(string this, string val, string altVal=na, string format=na, string changeFormat=na, string changePercentFormat=na) =>
	vl = val.new(stringValue = val)
	avl = val.new(stringValue = altVal)
	frmtValue = not na(format) ? str.format(format, val) : val
	frmtAltValue = not na(format) ? str.format(format, altVal) : altVal
	float chngValue = na 
	float chngPercentValue = na
	string frmtChngValue = na
	string frmtChngPercentValue = na
	bool etValue = na
	bool ltValue = na
	bool lteValue = na
	bool gtValue = na
	bool gteValue = na
	if not na(altVal)
		_val = str.length(val)
		_altVal = str.length(altVal)
		chngValue := __change(_val, _altVal, false)
		chngPercentValue := __change(_val, _altVal, true)
		frmtChngValue := not na(changeFormat) ? str.format(changeFormat, chngValue) : __changeF(_val, _altVal, false, 'string')
		frmtChngPercentValue := not na(changePercentFormat) ? str.format(changePercentFormat, chngValue) : __changeF(_val, _altVal, true, 'string')
		etValue := __et(_val, _altVal)
		ltValue := __lt(_val, _altVal)
		lteValue := __lte(_val, _altVal)
		gtValue := __gt(_val, _altVal)
		gteValue := __gte(_val, _altVal)
	kv.new(this, vl, avl, format, changeFormat, changePercentFormat, 'string', val, altVal, frmtValue, frmtAltValue, chngValue, chngPercentValue, frmtChngValue, frmtChngPercentValue, etValue, ltValue, lteValue, gtValue, gteValue)

export method kv(string this, float val, float altVal=na, string format=na, string changeFormat=na, string changePercentFormat=na) =>
	strVl = tools._str(val)
	strAvl = tools._str(altVal)
	vl = val.new(floatValue = val)
	avl = val.new(floatValue = altVal)
	frmtValue = not na(format) ? str.format(format, val) : strVl
	frmtAltValue = not na(format) ? str.format(format, altVal) : strAvl
	float chngValue = na 
	float chngPercentValue = na
	string frmtChngValue = na
	string frmtChngPercentValue = na
	bool etValue = na
	bool ltValue = na
	bool lteValue = na
	bool gtValue = na
	bool gteValue = na
	if not na(altVal)
		chngValue := __change(val, altVal, false)
		chngPercentValue := __change(val, altVal, true)
		frmtChngValue := not na(changeFormat) ? str.format(changeFormat, chngValue) : __changeF(val, altVal, false, 'float')
		frmtChngPercentValue := not na(changePercentFormat) ? str.format(changePercentFormat, chngValue) : __changeF(val, altVal, true, 'float')
		etValue := __et(val, altVal)
		ltValue := __lt(val, altVal)
		lteValue := __lte(val, altVal)
		gtValue := __gt(val, altVal)
		gteValue := __gte(val, altVal)
	kv.new(this, vl, avl, format, changeFormat, changePercentFormat, 'float', strVl, strAvl, frmtValue, frmtAltValue, chngValue, chngPercentValue, frmtChngValue, frmtChngPercentValue, etValue, ltValue, lteValue, gtValue, gteValue)

export method kv(string this, bool val, bool altVal=na, string format=na) =>
	strVl = tools._str(val)
	strAvl = tools._str(altVal)
	vl = val.new(boolValue = val)
	avl = val.new(boolValue = altVal)
	frmtValue = not na(format) ? str.format(format, strVl) : strVl
	frmtAltValue = not na(format) ? str.format(format, strAvl) : strAvl
	kv.new(this, vl, avl, format, "bool", strVl, strAvl, frmtValue, frmtAltValue)

export method kv(string this, color val, color altVal=na, string format=na) =>
	strVl = tools._str(val)
	strAvl = tools._str(altVal)
	vl = val.new(stringValue = strVl)
	avl = val.new(stringValue = strAvl)
	frmtValue = not na(format) ? str.format(format, strVl) : strVl
	frmtAltValue = not na(format) ? str.format(format, strAvl) : strAvl
	kv.new(this, vl, avl, format, "color", strVl, strAvl, frmtValue, frmtAltValue)

// @function    	Set alternate value for a `kv` object.
// @param this		(kv) A `kv` object.
// @param altVal	An object of type string, float, integer (including Unix timestamp), boolean, or color. It must match the data type of the kv object.
// @returns  		(kv) This `kv` object.
export method alt(kv this, int altVal) =>
	if this.dataType != "int" and this.dataType != "timestamp"
		runtime.error(ALT_ERROR + "'" + this.dataType + "'.")
	
	strAvl = tools._str(altVal)
	this.altVal := val.new(intValue = altVal)
	this.altValue := strAvl
	orgVal = this.val.intValue
	chngValue = __change(orgVal, altVal, false)
	chngPercentValue = __change(orgVal, altVal, true)

	if this.dataType == "int"
		this.formatAltValue := not na(this.format) ? str.format(this.format, altVal) : strAvl
		this.formatChangeValue := not na(this.changeFormat) ? str.format(this.changeFormat, chngValue) : __changeF(orgVal, altVal, false, 'int')
	else if this.dataType == "timestamp"
		this.formatAltValue := not na(this.format) ? str.format_time(altVal, this.format) : strAvl
		_cfKeys = not na(this.changeFormat) ? tools._commaSplit(this.changeFormat) : array.from('year', 'month', 'week', 'day', 'hour', 'minute', 'second')
		years = _cfKeys.includes('year')
		months = _cfKeys.includes('month') 
		weeks = _cfKeys.includes('week')
		days = _cfKeys.includes('day')
		hours = _cfKeys.includes('hour') 
		minutes = _cfKeys.includes('minute')
		seconds = _cfKeys.includes('second')
		this.formatChangeValue := tools._getTimeDifference(orgVal, altVal, years, months, weeks, days, hours, minutes, seconds)
	this.formatChangePercentValue := not na(this.changePercentFormat) ? str.format(this.changePercentFormat, chngValue) : __changeF(orgVal, altVal, true, 'int')
	this.etValue := __et(orgVal, altVal)
	this.ltValue := __lt(orgVal, altVal)
	this.lteValue := __lte(orgVal, altVal)
	this.gtValue := __gt(orgVal, altVal)
	this.gteValue := __gte(orgVal, altVal)
	this


export method alt(kv this, string altVal) =>
	if this.dataType != "string"
		runtime.error(ALT_ERROR + "'" + this.dataType + "'.")
	
	this.altVal := val.new(stringValue = altVal)
	this.altValue := altVal
	this.formatAltValue := not na(this.format) ? str.format(this.format, altVal) : altVal
	_orgVal = str.length(this.val.stringValue)
	_altVal = str.length(altVal)
	chngValue = __change(_orgVal, _altVal, false)
	chngPercentValue = __change(_orgVal, _altVal, true)
	this.formatChangeValue := not na(this.changeFormat) ? str.format(this.changeFormat, chngValue) : __changeF(_orgVal, _altVal, false, 'string')
	this.formatChangePercentValue := not na(this.changePercentFormat) ? str.format(this.changePercentFormat, chngValue) : __changeF(_orgVal, _altVal, true, 'string')
	this.etValue := __et(_orgVal, _altVal)
	this.ltValue := __lt(_orgVal, _altVal)
	this.lteValue := __lte(_orgVal, _altVal)
	this.gtValue := __gt(_orgVal, _altVal)
	this.gteValue := __gte(_orgVal, _altVal)
	this

export method alt(kv this, float altVal) =>
	if this.dataType != "float"
		runtime.error(ALT_ERROR + "'" + this.dataType + "'.")
	
	strAvl = tools._str(altVal)
	this.altVal := val.new(floatValue = altVal)
	this.altValue := strAvl
	this.formatAltValue := not na(this.format) ? str.format(this.format, altVal) : strAvl
	orgVal = this.val.floatValue
	chngValue = __change(orgVal, altVal, false)
	chngPercentValue = __change(orgVal, altVal, true)
	this.formatChangeValue := not na(this.changeFormat) ? str.format(this.changeFormat, chngValue) : __changeF(orgVal, altVal, false, 'float')
	this.formatChangePercentValue := not na(this.changePercentFormat) ? str.format(this.changePercentFormat, chngValue) : __changeF(orgVal, altVal, true, 'float')
	this.etValue := __et(orgVal, altVal)
	this.ltValue := __lt(orgVal, altVal)
	this.lteValue := __lte(orgVal, altVal)
	this.gtValue := __gt(orgVal, altVal)
	this.gteValue := __gte(orgVal, altVal)
	this

export method alt(kv this, bool altVal) =>
	if this.dataType != "bool"
		runtime.error(ALT_ERROR + "'" + this.dataType + "'.")
	
	strAvl = tools._str(altVal)
	this.altVal := val.new(boolValue = altVal)
	this.altValue := strAvl
	this.formatAltValue := not na(this.format) ? str.format(this.format, strAvl) : strAvl
	this

export method alt(kv this, color altVal) =>
	if this.dataType != "color"
		runtime.error(ALT_ERROR + "'" + this.dataType + "'.")
	
	strAvl = tools._str(altVal)
	this.altVal := val.new(stringValue = strAvl)
	this.altValue := strAvl
	this.formatAltValue := not na(this.format) ? str.format(this.format, strAvl) : strAvl
	this

// @function    The pkv() method is utilized to construct a `pkv` object, which is the preferred approach for generating `pkv` object. This method systematically constructs the pkv object, ensuring that all of its properties are properly satisfied.
// @param this	(string) A string object, representing a unique primary key identifier.
// @param kvs	(array<kv>) An array of `kv` objects.
// @returns  	(pkv) A `pkv` object.
export method pkv(string this, array<kv> kvs) =>
	dataKeys = array.new<string>()
	dataKeyTypes = array.new<string>()
	for kV in kvs
		dataKeys.push(kV.key)
		dataKeyTypes.push(kV.dataType)
	pkv.new(this, dataKeys, dataKeyTypes, kvs)

// }

// { Data2d: 

// Define `data2d` - A two dimensional array of key-value data structure..
// kvs 							(array<kv>) An array of `kv` objects.
// keys							(array<string>) An array of key identifiers. (`na` is used if no argument is supplied). 
// values						(array<string>) An array of converted main string values, formatted if applicable. (`na` is used if no argument is supplied).
// formatValues					(array<string>) An array of string main values, formatted if applicable. (`na` is used if no argument is supplied).
// altValues					(array<string>) An array of converted alternate string values, formatted if applicable. (`na` is used if no argument is supplied).
// formatAltValues				(array<string>) An array of main string values, formatted if applicable. (`na` is used if no argument is supplied).
// stringValues					(array<string>) An array of main string values. (`na` is used if no argument is supplied).
// stringAltValues				(array<string>) An array of alternate string values. (`na` is used if no argument is supplied).
// floatValues					(array<float>) An array of main float values. (`na` is used if no argument is supplied).
// floatAltValues				(array<float>) An array of alternate float values. (`na` is used if no argument is supplied).
// intValues					(array<int>) An array of main integer values. (`na` is used if no argument is supplied).
// intAltValues					(array<int>) An array of alternate integer values. (`na` is used if no argument is supplied).
// boolValues					(array<bool>) An array of main boolean values. (`na` is used if no argument is supplied).
// boolAltValues				(array<bool>) An array of alternate boolean values. (`na` is used if no argument is supplied).
// colorValues					(array<color>) An array of main color values. (`na` is used if no argument is supplied).
// colorAltValues				(array<color>) An array of alternate color values. (`na` is used if no argument is supplied).
// timestampValues				(array<int>) An array of main unix timestamp values. (`na` is used if no argument is supplied).
// timestampAltValues			(array<int>) An array of alternate unix timestamp values. (`na` is used if no argument is supplied).
// changeValues					(array<float>) An array of change values. (`na` is used if no argument is supplied).
// changePercentValues			(array<float>) An array of change percent values. (`na` is used if no argument is supplied).
// formatChangeValues			(array<string>) An array of formatted change values. (`na` is used if no argument is supplied).
// formatChangePercentValues	(array<string>) An array of formatted change percent values. (`na` is used if no argument is supplied).
// etValues						(array<bool>) An array of 'equal-to' values. (`na` is used if no argument is supplied).
// ltValues						(array<bool>) An array of 'less-than' values. (`na` is used if no argument is supplied).
// lteValues					(array<bool>) An array of 'less-than-or-equal-to' values. (`na` is used if no argument is supplied).
// gtValues						(array<bool>) An array of 'greater-than' values. (`na` is used if no argument is supplied).
// gteValues					(array<bool>) An array of 'greater-than-or-equal-to' values. (`na` is used if no argument is supplied).
// dataTypes 					(array<string>) An array of data types for each keys. (`na` is used if no argument is supplied).
// dataType 					(string) The data type representing all keys. Possible values are 'string', 'float', 'int', 'bool', 'color', 'timestamp', or 'mix'. 'mix' (`na` is used if no argument is supplied).
// size 						(int) Total size of a data2d object. (`na` is used if no argument is supplied).
// sorted						(bool) A boolean flag to determine if the data2d object is sorted or not. (`na` is used if no argument is supplied).
// asc							(bool) A boolean flag to determine if sorting is in ascending or descending order. Use 'true' for ascending and 'false' for descending. (`true` is used if no argument is supplied).
// change						(bool) A boolean flag to determine if sorting uses change percent values. (`false` is used if no argument is supplied).
// format						(string) A default string format for values. (`na` is used if no argument is supplied).
// formatTime					(string) A default time format for timestamp values. (`yyyy-MM-dd hh:mm:ss a z` is used if no argument is supplied).
// changeFormat					(string) A default string format targeting the change value between original and alt values. (`{0,number,currency}` is used if no argument is supplied).
// changePercentFormat			(string) A default string format targeting the change percent value between original and alt values. (`{0,number,0.00}%` is used if no argument is supplied).
// changeTimestampFormat		(string) A default string format targeting the change timestampr value between original and alt values. (`year,month,week,day,hour,minute,second` is used if no argument is supplied).
// index						(array<int>) An array of integer values representing key index. If the object is sorted, the index will be in sorted order. (`na` is used if no argument is supplied).
// timezone						(string) A default timezone value for the data2d object. (`syminfo.timezone` is used if no argument is supplied).
export type data2d
	array<kv> kvs
	array<string> keys = na
	array<string> values = na
	array<string> formatValues = na
	array<string> altValues = na
	array<string> formatAltValues = na
	array<string> stringValues = na
	array<string> stringAltValues = na
	array<float> floatValues = na
	array<float> floatAltValues = na
	array<int> intValues = na
	array<int> intAltValues = na
	array<bool> boolValues = na
	array<bool> boolAltValues = na
	array<color> colorValues = na
	array<color> colorAltValues = na
	array<int> timestampValues = na
	array<int> timestampAltValues = na
	array<float> changeValues = na
	array<float> changePercentValues = na
	array<string> formatChangeValues = na
	array<string> formatChangePercentValues = na
	array<bool> etValues = na
	array<bool> ltValues = na
	array<bool> lteValues = na
	array<bool> gtValues = na
	array<bool> gteValues = na
	array<string> dataTypes = na
	string dataType = na
	int size = na
	bool sorted = na
	bool asc = true
	bool change = false
	string format = na
	string formatTime = "yyyy-MM-dd hh:mm:ss a z"
	string changeFormat = "{0,number,currency}"
	string changePercentFormat = "{0,number,0.00}%"
	string changeTimestampFormat = "year,month,week,day,hour,minute,second"
	array<int> index = na
	string timezone = syminfo.timezone
 
// }

// { Data3d:

// Define `data3d` - A three dimensional array of primarykey-datakey-value data structure.
// data						(array<data2d>) An array of `data2d` objects.
// primaryKeys				(array<string>) An array of primary key identifiers. (`na` is used if no argument is supplied). 
// dataKeys					(array<string>) An array of data key identifiers. (`na` is used if no argument is supplied). 
// dataKeyTypes 			(array<string>) An array of data types for each data keys. (`na` is used if no argument is supplied).
// dataType 				(string) The data type representing all data keys. Possible values are string, float, int, bool, color, timestamp, or mix. (`na` is used if no argument is supplied).
// size 					(int) Total size of the data3d object. (`na` is used if no argument is supplied).
// sorted					(bool) A boolean flag to determine if the data3d object is sorted or not.  (`na` is used if no argument is supplied).
// sortByKey				(string) The data key used for sorting the data3d object.  (`na` is used if no argument is supplied).
// asc						(bool) A boolean flag to determine if sorting in ascending or descending order. Use true for ascending, and false for descending. (`true` is used if no argument is supplied).
// change					(bool) A boolean flag to determine if sorting uses change percent values. (`false` is used if no argument is supplied).
// format					(string) A default string format for values. (`na` is used if no argument is supplied).
// formatTime				(string) A default time format for timestamp values. (`yyyy-MM-dd hh:mm:ss a z` is used if no argument is supplied).
// changeFormat				(string) A default string format targeting the change value between original and alt values. (`{0,number,currency}` is used if no argument is supplied).
// changePercentFormat		(string) A default string format targeting the change percent value between original and alt values. (`{0,number,0.00}%` is used if no argument is supplied).
// changeTimestampFormat	(string) A default string format targeting the change timestampr value between original and alt values. (`year,month,week,day,hour,minute,second` is used if no argument is supplied).
// index					(array<int>) An array of integer values representing primary key index. If the object is sorted, the index will be in sorted order. (`na` is used if no argument is supplied).
// timezone					(string) A default timezone value for the data3d object. (`syminfo.timezone` is used if no argument is supplied).
export type data3d
	array<data2d> data
	array<string> primaryKeys = na
	array<string> dataKeys = na
	array<string> dataKeyTypes = na
	string dataType = na
	int size = na
	bool sorted = na
	string sortByKey = na
	bool asc = true
	bool change = false
	string format = na
	string formatTime = "yyyy-MM-dd hh:mm:ss a z"
	string changeFormat = "{0,number,currency}"
	string changePercentFormat = "{0,number,0.00}%"
	string changeTimestampFormat = "year,month,week,day,hour,minute,second"
	array<int> index = na
	string timezone = syminfo.timezone

// }

// { Get keys and values: 

// @function For *INTERNAL* use only.
__keyExist(data2d this, string key) =>
	idx = this.keys.indexof(key)
	exist = false
	if idx != -1
		exist := true
	[idx, exist]
		
// @function For *INTERNAL* use only.
__dataKeyExist(data3d this, string key) =>
	idx = this.dataKeys.indexof(key)
	exist = false
	if idx != -1
		exist := true
	[idx, exist]

// @function For *INTERNAL* use only.
__primaryKeyExist(data3d this, string key) =>
	idx = this.primaryKeys.indexof(key)
	exist = false
	if idx != -1
		exist := true
	[idx, exist]

// @function For *INTERNAL* use only.
__existErrorCheck(bool exist, string key) =>
	if not exist
		runtime.error(str.format('Key "{0}" does not exist.', key))

// @function    Get all the keys. Maintains sort order, if applicable. 
// @param this	(data2d) The `data2d` object.
// @returns  	(array<string>) All keys of a data2d object.
export method getKeys(data2d this) =>
	keys = this.keys
	result = keys
	if this.sorted
		result := array.new<string>()
		for i in this.index
			result.push(keys.get(i))
	result

// @function    Get all the key types. Maintains sort order, if applicable. 
// @param this	(data2d) The `data2d` object.
// @returns  	(array<string>) All key types of a data2d object.
export method keyTypes(data2d this) =>
	types = this.dataTypes
	result = types
	if this.sorted
		result := array.new<string>()
		for i in this.index
			result.push(types.get(i))
	result

// @function    Get all the primary keys. Maintains sort order, if applicable.
// @param this	(data3d) The `data3d` object.
// @returns  	(array<string>) All primary keys of a data3d object.
export method primaryKeys(data3d this) =>
	pks = this.primaryKeys
	result = pks
	if this.sorted
		result := array.new<string>()
		for i in this.index
			result.push(pks.get(i))
	result

// @function    Get all the data keys.
// @param this	(data3d) The `data3d` object.
// @returns  	(array<string>) All data keys of a data3d object.
export method dataKeys(data3d this) =>
	this.dataKeys

// @function    Get all the data key types.
// @param this	(data3d) The `data3d` object.
// @returns  	(array<string>) All data key types of a data3d object.
export method dataKeyTypes(data3d this) =>
	this.dataKeyTypes

// @function    	Get all values in string representation. Formatted and sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<string>) All values from the data2d object.
export method values(data2d this, bool alt=false) =>
	vals = alt ? this.formatAltValues : this.formatValues
	result = vals
	if this.sorted
		result := array.new<string>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all values from the primary key in string representation. Formatted and sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<string>) Primary key values.
export method pkValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.formatAltValues : d2d.formatValues

// @function    	Get all values from the data key in string representation. Formatted, if applicable.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<string>) Data key values.
export method dkValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<string>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.formatAltValues.get(idx))
		else
			vals.push(d2d.formatValues.get(idx))
	vals

// @function    	Get all string values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<string>) All string values from the data2d object.
export method stringValues(data2d this, bool alt=false) =>
	vals = alt ? this.stringAltValues : this.stringValues
	result = vals
	if this.sorted
		result := array.new<string>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all string values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<string>) Primary key string values from the data3d object.
export method pkStringValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.stringAltValues : d2d.stringValues

// @function    	Get all string values from the data key.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<string>) Primary key string values from the data3d object.
export method dkStringValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<string>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.stringAltValues.get(idx))
		else
			vals.push(d2d.stringValues.get(idx))
	vals

// @function    	Get all float values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<float>) All float values from the data2d object.
export method floatValues(data2d this, bool alt=false) =>
	vals = alt ? this.floatAltValues : this.floatValues
	result = vals
	if this.sorted
		result := array.new<float>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all float values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<float>) Primary key float values from the data3d object.
export method pkFloatValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.floatAltValues : d2d.floatValues

// @function    	Get all float values from the data key.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<float>) Primary key float values from the data3d object.
export method dkFloatValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<float>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.floatAltValues.get(idx))
		else
			vals.push(d2d.floatValues.get(idx))
	vals

// @function    	Get all integer values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<int>) All integer values from the data2d object.
export method intValues(data2d this, bool alt=false) =>
	vals = alt ? this.intAltValues : this.intValues
	result = vals
	if this.sorted
		result := array.new<int>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all integer values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<int>) Primary key integer values from the data3d object.
export method pkIntValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.intAltValues : d2d.intValues

// @function    	Get all integer values from the data key.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<int>) Primary key integer values from the data3d object.
export method dkIntValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<int>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.intAltValues.get(idx))
		else
			vals.push(d2d.intValues.get(idx))
	vals

// @function    	Get all boolean values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<bool>) All boolean values from the data2d object.
export method boolValues(data2d this, bool alt=false) =>
	alt ? this.boolAltValues : this.boolValues

// @function    		Get all boolean values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<bool>) Primary key boolean values from the data3d object.
export method pkBoolValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.boolAltValues : d2d.boolValues

// @function    	Get all boolean values from the data key.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<bool>) Primary key boolean values from the data3d object.
export method dkBoolValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<bool>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.boolAltValues.get(idx))
		else
			vals.push(d2d.boolValues.get(idx))
	vals

// @function    	Get all color values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<color>) All color values from the data2d object.
export method colorValues(data2d this, bool alt=false) =>
	alt ? this.colorAltValues : this.colorValues

// @function    		Get all color values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<color>) Primary key color values from the data3d object.
export method pkColorValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.colorAltValues : d2d.colorValues

// @function    	Get all color values from the data key.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<color>) Primary key color values from the data3d object.
export method dkColorValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<color>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.colorAltValues.get(idx))
		else
			vals.push(d2d.colorValues.get(idx))
	vals

// @function    	Get all unix timestamp values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<int>) All Unix timestamp values from the data2d object.
export method timestampValues(data2d this, bool alt=false) =>
	vals = alt ? this.timestampAltValues : this.timestampValues
	result = vals
	if this.sorted
		result := array.new<int>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all unix timestamp values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param alt			(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  			(array<int>) Primary key timestamp values from the data3d object.
export method pkTimestampValues(data3d this, string primaryKey, bool alt=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	alt ? d2d.timestampAltValues : d2d.timestampValues

// @function    	Get all unix timestamp values from the data key.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param alt		(bool) Set to true to obtain the alternate values instead. (`false` is used if no argument is supplied).
// @returns  		(array<int>) Primary key timestamp values from the data3d object.
export method dkTimestampValues(data3d this, string dataKey, bool alt=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<int>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		if alt
			vals.push(d2d.timestampAltValues.get(idx))
		else
			vals.push(d2d.timestampValues.get(idx))
	vals

// }

// { Get, getString, getFloat, getInt, getBool, getColor, getTimestamp

// @function    	Get string value from a specific key, formatted if applicable.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(string) A string value or `na` if not found.
export method get(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.formatAltValues.get(idx) : this.formatValues.get(idx)

// @function    		Get string value from a specific primary and data key, formatted if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(string) A string value or `na` if not found.
export method get(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.formatAltValues.get(dkIdx) : d2d.formatValues.get(dkIdx)

// @function    	Get string value from a specific key.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(string) A string value or `na` if not found.
export method getString(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.stringAltValues.get(idx) : this.stringValues.get(idx)

// @function    		Get string value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(string) A string value or `na` if not found.
export method getString(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.stringAltValues.get(dkIdx) : d2d.stringValues.get(dkIdx)

// @function    	Get float value from a specific key.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(float) A float value or `na` if not found.
export method getFloat(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.floatAltValues.get(idx) : this.floatValues.get(idx)

// @function    		Get float value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(float) A float value or `na` if not found.
export method getFloat(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.floatAltValues.get(dkIdx) : d2d.floatValues.get(dkIdx)

// @function    	Get integer value from a specific key.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(int) An integer value or `na` if not found.
export method getInt(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.intAltValues.get(idx) : this.intValues.get(idx)

// @function    		Get integer value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(int) An integer value or `na` if not found.
export method getInt(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.intAltValues.get(dkIdx) : d2d.intValues.get(dkIdx)

// @function    	Get boolean value from a specific key.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(bool) A bool value or `na` if not found.
export method getBool(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.boolAltValues.get(idx) : this.boolValues.get(idx)

// @function    		Get boolean value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(bool) A boolean value or `na` if not found.
export method getBool(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.boolAltValues.get(dkIdx) : d2d.boolValues.get(dkIdx)

// @function    	Get color value from a specific key.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(color) A color value or `na` if not found.
export method getColor(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.colorAltValues.get(idx) : this.colorValues.get(idx)

// @function    		Get color value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(color) A color value or `na` if not found.
export method getColor(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.colorAltValues.get(dkIdx) : d2d.colorValues.get(dkIdx)

// @function    	Get unix timestamp value from a specific key.
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param alt		(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  		(int) A unix timestamp value or `na` if not found.
export method getTimestamp(data2d this, string key, bool alt=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	alt ? this.timestampAltValues.get(idx) : this.timestampValues.get(idx)

// @function    		Get unix timestamp value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param alt			(bool) Use true to get the alternate value instead. (`false` is used if no argument is supplied).
// @returns  			(int) A unix timestamp value or `na` if not found.
export method getTimestamp(data3d this, string primaryKey, string dataKey, bool alt=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	alt ? d2d.timestampAltValues.get(dkIdx) : d2d.timestampValues.get(dkIdx)

// }

// { Comparison: 

__getTimestampChangeF(data2d this, int idx, bool years, bool months, bool weeks, bool days, bool hours, bool minutes, bool seconds) =>
	// @function For *INTERNAL* use only.
	keyType = this.kvs.get(idx).dataType
	if keyType != 'timestamp'
		runtime.error("Data type is not a (int) unix timestamp. Only (int) 'timestamp' data type allowed.")
	tools._getTimeDifference(this.timestampValues.get(idx), this.timestampAltValues.get(idx), years, months, weeks, days, hours, minutes, seconds)

__getTimestampChangeF(data2d this, bool years, bool months, bool weeks, bool days, bool hours, bool minutes, bool seconds) =>
	// @function For *INTERNAL* use only.
	result = array.new<string>()
	for key in this.getKeys()
		idx = this.keys.indexof(key)
		result.push(__getTimestampChangeF(this, idx, years, months, weeks, days, hours, minutes, seconds))
	result

// @function    		Get segmented time difference value of a single key.
// @param this			(data2d) The `data2d` object.
// @param key			(string) The key identifier.
// @param years			Switch to turn on/off the years segment. Off by default. (`false` is used if no argument is supplied).
// @param months		Switch to turn on/off the months segment. Off by default. (`false` is used if no argument is supplied).
// @param weeks			Switch to turn on/off the weeks segment. Off by default. (`false` is used if no argument is supplied).
// @param days			Switch to turn on/off the days segment. On by default. (`true` is used if no argument is supplied).
// @param hours			Switch to turn on/off the hours segment. On by default. (`true` is used if no argument is supplied).
// @param minutes		Switch to turn on/off the minutes segment. On by default. (`true` is used if no argument is supplied).
// @param seconds		Switch to turn on/off the seconds segment. On by default. (`true` is used if no argument is supplied).
// @returns  			(string) A string value.
export method timestampChange(data2d this, string key, bool years=false, bool months=false, bool weeks=false, bool days=true, bool hours=true, bool minutes=true, bool seconds=true) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	__getTimestampChangeF(this, idx, years, months, weeks, days, hours, minutes, seconds)

// @function    		Get segmented time difference value of all the keys.
// @param this			(data2d) The `data2d` object.
// @param key			(string) The key identifier.
// @param years			Switch to turn on/off the years segment. Off by default. (`false` is used if no argument is supplied).
// @param months		Switch to turn on/off the months segment. Off by default. (`false` is used if no argument is supplied).
// @param weeks			Switch to turn on/off the weeks segment. Off by default. (`false` is used if no argument is supplied).
// @param days			Switch to turn on/off the days segment. On by default. (`true` is used if no argument is supplied).
// @param hours			Switch to turn on/off the hours segment. On by default. (`true` is used if no argument is supplied).
// @param minutes		Switch to turn on/off the minutes segment. On by default. (`true` is used if no argument is supplied).
// @param seconds		Switch to turn on/off the seconds segment. On by default. (`true` is used if no argument is supplied).
// @returns  			(array<string>) A string array.
export method timestampChange(data2d this, bool years=false, bool months=false, bool weeks=false, bool days=true, bool hours=true, bool minutes=true, bool seconds=true) =>
	__getTimestampChangeF(this, years, months, weeks, days, hours, minutes, seconds)

// @function    		Get segmented time difference value of a single primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param years			Switch to turn on/off the years segment. Off by default. (`false` is used if no argument is supplied).
// @param months		Switch to turn on/off the months segment. Off by default. (`false` is used if no argument is supplied).
// @param weeks			Switch to turn on/off the weeks segment. Off by default. (`false` is used if no argument is supplied).
// @param days			Switch to turn on/off the days segment. On by default. (`true` is used if no argument is supplied).
// @param hours			Switch to turn on/off the hours segment. On by default. (`true` is used if no argument is supplied).
// @param minutes		Switch to turn on/off the minutes segment. On by default. (`true` is used if no argument is supplied).
// @param seconds		Switch to turn on/off the seconds segment. On by default. (`true` is used if no argument is supplied).
// @returns  			(string) A string value.
export method timestampChange(data3d this, string primaryKey, string dataKey, bool years=false, bool months=false, bool weeks=false, bool days=true, bool hours=true, bool minutes=true, bool seconds=true) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	__getTimestampChangeF(d2d, dkIdx, years, months, weeks, days, hours, minutes, seconds)

// @function    		Get segmented time difference value of all the data keys for the primary key. (main == alt)
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param years			Switch to turn on/off the years segment. Off by default. (`false` is used if no argument is supplied).
// @param months		Switch to turn on/off the months segment. Off by default. (`false` is used if no argument is supplied).
// @param weeks			Switch to turn on/off the weeks segment. Off by default. (`false` is used if no argument is supplied).
// @param days			Switch to turn on/off the days segment. On by default. (`true` is used if no argument is supplied).
// @param hours			Switch to turn on/off the hours segment. On by default. (`true` is used if no argument is supplied).
// @param minutes		Switch to turn on/off the minutes segment. On by default. (`true` is used if no argument is supplied).
// @param seconds		Switch to turn on/off the seconds segment. On by default. (`true` is used if no argument is supplied).
// @returns  			(array<string>) A string value.
export method pkTimestampChange(data3d this, string primaryKey, bool years=false, bool months=false, bool weeks=false, bool days=true, bool hours=true, bool minutes=true, bool seconds=true) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	d2d = this.data.get(pkIdx)
	__getTimestampChangeF(d2d, years, months, weeks, days, hours, minutes, seconds)

// @function    		Get segmented time difference value of all the primary keys for the data key. (main == alt)
// @param this			(data3d) The `data3d` object.
// @param dataKey		(string) The data key.
// @param years			Switch to turn on/off the years segment. Off by default. (`false` is used if no argument is supplied).
// @param months		Switch to turn on/off the months segment. Off by default. (`false` is used if no argument is supplied).
// @param weeks			Switch to turn on/off the weeks segment. Off by default. (`false` is used if no argument is supplied).
// @param days			Switch to turn on/off the days segment. On by default. (`true` is used if no argument is supplied).
// @param hours			Switch to turn on/off the hours segment. On by default. (`true` is used if no argument is supplied).
// @param minutes		Switch to turn on/off the minutes segment. On by default. (`true` is used if no argument is supplied).
// @param seconds		Switch to turn on/off the seconds segment. On by default. (`true` is used if no argument is supplied).
// @returns  			(array<string>) A string value.
export method dkTimestampChange(data3d this, string dataKey, bool years=false, bool months=false, bool weeks=false, bool days=true, bool hours=true, bool minutes=true, bool seconds=true) =>
	result = array.new<string>()
	for pk in this.primaryKeys()
		result.push(this.timestampChange(pk, dataKey, years, months, weeks, days, hours, minutes, seconds))
	result

// @function    	Get all change values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param percent	(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  		(array<float>) All values from the data2d object.
export method changeValues(data2d this, bool percent=false) =>
	vals = percent ? this.changePercentValues : this.changeValues
	result = vals
	if this.sorted
		result := array.new<float>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all change values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param percent		(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  			(array<float>) Primary key values.
export method pkChangeValues(data3d this, string primaryKey, bool percent=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	percent ? d2d.changePercentValues : d2d.changeValues

// @function    	Get all change values from the data key. Formatted, if applicable.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param percent	(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  		(array<float>) Data key values.
export method dkChangeValues(data3d this, string dataKey, bool percent=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<float>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		vals.push(percent ? d2d.changePercentValues.get(idx) : d2d.changeValues.get(idx))
	vals

// @function    	Get change value from a specific key..
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param percent	(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  		(float) A change value or `na` if not found.
export method getChange(data2d this, string key, bool percent=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	percent ? this.changePercentValues.get(idx) : this.changeValues.get(idx)

// @function    		Get change value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param percent		(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  			(float) A change value or `na` if not found.
export method getChange(data3d this, string primaryKey, string dataKey, bool percent=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	percent ? d2d.changePercentValues.get(dkIdx) : d2d.changeValues.get(dkIdx)

// @function    	Get all formatted change values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @param percent	(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  		(array<string>) All values from the data2d object.
export method formatChangeValues(data2d this, bool percent=false) =>
	vals = percent ? this.formatChangePercentValues : this.formatChangeValues
	result = vals
	if this.sorted
		result := array.new<string>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all formatted change values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param percent		(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  			(array<string>) Primary key values.
export method pkFormatChangeValues(data3d this, string primaryKey, bool percent=false) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	percent ? d2d.formatChangePercentValues : d2d.formatChangeValues

// @function    	Get all formatted change values from the data key. Formatted, if applicable.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @param percent	(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  		(array<string>) Data key values.
export method dkFormatChangeValues(data3d this, string dataKey, bool percent=false) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<string>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		vals.push(percent ? d2d.formatChangePercentValues.get(idx) : d2d.formatChangeValues.get(idx))
	vals

// @function    	Get formatted change value from a specific key..
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @param percent	(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  		(string) A change value or `na` if not found.
export method getFormatChange(data2d this, string key, bool percent=false) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	percent ? this.formatChangePercentValues.get(idx) : this.formatChangeValues.get(idx)

// @function    		Get formatted change value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @param percent		(bool) Set to true to obtain the percent change values instead. (`false` is used if no argument is supplied).
// @returns  			(string) A change value or `na` if not found.
export method getFormatChange(data3d this, string primaryKey, string dataKey, bool percent=false) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	percent ? d2d.formatChangePercentValues.get(dkIdx) : d2d.formatChangeValues.get(dkIdx)


// @function    	Get all 'et' compare values. Sorted, if applicable.
// @param this		(data2d) The `data2d` object.
// @returns  		(array<bool>) All values from the data2d object.
export method compareValues(data2d this, string compare) =>
	vals = switch compare
		'et' => this.etValues
		'lt' => this.ltValues
		'lte' => this.lteValues
		'gt' => this.gtValues
		'gte' => this.gteValues
	result = vals
	if this.sorted
		result := array.new<bool>()
		for i in this.index
			result.push(vals.get(i))
	result

// @function    		Get all 'et' compare values from the primary key. Sorted, if applicable.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @returns  			(array<bool>) Primary key values.
export method pkCompareValues(data3d this, string primaryKey, string compare) =>
	[idx, exist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(exist, primaryKey)
	d2d = this.data.get(idx)
	vals = switch compare
		'et' => d2d.etValues
		'lt' => d2d.ltValues
		'lte' => d2d.lteValues
		'gt' => d2d.gtValues
		'gte' => d2d.gteValues

// @function    	Get all 'et' compare values from the data key. Formatted, if applicable.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(string) The data key.
// @returns  		(array<bool>) Data key values.
export method dkCompareValues(data3d this, string dataKey, string compare) =>
	[idx, exist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(exist, dataKey)
	vals = array.new<bool>()
	d3dIdx = this.index
	for _idx in d3dIdx
		d2d = this.data.get(_idx)
		_val = switch compare
			'et' => d2d.etValues.get(idx)              
			'lt' => d2d.ltValues.get(idx)
			'lte' => d2d.lteValues.get(idx)
			'gt' => d2d.gtValues.get(idx)
			'gte' => d2d.gteValues.get(idx)
		vals.push(_val)
	vals

// @function    	Get 'et' compare value from a specific key..
// @param this		(data2d) The `data2d` object.
// @param key		(string) The key identifier.
// @returns  		(bool) A compare value or `na` if not found.
export method getCompare(data2d this, string key, string compare) =>
	[idx, exist] = __keyExist(this, key)
	__existErrorCheck(exist, key)
	switch compare
		'et' => this.etValues.get(idx)
		'lt' => this.ltValues.get(idx)
		'lte' => this.lteValues.get(idx)
		'gt' => this.gtValues.get(idx)
		'gte' => this.gteValues.get(idx)

// @function    		Get 'et' compare value from a specific primary and data key.
// @param this			(data3d) The `data3d` object.
// @param primaryKey	(string) The primary key.
// @param dataKey		(string) The data key.
// @returns  			(bool) A compare value or `na` if not found.
export method getCompare(data3d this, string primaryKey, string dataKey, string compare) =>
	[pkIdx, pkExist] = __primaryKeyExist(this, primaryKey)
	__existErrorCheck(pkExist, primaryKey)
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	d2d = this.data.get(pkIdx)
	switch compare
		'et' => d2d.etValues.get(dkIdx)
		'lt' => d2d.ltValues.get(dkIdx)
		'lte' => d2d.lteValues.get(dkIdx)
		'gt' => d2d.gtValues.get(dkIdx)
		'gte' => d2d.gteValues.get(dkIdx)

// }

// { Mechanics: 

// @function    	Set custom format for a `kv` object.
// @param this		(kv) A `kv` object.
// @param format	(string) A custom format.
// @param timezone	(string) A timezone for timestamp value. (`na` is used if no argument is supplied).
// @returns  		(kv) The `kv` object.
export method format(kv this, string format, string timezone=na) =>
	this.format := format
	alt = tools._bool(this.formatAltValue)
	string fv = na
	string afv = na

	switch this.dataType
		"float" =>
			fv := str.format(format, this.val.floatValue)
			afv := alt ? str.format(format, this.altVal.floatValue) : afv
		"int" =>
			fv := str.format(format, this.val.intValue)
			afv := alt ? str.format(format, this.altVal.intValue) : afv
		"timestamp" =>
			tz = syminfo.timezone
			if not na(timezone)
				this.timezone := timezone
				this._tz := true
				tz := timezone
			else
				tz := this._tz ? this.timezone : tz
			fv := str.format_time(this.val.intValue, format, tz)
			afv := alt ? str.format_time(this.altVal.intValue, format, tz) : afv
		"bool" =>
			fv := str.format(format, this.val.boolValue)
			afv := alt ? str.format(format, this.altVal.boolValue) : afv
		=>
			fv := str.format(format, this.val.stringValue)
			afv := alt ? str.format(format, this.altVal.stringValue) : afv
	
	this.formatValue := fv
	this.formatAltValue := afv
	this

// @function    	Set custom change format or change percent format for a `kv` object.
// @param this		(kv) A `kv` object.
// @param format	(string) A custom format.
// @param percent	(bool) If set to true, the format will be used for change percent, rather than change. (`false` is used if no argument is supplied).
// @returns  		(kv) The `kv` object.
export method changeFormat(kv this, string format, bool percent=false) =>
	this.changePercentFormat := percent ? format : this.changePercentFormat
	this.changeFormat := not percent ? format : this.changeFormat
	if tools._bool(this.formatAltValue)			
		switch this.dataType
			"float" =>
				if percent
					this.formatChangePercentValue := str.format(format, __change(this.val.floatValue, this.altVal.floatValue, true))
				else
					this.formatChangeValue := str.format(format, __change(this.val.floatValue, this.altVal.floatValue, false))
			"int" =>
				if percent
					this.formatChangePercentValue := str.format(format, __change(this.val.intValue, this.altVal.intValue, true))
				else
					this.formatChangeValue := str.format(format, __change(this.val.intValue, this.altVal.intValue, false))
			"timestamp" =>
				if percent
					this.formatChangePercentValue := str.format(format, __change(this.val.intValue, this.altVal.intValue, true))
				else
					_cfKeys = tools._commaSplit(this.changeFormat)
					years = _cfKeys.includes('year')
					months = _cfKeys.includes('month') 
					weeks = _cfKeys.includes('week')
					days = _cfKeys.includes('day')
					hours = _cfKeys.includes('hour') 
					minutes = _cfKeys.includes('minute')
					seconds = _cfKeys.includes('second')
					this.formatChangeValue := tools._getTimeDifference(this.val.intValue, this.altVal.intValue, years, months, weeks, days, hours, minutes, seconds)
			"string" =>
				if percent
					this.formatChangePercentValue := str.format(format, __change(str.length(this.val.stringValue), str.length(this.altVal.stringValue), true))
				else
					this.formatChangeValue := str.format(format, __change(str.length(this.val.stringValue), str.length(this.altVal.stringValue), false))
	this

// @function For *INTERNAL* use only.
__format(multidata, string format) =>
	switch multidata.dataType
		"timestamp" => 
			multidata.formatTime := format
		=> 
			multidata.format := format
	multidata

// @function    	Set custom format for a multidata object.
// @param this		A data2d or data3d object.
// @param format	(string) A custom format.
// @returns  		This object.
export method format(data2d this, string format) =>
	__format(this, format)
	frmtVals = this.formatValues
	frmtAltVals = this.formatAltValues
	for [i,kV] in this.kvs
		if na(kV.format)
			tz = kV._tz ? kV.timezone : this.timezone
			kV.format(format, tz)
		frmtVals.set(i, kV.formatValue)
		frmtAltVals.set(i, kV.formatAltValue)
	this

export method format(data3d this, string format) =>
	__format(this, format)
	for data in this.data
		data.format(format)
	this

// @function    	Set custom change format or change percent format for a multidata object.
// @param this		A data2d or data3d object.
// @param format	(string) A custom format.
// @param percent	(bool) If set to true, the format will be used for change percent, rather than change. (`false` is used if no argument is supplied).
// @returns  		This object.
export method changeFormat(data2d this, string format, bool percent=false) =>
	this.changePercentFormat := percent ? format : this.changePercentFormat
	this.changeFormat := not percent ? format : this.changeFormat
	frmtChangeVals = this.formatChangeValues
	frmtChangePercentVals = this.formatChangePercentValues
	for [i,kV] in this.kvs
		if kV.dataType != 'timestamp'
			if percent and na(kV.changePercentFormat)
				kV.changeFormat(format, true)
			else if not percent and na(kV.changePercentFormat)
				kV.changeFormat(format, false)
			frmtChangeVals.set(i, kV.formatChangeValue)
			frmtChangePercentVals.set(i, kV.formatChangePercentValue)
	this

export method changeFormat(data3d this, string format, bool percent=false) =>
	for data in this.data
		data.changeFormat(format, percent)
	this

// @function    	Set custom change timestamp format for a multidata object.
// @param this		A data2d or data3d object.
// @param format	(string) A custom format.
// @returns  		This object.
export method changeTimestampFormat(data2d this, string format) =>
	this.changeTimestampFormat := format
	frmtChangeVals = this.formatChangeValues
	for [i,kV] in this.kvs
		if kV.dataType == 'timestamp'
			kV.changeFormat(format, false)
			frmtChangeVals.set(i, kV.formatChangeValue)
	this

export method changeTimestampFormat(data3d this, string format) =>
	for data in this.data
		data.changeFormat(format)
	this


// @function For *INTERNAL* use only.
__index(multidata) =>
	size = multidata.size
	index = array.new<int>(size)
	for i=0 to size - 1
		index.set(i, i)
	multidata.index := index

// @function For *INTERNAL* use only.
__sort2d(data2d this, order, bool change) =>
	if this.dataType != "mix"
		if change
			this.index := this.changeValues(true).sort_indices(order)
		else
			switch this.dataType
				"float" => 
					this.index := this.floatValues.sort_indices(order)
				"int" => 
					this.index := this.intValues.sort_indices(order)
				"timestamp" => 
					this.index := this.timestampValues.sort_indices(order)
				=> 
					this.index := this.values.sort_indices(order)
		this.sorted := true
	this

// @function For *INTERNAL* use only.
__sort3d(data3d this, string dataKey, order, bool change) =>
	[dkIdx, dkExist] = __dataKeyExist(this, dataKey)
	__existErrorCheck(dkExist, dataKey)
	dataType = this.dataKeyTypes.get(dkIdx)
	if change
		changeValues = array.new<float>()
		for d2d in this.data
			changeValues.push(d2d.changeValues(true).get(dkIdx))
		this.index := changeValues.sort_indices(order)
	else
		switch dataType
			"float" => 
				floatValues = array.new<float>()
				for d2d in this.data
					floatValues.push(d2d.floatValues.get(dkIdx))
				this.index := floatValues.sort_indices(order)
			"int" =>
				intValues = array.new<int>()
				for d2d in this.data
					intValues.push(d2d.intValues.get(dkIdx))
				this.index := intValues.sort_indices(order)
			"timestamp" =>
				timestampValues = array.new<int>()
				for d2d in this.data
					timestampValues.push(d2d.timestampValues.get(dkIdx))
				this.index := timestampValues.sort_indices(order)
			=>
				values = array.new<string>()
				for d2d in this.data
					values.push(d2d.values.get(dkIdx))
				this.index := values.sort_indices(order)
	this.sorted := true
	this

// @function    	Sort a `data2d` object.
// @param this		(data2d) The `data2d` object.
// @param asc		(bool) Use true for ascending order, false for Descending order.
// @param change	(bool) Sorts using change percent if true. (`na` is used if no argument is supplied).
// @returns  		(data2d) The `data2d` object.
export method sort(data2d this, bool asc, bool change = na) =>
	order = asc ? order.ascending : order.descending
	this.asc := asc
	this.change := not na(change) ? change : this.change
	__sort2d(this, order, this.change)

// @function    	Sort a `data2d` object using it's default sorting order.
// @param this		(data2d) The `data2d` object.
// @param change	(bool) Sorts using change percent if true. (`na` is used if no argument is supplied).
// @returns  		(data2d) The `data2d` object.
export method sort(data2d this, bool change = na) =>
	order = this.asc ? order.ascending : order.descending
	this.change := not na(change) ? change : this.change
	__sort2d(this, order, this.change)

// @function    	Sort a `data3d` object.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(dataKey) The data key values used for sorting.
// @param asc		(bool) Use true for ascending order, false for Descending order.
// @param change	(bool) Sorts using change percent if true. (`na` is used if no argument is supplied).
// @returns  		(data3d) The `data3d` object.
export method sort(data3d this, string dataKey, bool asc, bool change = na) =>
	order = asc ? order.ascending : order.descending
	this.asc := asc
	this.change := not na(change) ? change : this.change
	__sort3d(this, dataKey, order, this.change)
	
// @function    	Sort a `data3d` object using it's default sorting order.
// @param this		(data3d) The `data3d` object.
// @param dataKey	(dataKey) The data key values used for sorting.
// @param change	(bool) Sorts using change percent if true. (`na` is used if no argument is supplied).
// @returns  		(data3d) The `data3d` object.
export method sort(data3d this, string dataKey, bool change = na) =>
	order = this.asc ? order.ascending : order.descending
	this.change := not na(change) ? change : this.change
	__sort3d(this, dataKey, order, this.change)

// @function For *INTERNAL* use only.
__init2d(data2d this, bool sort=false, bool asc=true, bool change=na) =>
	kvs = this.kvs
	size = kvs.size()
	this.size := size
	this.asc := asc
	this.change := not na(change) ? change : this.change
	vt = kvs.first().dataType
	countDiffValType = 0
	keys = array.new<string>(size)
	vals = array.new<string>(size)
	frmtVals = array.new<string>(size)
	altVals = array.new<string>(size)
	frmtAltVals = array.new<string>(size)
	stringValues = array.new<string>(size)
	stringAltVals = array.new<string>(size)
	floatValues = array.new<float>(size)
	floatAltVals = array.new<float>(size)
	intValues = array.new<int>(size)
	intAltVals = array.new<int>(size)
	boolValues = array.new<bool>(size)
	boolAltVals = array.new<bool>(size)
	colorVals = array.new<color>(size)
	colorAltVals = array.new<color>(size)
	timestampVals = array.new<int>(size)
	timestampAltVals = array.new<int>(size)
	changeValues = array.new<float>(size)
	changePercentValues = array.new<float>(size)
	formatChangeValues = array.new<string>(size)
	formatChangePercentValues = array.new<string>(size)
	etValues = array.new<bool>(size)
	ltValues = array.new<bool>(size)
	lteValues = array.new<bool>(size)
	gtValues = array.new<bool>(size)
	gteValues = array.new<bool>(size)
	dataTypes = array.new<string>(size)
	
	__index(this)
	
	for [i, kV] in kvs
		key = kV.key
		if keys.includes(key)
			msg = str.format("Key \"{0}\" already exist. Keys must be unique.", key)
			runtime.error(msg)
		countDiffValType := kV.dataType != vt ? countDiffValType + 1 : countDiffValType
		keys.set(i, key)
		vals.set(i, kV.value)
		frmtVals.set(i, kV.formatValue)
		altVals.set(i, kV.altValue)
		frmtAltVals.set(i, kV.formatAltValue)
		dataTypes.set(i, kV.dataType)
		changeValues.set(i, kV.changeValue)
		changePercentValues.set(i, kV.changePercentValue)
		formatChangeValues.set(i, kV.formatChangeValue)
		formatChangePercentValues.set(i, kV.formatChangePercentValue)
		etValues.set(i, kV.etValue)
		ltValues.set(i, kV.ltValue)
		lteValues.set(i, kV.lteValue)
		gtValues.set(i, kV.gtValue)
		gteValues.set(i, kV.gteValue)
		if kV.dataType == "string"
			stringValues.set(i, kV.val.stringValue)
			stringAltVals.set(i, kV.altVal.stringValue)
		else if kV.dataType == "float"
			floatValues.set(i, kV.val.floatValue)
			floatAltVals.set(i, kV.altVal.floatValue)
		else if kV.dataType == "int"
			intValues.set(i, kV.val.intValue)
			intAltVals.set(i, kV.altVal.intValue)
		else if kV.dataType == "bool"
			boolValues.set(i, kV.val.boolValue)
			boolAltVals.set(i, kV.altVal.boolValue)
		else if kV.dataType == "color"
			colorVals.set(i, tools._clr(kV.val.stringValue))
			colorAltVals.set(i, tools._clr(kV.altVal.stringValue))
		else if kV.dataType == "timestamp"
			timestampVals.set(i, kV.val.intValue)
			timestampAltVals.set(i, kV.altVal.intValue)
			if na(kV.format)
				tz = kV._tz ? kV.timezone : this.timezone
				frmtTimeVal = str.format_time(kV.val.intValue, this.formatTime, tz)
				frmtVals.set(i, frmtTimeVal)
				if not na(kV.altValue)
					frmtTimeAltVal = str.format_time(kV.altVal.intValue, this.formatTime, tz)
					frmtAltVals.set(i, frmtTimeAltVal)

	this.dataTypes := dataTypes
	this.dataType := countDiffValType > 0 ? "mix" : vt
	this.keys := keys
	this.values := vals
	this.formatValues := frmtVals
	this.altValues := altVals
	this.formatAltValues := frmtAltVals
	this.stringValues := stringValues
	this.stringAltValues := stringAltVals
	this.floatValues := floatValues
	this.floatAltValues := floatAltVals
	this.intValues := intValues
	this.intAltValues := intAltVals
	this.boolValues := boolValues
	this.boolAltValues := boolAltVals
	this.colorValues := colorVals
	this.colorAltValues := colorAltVals
	this.timestampValues := timestampVals
	this.timestampAltValues := timestampAltVals
	this.changeValues := changeValues
	this.changePercentValues := changePercentValues
	this.formatChangeValues := formatChangeValues
	this.formatChangePercentValues := formatChangePercentValues
	this.etValues := etValues
	this.ltValues := ltValues
	this.lteValues := lteValues
	this.gtValues := gtValues
	this.gteValues := gteValues

	if sort
		__sort2d(this, asc ? order.ascending : order.descending, change)
	this

// @function    		Create and initialize a data2d object.
// @param kvs			(array<kv> ) An array of `kv` objects.
// @param sort			(bool) Use true for sorting, false otherwise. (`false` is used if no argument is supplied).
// @param asc			(bool) Use true for sort order.ascending, false for sort order.descending. (`true` is used if no argument is supplied).
// @param sortByChange	(bool) Sorts using change percent if true. (`na` is used if no argument is supplied).
// @param format		(string) The default format. (`na` is used if no argument is supplied).
// @param timezone		(string) The default timezone. (`syminfo.timezone` is used if no argument is supplied).
// @returns  			(data2d) The `data2d` object.
export data2d(array<kv> kvs, bool sort=false, bool asc=true, bool sortByChange=na, string format=na, string timezone=na) =>
	d2d = data2d.new(kvs=kvs)
	d2d.timezone := not na(timezone) ? timezone : d2d.timezone
	__init2d(d2d, sort, asc, sortByChange)
	if not na(format)
		d2d.format(format)
	d2d

// @function    		Create a data3d object.
// @param pkvs			(array<pkv> ) An array of `pkv` objects.
// @param sort			(bool) Use true for sorting, false otherwise. (`false` is used if no argument is supplied).
// @param sortByKey		(string) The data key used for sorting. (`na` is used if no argument is supplied).
// @param asc			(bool) Use true for sort order.ascending, false for sort order.descending. (`true` is used if no argument is supplied).
// @param sortByChange	(bool) Sorts using change percent if true. (`na` is used if no argument is supplied).
// @param format		(string) The default format. (`na` is used if no argument is supplied).
// @param timezone		(string) The default timezone. (`syminfo.timezone` is used if no argument is supplied).
// @returns  			(data2d) The `data2d` object.
export data3d(array<pkv> pkvs, bool sort=false, string sortByKey=na, bool asc=true, bool sortByChange=na, string format=na, string timezone=na) =>
	primaryKeys = array.new<string>()
	dataKeys = pkvs.first().dataKeys
	dataKeyTypes = pkvs.first().dataKeyTypes
	data = array.new<data2d>()
	string dataType = na
	// For matching all other data keys and their types
	flatDks = tools._join(dataKeys)
	for [i, pKv] in pkvs
		for [x,dkt] in dataKeyTypes
			if dkt != pKv.dataKeyTypes.get(x)
				runtime.error("Data key type does not match. Please make sure data key types match across all `pkv` objects.")
		d2d = data2d(pKv.kvs, format=format, timezone=timezone)
		if flatDks != tools._join(d2d.keys)
			runtime.error("Data keys does not match. Please make sure data keys match across all `pkv` objects.")
		primaryKeys.push(pKv.primaryKey)
		data.push(d2d)
		if i == 0
			dataType := d2d.dataType
	d3d = data3d.new(
			  data=data,
			  primaryKeys=primaryKeys,
			  dataKeys=dataKeys,
			  dataKeyTypes=dataKeyTypes,
			  dataType=dataType,
			  size=pkvs.size(),
			  sorted=sort,
			  sortByKey=sortByKey,
			  asc=asc)
	d3d.change := not na(sortByChange) ? sortByChange : d3d.change
	d3d.index := __index(d3d)
	d3d.timezone := not na(timezone) ? timezone : d3d.timezone
	d3d.formatTime := not na(format) and dataType == "timestamp" ? format : d3d.formatTime
	d3d.format := not na(format) and dataType != "timestamp" ? format : d3d.format
	if not na(sortByKey) and sort
		__sort3d(d3d, sortByKey, asc ? order.ascending : order.descending, d3d.change)
	d3d

// }

// { Printer Extend:

_rawValue(data2d data2d, key, _type) =>
	switch _type
		'string' => p.kdv(key, data2d.getString(key))
		'float' => p.kdv(key, data2d.getFloat(key))
		'int' => p.kdv(key, data2d.getInt(key))
		'bool' => p.kdv(key, data2d.getBool(key))
		'color' => p.kdv(key, data2d.getColor(key))
		'timestamp' => p.kdv(key, data2d.getTimestamp(key))

_rawValue(data3d data3d, pk, dk, _type) =>
	switch _type
		'string' => p.kdv(dk, data3d.getString(pk, dk))
		'float' => p.kdv(dk, data3d.getFloat(pk, dk))
		'int' => p.kdv(dk, data3d.getInt(pk, dk))
		'bool' => p.kdv(dk, data3d.getBool(pk, dk))
		'color' => p.kdv(dk, data3d.getColor(pk, dk))
		'timestamp' => p.kdv(dk, data3d.getTimestamp(pk, dk))

__displayValues(data2d data2d, string name) =>
	_vals = array.new<p.kdv>()
	for [i, key] in data2d.getKeys()
		switch name
			'alt' => 
				_val = data2d.get(key, true)
				_vals.push(p.kdv(key, _val)) 
			'change' =>
				_val = data2d.getChange(key)
				_vals.push(p.kdv(key, _val)) 
			'change_format' =>
				_val = data2d.getFormatChange(key)
				_vals.push(p.kdv(key, _val)) 
			'change_percent' =>
				_val = data2d.getChange(key, true)
				_vals.push(p.kdv(key, _val)) 
			'change_percent_format' =>
				_val = data2d.getFormatChange(key, true)
				_vals.push(p.kdv(key, _val)) 
			'et' =>
				_val = data2d.getCompare(key, 'et')
				_vals.push(p.kdv(key, _val)) 
			'lt' =>
				_val = data2d.getCompare(key, 'lt')
				_vals.push(p.kdv(key, _val)) 
			'lte' =>
				_val = data2d.getCompare(key, 'lte')
				_vals.push(p.kdv(key, _val)) 
			'gt' =>
				_val = data2d.getCompare(key, 'gt')
				_vals.push(p.kdv(key, _val)) 
			'gte' =>
				_val = data2d.getCompare(key, 'gte')
				_vals.push(p.kdv(key, _val))
			'raw' =>
				_vals.push(_rawValue(data2d, key, data2d.dataTypes.get(i)))
			=>
				_vals.push(p.kdv(key, data2d.get(key)))
	p.kdvs(_vals)

__displayValues(data3d data3d, string name) =>
	_vals = array.new<p.pkdv>()
	for [i, pk] in data3d.primaryKeys()
		_kdvs = array.new<p.kdv>()
		for [n, dk] in data3d.dataKeys()
			switch name
				'alt' => _kdvs.push(p.kdv(dk, data3d.get(pk, dk, true)))
				'change' => _kdvs.push(p.kdv(dk, data3d.getChange(pk, dk)))
				'change_format' => _kdvs.push(p.kdv(dk, data3d.getFormatChange(pk, dk)))
				'change_percent' => _kdvs.push(p.kdv(dk, data3d.getChange(pk, dk, true)))
				'change_percent_format' => _kdvs.push(p.kdv(dk, data3d.getFormatChange(pk, dk, true)))
				'et' => _kdvs.push(p.kdv(dk, data3d.getCompare(pk, dk, 'et')))
				'lt' => _kdvs.push(p.kdv(dk, data3d.getCompare(pk, dk, 'lt')))
				'lte' => _kdvs.push(p.kdv(dk, data3d.getCompare(pk, dk, 'lte')))
				'gt' => _kdvs.push(p.kdv(dk, data3d.getCompare(pk, dk, 'gt')))
				'gte' => _kdvs.push(p.kdv(dk, data3d.getCompare(pk, dk, 'gte')))
				'raw' => _kdvs.push(_rawValue(data3d, pk, dk, data3d.dataKeyTypes.get(n)))
				=> _kdvs.push(p.kdv(dk, data3d.get(pk, dk)))
		_vals.push(p.pkdv(pk, p.kdvs(_kdvs)))
	p.pkdvs(_vals)

// @function            Print data on screen.
// @param this          (_printer) A tools._printer object. 
// @param values        Multidatas object to use for print.
// @param displayValues Override default data printing.
// @param title         (string) A title for the value. (`na` is used if no argument is supplied). 
// @param titleStyle    (titleStyle) Add a custom `titleStyle` for this print only. (`na` is used if no argument is supplied). 
// @param styles     	(map<string, p.cellStyle>) Add custom cellStyles for this print only. (`na` is used if no argument is supplied).
// @param theme         (string) The name of the theme. Choices: gray, blue, green, orange, green, red, purple, pink and white. (`na` is used if no argument is supplied).
// @param dynamicValues (map<string, p.dvs>) Add custom dynamic values to use for dynamic colors. (`na` is used if no argument is supplied).
// @param dynamicKey    A bool value for data2d, and primary key value for data3d. When set, the key or primary key cells will match dynamic colors of value cell. (`false` or `na` is used if no argument is supplied).
// @returns             (_printer) Returns the _printer UDT object.
export method print(p._printer this, data2d values, string displayValues=na, string title=na, p.titleStyle titleStyle=na, map<string, p.cellStyle> styles=na, string theme=na, map<string, p.dvs> dynamicValues=na, bool dynamicKey=false) =>
	p.print2d(this=this, values=__displayValues(values, displayValues), title=title, titleStyle=titleStyle, styles=styles, theme=theme, dynamicValues=dynamicValues, dynamicKey=dynamicKey)

export method print(p._printer this, data3d values, string displayValues=na, string title=na, p.titleStyle titleStyle=na, map<string, p.cellStyle> styles=na, string theme=na, map<string, p.dvs> dynamicValues=na, string dynamicKey=na) =>
	p.print3d(this=this, values=__displayValues(values, displayValues), title=title, titleStyle=titleStyle, styles=styles, theme=theme, dynamicValues=dynamicValues, dynamicKey=dynamicKey)

// }

// var d2d1 = array.from(
//   '- Key-Value data structure: key -> [ main-value | alternate-value ].',
//   '- Ability to store multiple data types.',
//   '- Add a second alternate value, enabling two values under one key.',
//   '- Built in comparison methods to compare main values with their alternate values.',
//   '- Efforlessly store, retrieve and display Unix timestamps.',
//   '- Automaticaly format stored data.',
//   '- Sort all data types.',
//   '- Familiar array functions for intuitive data manipulation.',
//   '- You can now print a data2d object directly, using the `tools` print() method.')

// var d3d1 = array.from(
//   '- A nested data structure: primary key -> data key -> [ main-value | alternate-value ].',
//   '- Sorting based on a data key.',
//   '- Plus, shares most features of data2d.',
//   '- Similar to data2d, you can print data3d objects using the `tools` print() method.')

// General printer setup and styling:
var tbs = p.tableStyle.new(bgColor=na, frameColor=na)
var hdr = "Multidimensional Data Structures."
var ftr = "Full Documentation: https://faiyaz7283.github.io/multidata"
var hs = p.headerStyle.new(textSize=size.large, textColor=tools._color('purple_bright'))
var fs = p.footerStyle.new(textSize=size.large, textHalign=text.align_right, textColor=tools._color('green_bright'))
var ts = p.titleStyle.new(top=false, textHalign=text.align_left, textValign=text.align_center, textSize=size.normal, 
  textColor=tools._color('yellow_bright'))
var cs = p.cellStyle.new(horizontal=true, textHalign=text.align_left, textSize=size.normal, 
  textColor=tools._color('white_bright'))

// if barstate.islast
// 	printer = p.printer(header=hdr, footer=ftr, tableStyle=tbs, headerStyle=hs, footerStyle=fs, titleStyle=ts, 
// 	  cellStyle=cs).gutter(true).stack(true)

// 	printer.print(d2d1, title='data2d')
// 	printer.print(d3d1, title='data3d')

// Test {

// getOHLC(simple string ticker) =>
//     m = array.from(open, high, low, close, volume, time)
//     a = array.from(open[1], high[1], low[1], close[1], volume[1], time[1])
// 	tckr = ticker.new(syminfo.prefix(ticker), syminfo.ticker(ticker), syminfo.session)
//     [main, alt] = request.security(tckr, '', [m, a])
//     kvs = array.new<kv>()
//     for [idx, dk] in array.from('open', 'high', 'low', 'close', 'volume', 'time')
// 		_val = main.get(idx)
// 		_alt = alt.get(idx)
// 		format = dk == 'volume' ? tools.numCompact(_val): '{0,number,currency}'
// 		if dk == 'time'
// 			curr_tf = timeframe.period
// 			frmt = switch 
// 				str.endswith(curr_tf, 'S') => 'second'
// 				str.endswith(curr_tf, 'D') => 'day'
// 				str.endswith(curr_tf, 'W') => 'day'
// 				str.endswith(curr_tf, 'M') => 'day'
// 				=> 'minute'

// 			kvs.push(dk.kv(val=tools._int(_val), altVal=tools._int(_alt), timestamp=true, changeFormat=frmt))
// 		else
//         	kvs.push(dk.kv(val=_val, altVal=_alt, format=format, changeFormat=format, changePercentFormat='{0,number,0.000} %'))
//     kvs

// if barstate.islast
// 	printer = p.printer(tableStyle=tbs, headerStyle=hs, footerStyle=fs, titleStyle=ts, cellStyle=cs, theme=na, debug=true).gutter(false).stack(true)

// 	// Setup Data:
// 	aapl = pkv('AAPL', getOHLC('NASDAQ:AAPL'))
// 	msft = pkv('MSFT', getOHLC('NASDAQ:MSFT'))
// 	nvda = pkv('NVDA', getOHLC('NASDAQ:NVDA'))
// 	nflx = pkv('NFLX', getOHLC('NASDAQ:NFLX'))
// 	tsla = pkv('TSLA', getOHLC('NASDAQ:TSLA'))
// 	goog = pkv('GOOG', getOHLC('NASDAQ:GOOG'))
// 	pkvs = array.from(aapl, msft, nvda, nflx, tsla, goog)

// 	// Create Data2d storage:
// 	d2d = data2d(getOHLC(syminfo.tickerid))

// 	// Dynamic values
// 	_valDv = d2d.changeValues(true)
// 	d2dDv = map.new<string, p.dvs>()
//     d2dDv.put('key', _valDv.dvs())
//     d2dDv.put('val', _valDv.dvs())

// 	// Styling key cells
// 	d2d_styles = map.new<string, p.cellStyle>()
//     styleKey = p.cellStyle.copy(printer.keyCellStyle)
//     styleKey.dynamicColor := p.dynamicColor.new(numberUp=_valDv.max(), numberDown=_valDv.min(), offsetItem='text', offsetColor=#000000)
// 	styleKey.gradient := true
//     d2d_styles.put('key', styleKey)

// 	// Styling val cells
//     styleVal = p.cellStyle.copy(cs)
//     styleVal.dynamicColor := p.dynamicColor.new(numberUp=_valDv.max(), numberDown=_valDv.min(), offsetItem='bg')
// 	styleVal.gradient := true
//     d2d_styles.put('val', styleVal)

// 	// Create Data3d storage:
// 	d3d = data3d(pkvs, sort=true, asc=false, sortByKey='close', sortByChange=true)
// 	// Dynamic values
// 	_openDv = d3d.dkChangeValues('open', true)
// 	_highDv = d3d.dkChangeValues('high', true)
// 	_lowDv = d3d.dkChangeValues('low', true)
// 	_closeDv = d3d.dkChangeValues('close', true)
// 	_volumeDv = d3d.dkChangeValues('volume', true)
// 	_timeDv = d3d.dkChangeValues('time', true)
// 	d3dDv = map.new<string, p.dvs>()
//     d3dDv.put('open', _openDv.dvs())
//     d3dDv.put('high', _highDv.dvs())
// 	d3dDv.put('low', _lowDv.dvs())
//     d3dDv.put('close', _closeDv.dvs())
// 	d3dDv.put('volume', _volumeDv.dvs())
// 	d3dDv.put('time', _timeDv.dvs())
// 	d3dDv.put('dk', array.from(color.silver, color.silver, color.silver, color.lime, color.orange, color.silver).dvs())

// 	d3d_styles = map.new<string, p.cellStyle>()
// 	stylePk = p.cellStyle.copy(printer.keyCellStyle)
//     stylePk.dynamicColor := p.dynamicColor.new(numberUp=_closeDv.max(), numberDown=_closeDv.min(), offsetItem='text', offsetColor=#000000)
// 	stylePk.gradient := true
// 	d3d_styles.put('pk', stylePk)
// 	styleDk = p.cellStyle.copy(printer.keyCellStyle)
//     styleDk.dynamicColor := p.dynamicColor.new(offsetItem='bg', offsetColor=na)
// 	d3d_styles.put('dk', styleDk)
// 	styleOpen = p.cellStyle.copy(cs)
//     styleOpen.dynamicColor := p.dynamicColor.new(numberUp=_openDv.max(), numberDown=_openDv.min(), offsetItem='bg')
// 	styleOpen.gradient := true
//     d3d_styles.put('open', styleOpen)
// 	styleHigh = p.cellStyle.copy(cs)
//     styleHigh.dynamicColor := p.dynamicColor.new(numberUp=_highDv.max(), numberDown=_highDv.min(), offsetItem='bg')
// 	styleHigh.gradient := true
//     d3d_styles.put('high', styleHigh)
// 	styleLow = p.cellStyle.copy(cs)
//     styleLow.dynamicColor := p.dynamicColor.new(numberUp=_lowDv.max(), numberDown=_lowDv.min(), offsetItem='bg')
// 	styleLow.gradient := true
//     d3d_styles.put('low', styleLow)
// 	styleClose = p.cellStyle.copy(cs)
//     styleClose.dynamicColor := p.dynamicColor.new(numberUp=_closeDv.max(), numberDown=_closeDv.min(), offsetItem='text', offsetColor = #000000)
// 	styleClose.gradient := true
//     d3d_styles.put('close', styleClose)
// 	styleVolume = p.cellStyle.copy(cs)
// 	styleVolume.dynamicColor := p.dynamicColor.new(numberUp=_volumeDv.max(), numberDown=_volumeDv.min(), offsetItem='bg')
// 	styleVolume.gradient := true
//     d3d_styles.put('volume', styleVolume)
// 	styleTime = p.cellStyle.copy(cs)
// 	styleTime.dynamicColor := p.dynamicColor.new(numberUp=_timeDv.max(), numberDown=_timeDv.min(), offsetItem='bg')
// 	styleTime.gradient := true
//     d3d_styles.put('date', styleTime)


// 	// Output
// 	// printer.print(d2d, displayValues='change_percent_format', styles=d2d_styles, dynamicValues=d2dDv, dynamicKey=true)
// 	printer.print(d3d, styles=d3d_styles, displayValues='change_format', dynamicValues=d3dDv, dynamicKey='close')
// }