Calculation - Subtract - Table(s)

From Q
Jump to navigation Jump to search
This page is currently under construction, or it refers to features which are under development and not yet available for use.
This page is under construction. Its contents are only visible to developers!

Subtract two outputs or elements

Background

Usage

Code

const UNCHECK_NAMES = ["SUM", "NET", "TOTAL"];
const MULTI_QUESTIONTYPES = ["Text - Multi",
                             "Pick One - Multi", "Pick Any - Compact",
                             "Pick Any - Grid", "Number - Grid"];
const ALLOWED_R_CLASSES = ["numeric", "integer", "logical", "factor", "matrix", "array", "data.frame", "table"];

function addListBox(listbox_names, dim, guid)
{
	let dim_str = dim === 0 ? "Row" : "Column";
	return form.listBox({name: "formInclude" + dim_str + "s" + guid.replace(/-/g,""),
	                       label: dim_str + "s to include",
	                       alternatives: listbox_names["names"], names: listbox_names["names"],
	                       required: false,
	                       prompt: "Select the " + dim_str.toLowerCase() + " labels to be included in the output table.",
	                       initialValues: listbox_names["initial"], multiSelection: true,
	                      nItemsVisible: 5});
}

function getInputNames(input, dim = 0)
{
	var input_names;
	var listbox_names = {};
	let input_type = input.type;
	if (input_type === "R Output")
	{
		try
		{
			var output_class = input.outputClasses;
			if (output_class.includes("array") || output_class.includes("matrix"))
			{
				var dimnames = input.data.getAttribute([], "dimnames");
				if (dim < dimnames.length && dimnames[dim] != null)
					input_names = dimnames[dim];
				else
					input_names = [];
			} else if (output_class.includes("data.frame"))
			{
				if (dim === 1)
					input_names = input.data.getAttribute([], "names");
				else
					input_names = input.data.getAttribute([], "row.names");
			} else
			{
				if (dim === 0)
					input_names = input.data.getAttribute([], "names");
				else
				input_names = [];
			}
		}
		catch(e)
		{
			input_names = [];
		}
		listbox_names["names"] = input_names;
		listbox_names["initial"] = filterSingleNames(input_names);
	} else {
		listbox_names = getTableDimNames(input, dim);
	}
	// DS-3147: replace newline chars/any whitespace with single space
	if (listbox_names["names"].length > 0)
	{
		Object.keys(listbox_names).map(key => {
			listbox_names[key] = listbox_names[key].map(str => str.replace(/s+/g, " "));
		});
	}
	return listbox_names;
}

function getTableDimNames(table, dim)
{
    let has_primary = table.primary != null;
    let table_output_names = {"names": [], "initial": []};
    if (has_primary)
    {
        let table_output = table.calculateOutput();
        let is_crosstab_or_multi_or_raw = table.secondary.type === "Question"
	|| MULTI_QUESTIONTYPES.includes(table.primary.questionType)
	|| table.secondary === "RAW DATA";
        if (table.primary.isBanner && table.secondary === "SUMMARY")
            is_crosstab_or_multi_or_raw = false;
        if (dim === 0)
        {
            let row_names = table_output.rowLabels;
            let row_spans = table_output.rowSpans;
            if (row_spans.length > 1)
            {
                table_output_names = flattenSpanNames(row_names, row_spans);
            } else
            {
                table_output_names = {"names": row_names, "initial": filterSingleNames(row_names)};
            }
        }
        if (dim === 1)
        {
            let n_columns = table_output.numberColumns;
            let col_spans = n_columns < 2 ? [] : table_output.columnSpans;
            let col_names = [];
            if (col_spans.length > 1)
            {
                col_names = table_output.columnLabels;
                table_output_names = flattenSpanNames(col_names, col_spans);
            } else
            {
                col_names = is_crosstab_or_multi_or_raw ? table_output.columnLabels : table_output.statistics;
                table_output_names = {"names": col_names, "initial": filterSingleNames(col_names)};
            }
        }
    }
    return table_output_names;
}

function filterSingleNames(names)
{
	return names.filter(n => !UNCHECK_NAMES.includes(n));
}

function flattenSpanNames(labels, span_names)
{
	let span_length = span_names.length;
	let span_labels = labels;
	let unselect_labels = span_names.filter(span => UNCHECK_NAMES.includes(span["label"]));
	let unselect_span_indices = [];
	if(unselect_labels.length > 0)
	{
		unselect_span_indices = unselect_labels.map(unselect => unselect["indices"]);
		unselect_span_indices = [].concat.apply([], unselect_span_indices);
		unselect_span_indices = uniq(unselect_span_indices);
	}
	let unselected_base_indices = labels.map((l, i) => UNCHECK_NAMES.includes(l) ? i : "").filter(Number);
	let unselected_indices = [].concat.apply([], [unselect_span_indices, unselected_base_indices]);
	unselected_indices = uniq(unselected_indices)
	labels.forEach((item, i) => {
		for (j = 0; j < span_length; j++)
		{
			let curr_span = span_names[j];
			if (curr_span["indices"].includes(i))
			{
			    span_labels[i] = span_names[j]["label"] + " - " + span_labels[i];
			}
		}
	});
	let initial_values = span_labels.filter((label, i) => !unselected_indices.includes(i));
	return {"names": span_labels, "initial": initial_values};
}

function recursiveGetItemByGuid(group_item, guid)
{
	var cur_sub_items = group_item.subItems;
	for (var j = 0; j < cur_sub_items.length; j++)
	{
		if (cur_sub_items[j].type == "ReportGroup") {
			var res = recursiveGetItemByGuid(cur_sub_items[j], guid);
			if (res != null)
			    return(res)
		}
		else if (cur_sub_items[j].guid == guid)
			return(cur_sub_items[j]);
	}
	return null;
}

function uniq(a)
{
	var seen = {};
	return a.filter(function(item) {
		return seen.hasOwnProperty(item) ? false : (seen[item] = true);
	});
}

function addListBoxAfterProcessingNames(all_listbox_names, dim, guid)
{
	if (all_listbox_names.length === 1)
	{
		return addListBox(all_listbox_names[0], dim, guid);
	} else
	{
		let keys = Object.keys(all_listbox_names[0]);
		let final_listbox_names = {};
		keys.forEach(key => {
			let names = all_listbox_names.map(names => names[key]);
			names = [].concat.apply([], names);
			final_listbox_names[key] = uniq(names);
		})
		return addListBox(final_listbox_names, dim,guid);
	}
}


let input_structure = {"names": ["Minuend", "Subtrahend"],
                       "labels": ["From the", "Subtract the"]};
let input_guids = [];

for (let i = 0; i < 2; i++)
{
	let name  = input_structure["names"][i];
	let label = input_structure["labels"][i];
	let combo_control = form.comboBox({name: "formCombo" + name, label: label,
			                           alternatives: ["Output", "Single numeric value"],
			                           default_value: "Output",
			                           prompt: "Choose an output on the page or specify a single value"});
	if (combo_control.getValue() === "Single numeric value")
	{
			form.textBox({name: "formSingle" + name, label: "", type: "number",
			              default_value: 0,
			              error: "The " + name + " here cannot be empty and must be a single numeric value",
			              prompt: "The single value to be used in the calculation"});
	} else
	{
		let data_input_control = form.dropBox({name: "form" + name, label: "",
		                                       types: ["table", "RItem: " + ALLOWED_R_CLASSES.join(", ")],
		                                       multi: false,
			                               error: "Please input data such as a table, vector or matrix",
		                                       prompt: "Input data such as a table or R vector or matrix"}).getValue();
		if (data_input_control != null)
			input_guids.push(data_input_control.guid);
	}
}
let n_inputs = input_guids.length;
if (n_inputs > 0)
{
	let row_names = [];
	let col_names = [];
	let inputs = input_guids.map(guid => recursiveGetItemByGuid(project.report, guid));
	row_names = inputs.map(input => getInputNames(input, 0));
	col_names = inputs.map(input => getInputNames(input, 1));
	row_names = row_names.filter(item => item["names"].length > 1);
	col_names = col_names.filter(item => item["names"].length > 1);
	let add_row_listbox = row_names.length > 0;
	let add_col_listbox = col_names.length > 0;
	if ((add_row_listbox || add_col_listbox) && n_inputs == 2)
	{
		var automatic_choice = form.comboBox({label: "Automatically match elements",
		                                      name: "formMatchElements",
		                                      alternatives : ["Yes - hide unmatched",
		                                                      "Yes - show unmatched",
		                                                      "No",
		                                                      "Custom"],
		                                      default_value: "Yes - hide unmatched",
		                                      prompt: "Automatically determine elements to match via row and column names"});
		automatic_choice = automatic_choice.getValue();
		if (automatic_choice === "Custom")
		{
			let has_both_row_names = row_names.every(item => item["names"].length > 0);
			let has_both_col_names = col_names.every(item => item["names"].length > 0);
			form.comboBox({name: "formMatchRows",
			               label: "Match rows",
			               alternatives: ["Yes - hide unmatched", "Yes - show unmatched", "Fuzzy - hide unmatched", "Fuzzy - show unmatched", "No"],
			               default_value: has_both_row_names ? "Yes - hide unmatched" : "No",
			               prompt: "Specify the matching behavior across the rows"});
			form.comboBox({name: "formMatchColumns",
			               label: "Match columns",
			               alternatives: ["Yes - hide unmatched", "Yes - show unmatched", "Fuzzy - hide unmatched", "Fuzzy - show unmatched", "No"],
			               default_value: has_both_col_names ? "Yes - hide unmatched" : "No",
			               prompt: "Specify the matching behavior across the columns"});
		}
	}
	if (add_row_listbox)
	{
		addListBoxAfterProcessingNames(row_names, 0, input_guids[0]);
	}
	if (add_col_listbox)
	{
		addListBoxAfterProcessingNames(col_names, 1, input_guids[0]);
	}
}
form.setHeading("Subtract");
library(verbs)

match.elements <- get0("formMatchElements", ifnotfound = c(rows = "No", columns = "No"))
if (length(match.elements) == 1L && match.elements == "Custom")
    match.elements <- c(rows = formMatchRows, columns = formMatchColumns)

row.ctrl.name <- ls(pattern = "^formIncludeRows")
col.ctrl.name <- ls(pattern = "^formIncludeColumns")
include.rows <- if (length(row.ctrl.name))
                    get(row.ctrl.name)
include.cols <- if (length(col.ctrl.name))
                    get(col.ctrl.name)

remove.rows    <- if (is.null(include.rows)) NULL else names(which(!include.rows))
remove.columns <- if (is.null(include.cols)) NULL else names(which(!include.cols))

minuend <- if (formComboMinuend == "Single numeric value") as.numeric(formSingleMinuend) else formMinuend
subtrahend <- if (formComboSubtrahend == "Single numeric value") as.numeric(formSingleSubtrahend) else formSubtrahend

subtracted.output <- Subtract(minuend, subtrahend,
                              remove.rows = remove.rows,
                              remove.columns = remove.columns,
                              match.elements =  match.elements,
                              subset = QFilter,
                              warn = TRUE)

See Also

LINK TO ANY PAGES HERE. MAY NEED A BOILERPLATE IN FUTURE.