array/array_dataframes.js

//Initialise functions
{
	if (!global.Array) global.Array = {};
	
	/**
	 * Appends two dataframes to one another.
	 * @alias Array.appendDataframes
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to input into the function.
	 * @param {Array.<any[]>} arg1_dataframe - The dataframe to append.
	 * @param {Object} [arg2_options]
	 *  @param {any} [arg2_options.default_value] - Optional. What the default variable should be.
	 * 
	 * @returns {Array.<any[]>}
	 */
	Array.appendDataframes = function (arg0_dataframe, arg1_dataframe, arg2_options) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let ot_dataframe = arg1_dataframe;
		let options = (arg2_options) ? arg2_options : {};
		
		//Declare local instance variables
		let headers = Array.from(new Set([...dataframe[0], ...ot_dataframe[0]]));
		let new_dataframe = [headers];
		
		//Remove headers from original dataframes
		let dataframe_one = dataframe.slice(1);
		let dataframe_two = ot_dataframe.slice(1);
		
		//Append data from first dataframe
		new_dataframe.push(...dataframe_one);
		
		//Append data from the second dataframe
		for (let row of dataframe_two) {
			let new_row = new Array(headers.length).fill(options.default_value);
			
			for (let x = 0; x < row.length; x++) {
				let column_index = headers.indexOf(ot_dataframe[0][x]);
				new_row[column_index] = row[x];
			}
			
			new_dataframe.push(new_row);
		}
		
		//Return statement
		return new_dataframe;
	};
	
	/**
	 * Converts a dataframe to an object.
	 * @alias Array.convertDataframeToObject
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to convert to an object
	 * 
	 * @returns {any[]}
	 */
	Array.convertDataframeToObject = function (arg0_dataframe) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		
		//Guard clause
		if (!Array.isArray(dataframe)) return dataframe;
		
		//Declare local instance variables
		let dataframe_header = dataframe[0];
		let dataframe_obj = {};
		
		//Guard clause if dataframe has no header, or is not 2D array
		if (!dataframe_header || !Array.isArray(dataframe_header))
			return dataframe;
		
		//Initialise dataframe_obj subobjects
		for (let i = 0; i < dataframe_header.length; i++)
			dataframe_obj[dataframe_header[i]] = {};
		
		//Iterate over dataframe (rows)
		for (let i = 1; i < dataframe.length; i++)
			for (let x = 0; x < dataframe[i].length; x++)
				if (dataframe_header[x])
					dataframe_obj[dataframe_header[x]][i] = dataframe[i][x];
		
		//Return statement
		return dataframe_obj;
	};
	
	/**
	 * Converts a given object to a dataframe.
	 * @alias Array.convertObjectToDataframe
	 * 
	 * @param {Object} arg0_dataframe_obj - The object to convert into a dataframe.
	 * 
	 * @returns {Array.<any[]>}
	 */
	Array.convertObjectToDataframe = function (arg0_dataframe_obj) {
		//Convert from parameters
		let dataframe_obj = arg0_dataframe_obj;
		
		//Guard clause
		if (typeof dataframe_obj != "object") return dataframe_obj;
		
		//Declare local instance variables
		let all_variables = Object.keys(dataframe_obj);
		let return_dataframe = [];
		
		//Set header
		return_dataframe.push(all_variables);
		
		//Iterate over all_variables
		for (let i = 0; i < all_variables.length; i++) {
			let local_subobj = dataframe_obj[all_variables[i]];
			
			let all_local_keys = Object.keys(local_subobj);
			
			for (let x = 0; x < all_local_keys.length; x++) {
				let local_value = local_subobj[all_local_keys[x]];
				
				//Initialise array if nonexistent
				if (!return_dataframe[all_local_keys[x]])
					return_dataframe[all_local_keys[x]] = [];
				return_dataframe[all_local_keys[x]][i] = local_value;
			}
		}
		
		//Return statement
		return return_dataframe;
	};
	
	/**
	 * Fetches the number of columns in a given dataframe.
	 * @alias Array.getColumns
	 * 
	 * @param {Object} arg0_dataframe - The dataframe to pass to the function.
	 * 
	 * @returns {number}
	 */
	Array.getColumns = function (arg0_dataframe) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		
		//Guard clause
		if (dataframe.length === 0) return 0;
		
		//Declare local instance variables
		let max_columns = 0;
		
		//Iterate over all rows in the dataframe
		for (let i = 0; i < dataframe.length; i++)
			if (Array.isArray(dataframe[i]))
				max_columns = Math.max(dataframe[i].length, max_columns);
		
		//Return statement
		return max_columns;
	};
	
	/**
	 * Returns the number of columns and rows.
	 * @alias Array.getDimensions
	 * 
	 * @param {Array.<any[]>} arg0_dataframe
	 * 
	 * @returns {number[]}
	 */
	Array.getDimensions = function (arg0_dataframe) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		
		//Return statement
		return [Array.getColumns(dataframe), Array.getRows(dataframe)];
	};
	
	/**
	 * Fetches the number of rows in a dataframe.
	 * @alias Array.getRows
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * 
	 * @returns {number}
	 */
	Array.getRows = function (arg0_dataframe) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		
		//Return statement
		return dataframe.length;
	};
	
	/**
	 * Checks whether a dataframe has a true header.
	 * @alias Array.getHeader
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * 
	 * @returns {boolean}
	 */
	Array.hasHeader = function (arg0_dataframe) {
		//Convert from parameters
		let dataframe = Array.toArray(arg0_dataframe);
		
		//Declare local instance variables
		let has_header = false;
		
		if (Array.isArray(dataframe[0])) {
			let all_strings = true;
			
			for (let i = 0; i < dataframe[0].length; i++)
				if (typeof dataframe[0][i] != "string") {
					all_strings = false;
					break;
				}
			
			if (all_strings)
				has_header = true;
		}
		
		//Return statement
		return has_header;
	};
	
	/**
	 * Merges two dataframes; with the second dataframe's columns being appended to the first dataframe post-operation. Mathematical operations can be applied here as a system of equations. Dataframes may have different dimensions, non-corresponding values are assumed to be zero or undefined.
	 * 
	 * Dataframes are a 2D array, typically with a header row.
	 * 
	 * @alias Array.mergeDataframes
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The 1st dataframe to pass to the function.
	 * @param {Array.<any[]>} arg1_dataframe - The 2nd dataframe to pass to the function.
	 * @param {Object} [arg2_options]
	 *  @param {string} [arg2_options.equation] - The string literal to use as an equation (e.g. 'i + x*5'). If no equal sign is provided, this applies to every cell, regardless of column. Equations are split by semicolons.<br>- <br>As an example, x$D = i$B, replaces the D column of the 2nd dataframe with the B column of the 1st.<br>- 'i' represents the corresponding element of the first dataframe,<br>  - 'i$Column' represents the selection of a 1st dataframe column named 'Column'.<br>- 'x' represents the corresponding element of the second dataframe,<br>  - 'x$Column' represents the selection of a 2nd dataframe column named 'Column'.
	 * 
	 * @returns {Array.<any[]>}
	 */
	Array.mergeDataframes = function (arg0_dataframe, arg1_dataframe, arg2_options) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let ot_dataframe = arg1_dataframe;
		let options = (arg2_options) ? arg2_options : {};
		
		//Process options.equation
		if (options.equation) {
			let operate_dataframes = Array.operateDataframes(dataframe, ot_dataframe, options);
			
			dataframe = operate_dataframes.dataframe;
			ot_dataframe = operate_dataframes.ot_dataframe;
		}
		
		//Append dataframes
		let return_dataframe = Array.appendDataframes(dataframe, ot_dataframe);
		
		//Return statement
		return return_dataframe;
	};
	
	/**
	 * Operates on two dataframes by applying an equation string.
	 * @alias Array.operateDataframes
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The 1st dataframe to operate on as i.
	 * @param {Array.<any[]>} arg1_dataframe - The 2nd dataframe to operate on as x.
	 * @param {Object} [arg2_options]
	 *  @param {string} [arg2_options.equation] - The string literal to use as an equation (e.g. 'i + x*5'). If no equal sign is provided, this applies to every cell, regardless of column. Equations are split by semicolons.<br>- <br>As an example, x$D = i$B, replaces the D column of the 2nd dataframe with the B column of the 1st.<br>- 'i' represents the corresponding element of the first dataframe,<br>  - 'i$Column' represents the selection of a 1st dataframe column named 'Column'.<br>- 'x' represents the corresponding element of the second dataframe,<br>  - 'x$Column' represents the selection of a 2nd dataframe column named 'Column'.
	 * 
	 * @returns {{dataframe: Array.<any[]>, ot_dataframe: Array.<any[]>}}
	 */
	Array.operateDataframes = function (arg0_dataframe, arg1_dataframe, arg2_options) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let ot_dataframe = arg1_dataframe;
		let options = (arg2_options) ? arg2_options : {};
		
		//Initialise options
		if (options.return_safe_number !== false)
			options.return_safe_number = true;
		
		//Parse options.equation
		if (options.equation) {
			//Convert dataframes to objects for easier corresponding manipulation
			let dataframe_length = dataframe.length;
			let ot_dataframe_length = ot_dataframe.length;
			
			//Formatting variables
			let f_0 = (options.return_safe_number) ? `returnSafeNumber(` : "";
			let f_1 = (options.return_safe_number) ? `)` : "";
			
			dataframe = Array.convertDataframeToObject(dataframe);
			ot_dataframe = Array.convertDataframeToObject(ot_dataframe);
			
			let split_equation = options.equation.split(";");
			
			//Iterate over split_equation to apply them to dataframe; ot_dataframe
			for (let i = 0; i < split_equation.length; i++) {
				let local_regex = /\$(\w+)/g;
				
				let converted_string = split_equation[i].replace(local_regex, `["$1"]`);
				
				//Replace "/" with the division operator
				converted_string = converted_string.replace(/\//g, "/");
				
				//Equation function declaration; fetching the max length of variables involved in that equation; then performing iterative equations. Non-values assumed to be zero
				let max_length = (dataframe.length > ot_dataframe.length) ?
					dataframe_length : ot_dataframe_length;
				
				//Iterate over all max_length
				for (let x = 1; x < max_length; x++) {
					let processed_equation = converted_string.replace(/"]/g, `"][${x}]`);
					let regex_i = /i\["(\w+)"\]\[(\d+)\]/g;
					let regex_x = /x\["(\w+)"\]\[(\d+)\]/g;
					
					//Split processed_equation
					processed_equation = processed_equation.split("=");
					
					processed_equation[1] = processed_equation[1].replace(regex_i, `${f_0}i["$1"][${x}]${f_1}`);
					processed_equation[1] = processed_equation[1].replace(regex_x, `${f_0}x["$1"][${x}]${f_1}`);
					
					let equation_expression = `${processed_equation[0]} = ${processed_equation[1]};`;
					let equation_function = new Function("i", "x", equation_expression);
					
					//Process function
					equation_function(dataframe, ot_dataframe);
				}
			}
			
			//Reconvert back to dataframe arrays
			dataframe = Array.convertObjectToDataframe(dataframe);
			ot_dataframe = Array.convertObjectToDataframe(ot_dataframe);
		}
		
		//Return statement
		return {
			dataframe: dataframe,
			ot_dataframe: ot_dataframe
		};
	};
	
	/**
	 * Sets the upper header variables.
	 * @alias Array.setHeader
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * @param {string[]} arg1_header_array - The names of variables to set on the 0th row.
	 * 
	 * @returns {Array.<any[]>}
	 */
	Array.setHeader = function (arg0_dataframe, arg1_header_array) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let headers = Array.toArray(arg1_header_array);
		
		//Set header
		dataframe[0] = headers;
		
		//Return statement
		return dataframe;
	};
	
	/**
	 * Selects a 2D array column (by header name).
	 * @alias Array.selectColumn
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * @param {string} arg1_column_name - The name of the variable/column to select.
	 * @param {Object} [arg2_options]
	 *  @param {boolean} [arg2_options.return_index=false] - Whether to return an index.
	 * 
	 * @returns {any[]|boolean|number}
	 */
	Array.selectColumn = function (arg0_dataframe, arg1_column_name, arg2_options) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let column_name = arg1_column_name.toString().toLowerCase();
		let options = (arg2_options) ? arg2_options : {};
		
		//Declare local instance variables
		let column_exists = [false, -1]; //[column_exists, column_index];
		let return_array = [];
		
		//Iterate over dataframe header
		if (Array.isArray(dataframe))
			if (dataframe[0]) {
				//Soft-match first
				for (let i = 0; i < dataframe[0].length; i++) {
					//Check against local string
					if (dataframe[0][i].toString().toLowerCase().indexOf(column_name) !== -1)
						column_exists = [true, i];
				}
				
				//Hard-match second
				for (let i = 0; i < dataframe[0].length; i++) {
					//Check against local string
					if (dataframe[0][i].toString().toLowerCase() === column_name)
						column_exists = [true, i];
				}
			}
		
		//Return statement; options.return_index guard clause
		if (options.return_index) return column_exists[1];
		
		//If column_exists[0], process return_array
		if (column_exists[0])
			for (let i = 0; i < dataframe.length; i++)
				return_array.push(dataframe[i][column_exists[1]]);
		
		//Return statement
		return return_array;
	};
	
	/**
	 * Selects a 2D array row (by header name or index).
	 * @alias Array.selectRow
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * @param {number} arg1_row_index - The row index to pass to the function.
	 * @param {Object} [arg2_options]
	 *  @param {boolean} [arg2_options.exclude_header=false] - Whether to exclude the header.
	 * 
	 * @returns {any[]}
	 */
	Array.selectRow = function (arg0_dataframe, arg1_row_index, arg2_options) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let row_index = arg1_row_index;
		let options = (arg2_options) ? arg2_options : {};
		
		//Return statement
		return (!options.exclude_header) ? dataframe[row_index] : dataframe[row_index + 1];
	};
	
	/**
	 * Sets a 2D array column.
	 * @alias Array.setColumn
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * @param {string} arg1_column_name - The name of the variable/column to set.
	 * @param {any[]} arg2_values - The list of values to set for this column.
	 * 
	 * @returns {any[]}
	 */
	Array.setColumn = function (arg0_dataframe, arg1_column_name, arg2_values) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let column_name = arg1_column_name;
		let values = Array.toArray(arg2_values);
		
		//Declare local instance variables
		let column_index = (isNaN(column_name)) ? 
			Array.selectColumn(dataframe, column_name, { return_index : true }) : column_name;
		
		//Set new values, delete rest
		for (let i = 0; i < dataframe.length; i++)
			dataframe[i][column_index] = values[i];
		
		//Return statement
		return dataframe.filter((row) => {
			row.some(element => element !== undefined && element !== null);
		});
	};
	
	/**
	 * Sets a 2D array row.
	 * @alias Array.setRow
	 * 
	 * @param {Array.<any[]>} arg0_dataframe - The dataframe to pass to the function.
	 * @param {number} arg1_row_index - The row index to pass to the function.
	 * @param {any[]} arg2_values - The list of values to set for this row.
	 * 
	 * @returns {any[]}
	 */
	Array.setRow = function (arg0_dataframe, arg1_row_index, arg2_values) {
		//Convert from parameters
		let dataframe = arg0_dataframe;
		let row_index = arg1_row_index;
		let values = Array.toArray(arg2_values);
		
		//Set local row_index to values
		if (Array.isEmpty(dataframe)) {
			dataframe[row_index] = values;
		} else {
			dataframe.splice(row_index, 1);
		}
		
		//Return statement
		return dataframe;
	};
}