QScript Functions for Calculations
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!
includeWeb("QScript Utility Functions");
const VALID_ROUTPUT_CLASSES = ["numeric","integer","logical","factor","matrix","array","data.frame","table"];
const INVALID_VARIABLE_SET_TYPES = ["Text", "Text - Multi", "Date/Time"];
function mathOperatorOnVariablesRCode(operator = "Divide", n_cases)
{
let variable_name;
let argument_names;
switch(operator)
{
case "Divide":
variable_name = "divided.variable";
argument_names = ["numerator", "denominator"];
break;
case "Multiply":
variable_name = "multiplied.variable";
argument_names = ["multiplicand", "multiplier"];
break;
case "Subtract":
variable_name = "subtracted.variable";
argument_names = ["minuend", "subtrahend"];
break;
}
let white_space = ' '.repeat(variable_name.length + 5 + operator.length) ;
capitalized_names = argument_names.map(name => name.charAt(0).toUpperCase() + name.slice(1));
return 'library(verbs)\n' +
'\n' +
argument_names[0] + ' <- if (formCombo' + capitalized_names[0] + ' == "Single numeric value") as.numeric(formSingle' + capitalized_names[0] + ') else form' + capitalized_names[0] + '\n' +
argument_names[1] + ' <- if (formCombo' + capitalized_names[1] + ' == "Single numeric value") as.numeric(formSingle' + capitalized_names[1] + ') else form' + capitalized_names[1] + '\n' +
'combo.choices <- c(' + capitalized_names.map(name => 'formCombo' + name).join(", ") + ')\n' +
'if(all(combo.choices == "Single numeric value"))\n' +
'{\n' +
' warning("The same single value from the calculation was used in all cases in the output variable")\n' +
' ' + argument_names[0] + ' <- rep(' + argument_names[0] + ', ' + n_cases + ')\n' +
' ' + argument_names[1] + ' <- rep(' + argument_names[1] + ', ' + n_cases + ')\n' +
'}\n' +
'\n' +
variable_name + ' <- ' + operator + '(' + argument_names.join(', ') + ',\n' +
white_space + 'remove.rows = NULL, remove.columns = NULL,\n' +
white_space + 'match.elements = "No", warn = TRUE)'
}
function mathOperatorOnVariablesJSCode(operator = "Divide")
{
let argument_names;
let label_names;
switch(operator)
{
case "Divide":
argument_names = ["Numerator", "Denominator"];
label_names = ["Divide the", "by the"];
break;
case "Multiply":
argument_names = ["Multiplicand", "Multiplier"];
label_names = ["Multiply the", "by the"];
break;
case "Subtract":
argument_names = ["Minuend", "Subtrahend"];
label_names = ["From the", "Subtract the"];
break;
}
let default_value = operator === "Subtract" ? "0" : "1";
let is_displayr = (!!Q.isOnTheWeb && Q.isOnTheWeb());
let structure_name = is_displayr ? "Variable Set" : "Question";
return 'const ALLOWED_VARIABLE_TYPES = ["Numeric", "Categorical", "Ordered Categorical", "Money"];\n' +
'const INVALID_VARIABLE_SET_TYPES = ["Text", "Text - Multi", "Date/Time"];\n' +
'\n' +
'let allowed_vars = ALLOWED_VARIABLE_TYPES.join(", ");\n' +
'let allowed_variable_sets = "!" + INVALID_VARIABLE_SET_TYPES.join(", !");\n' +
'let input_structure = {"names": ["' + argument_names[0] + '", "' + argument_names[1] + '"],\n' +
' "labels": ["' + label_names[0] + '", "' + label_names[1] + '"]};\n' +
'\n' +
'for (let i = 0; i < 2; i++)\n' +
'{\n' +
' let name = input_structure["names"][i];\n' +
' let label = input_structure["labels"][i];\n' +
' let combo_control = form.comboBox({name: "formCombo" + name, label: label,\n' +
' alternatives: ["Variable(s)", "Single numeric value"],\n' +
' default_value: "Variable(s)",\n' +
' prompt: "Choose a variable in the dataset or specify a single value"});\n' +
' if (combo_control.getValue() === "Single numeric value")\n' +
' {\n' +
' form.textBox({name: "formSingle" + name, label: "", type: "number",\n' +
' default_value: ' + default_value + ',\n' +
' error: "The " + name + " here cannot be empty and must be a single numeric value",\n' +
' prompt: "The single value to be used in the calculation"});\n' +
' } else\n' +
' {\n' +
' let data_input_control = form.dropBox({name: "form" + name, label: "",\n' +
' types: ["Variable: " + allowed_vars, "Variable Set: " + allowed_variable_sets],\n' +
' error: "Please select an input variable or ' + structure_name.toLowerCase() + '.",\n' +
' multi: false, prompt: "Input Variable or ' + structure_name + '"});\n' +
' }\n' +
'}\n' +
'form.setHeading("' + operator + '");'
}
function calculateVariableRCode(operator = "Sum", n_cases = null)
{
let r_code;
let form_names;
switch(operator)
{
case "Sum":
case "Average":
let operator_past_tense = generateOperatorPastTenseName(operator);
let function_call = operator_past_tense.toLowerCase() + '.variables <- ' + operator + 'EachRow';
let white_space = ' '.repeat(function_call.length);
r_code = 'library(verbs)\n' +
'\n' +
'n.variables <- length(formInputs)\n' +
'all.variables <- if (n.variables > 1L) QDataFrame(formInputs, check.names = FALSE) else formInputs[[1L]]\n' +
function_call + '(all.variables ,\n' +
white_space + 'remove.missing = formRemoveMissing,\n' +
white_space + 'remove.columns = NULL,\n' +
white_space + 'warn = TRUE)';
break;
case "Divide":
case "Multiply":
case "Subtract":
r_code = mathOperatorOnVariablesRCode(operator, n_cases);
break;
}
return r_code;
}
function calculateVariableJSCode(operator = "Sum")
{
let js_code;
switch(operator)
{
case "Average":
case "Sum":
js_code = 'form.dropBox({name: "formInputs",\n' +
' label: "Variables",\n' +
' duplicates: true,\n' +
' types: ["Variable:Numeric,Categorical,OrderedCategorical,Money"],\n' +
' multi:true,\n' +
' prompt: "Input variables that are not text or date variables"});\n' +
'form.checkBox({name: "formRemoveMissing",\n' +
' label: "Calculate for observations with incomplete data",\n' +
' default_value: true});\n' +
'form.setHeading("' + operator + ' variables by case");';
break;
case "Divide":
case "Multiply":
case "Subtract":
js_code = mathOperatorOnVariablesJSCode(operator);
break;
}
return js_code;
}
function uniq(a) {
var seen = {};
return a.filter(function(item) {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
}
function determineGroup(is_displayr)
{
let group = project.currentPage === undefined ? false : project.currentPage();
if (!group)
{
if (!is_displayr)
{
group = project.report;
}
else
{
group = project.report.appendGroup();
group.name = "New page";
}
}
return group
}
function extractQuestionsWithValidStructure(selected_variable_sets, operator, is_displayr, max_n_valid = 2)
{
let invalid_variable_sets = [];
selected_variable_sets = selected_variable_sets.filter(variable_set => {
let variable_set_structure = variable_set.variableSetStructure;
let invalid_variable_set = INVALID_VARIABLE_SET_TYPES.includes(variable_set_structure);
if (invalid_variable_set)
invalid_variable_sets.push(variable);
return !invalid_variable_set;
});
let n_valid_variable_sets = selected_variable_sets.length;
let all_invalid_variable_sets = selected_variable_sets.length === 0;
let n_variables = selected_variable_sets.map(variable_set => variable_set.variables.length);
let single_variables_selected = n_variables.every(variable_length => variable_length === 1);
let structure_name = single_variables_selected ? "variable" : is_displayr ? "Variable Set" : "Question";
if (invalid_variable_sets.length > 0)
{
let invalid_types = uniq(invalid_variable_sets.map(variable_set => variable_set.variableSetStructure)).join(" and ");
let invalid_names = invalid_variable_sets.map(variable_set => variable_set.name).join(", ");
if (all_invalid_variable_sets)
{
if (invalid_variable_sets.selected == 1)
{
log("The selected " + structure_name + invalid_names + " is a " + invalid_types + " " + structure_name + " and is not appropriate to use in " + operator + ".");
}
else
{
let appropriate_vars = is_displayr ? 'Number, Categorical, Ordered Categorical or Money': 'Numeric, Nominal or Ordinal';
log("All of the selected " + structure_name + "s are contain either Text or Date variables and are not appropriate to use in " + operator +
". Please select " + structure_name + "s containing " + appropriate_vars + " variables to use " + operator + ".");
}
} else
{
let var_msg = invalid_variable_sets.length === 1 ? " selected is" : " selected are";
log("The " + invalid_types + structure_name + " " + var_msg + " not appropriate to use in " + operator +
" and have been removed from the calculation.");
}
}
if (isFinite(max_n_valid) && n_valid_variable_sets > max_n_valid)
{
log("Only two " + structure_name + "s can be used in " + operator + ". " +
"The first two have been used in the output variable and the other selections ignored.");
}
return selected_variable_sets;
}
function extractValidVariables(selected_variables, operator, is_displayr)
{
let invalid_variables = [];
selected_variables = selected_variables.filter(variable => {
let var_type = variable.variableType;
let invalid_var = var_type === "Text" || var_type === "Date";
if (invalid_var)
invalid_variables.push(variable);
return !invalid_var;
});
let n_valid_variables = selected_variables.length;
let all_invalid_variables_selected = selected_variables.length === 0;
if (invalid_variables.length > 0)
{
let invalid_types = uniq(invalid_variables.map(variable => variable.variableType)).join(" and ");
let invalid_names_and_labels = invalid_variables.map(variable => variable.label + " (" + variable.name + ")").join(", ");
if (all_invalid_variables_selected)
{
if (invalid_variables.selected == 1)
{
log("The selected variable " + invalid_names_and_labels + " is a " + invalid_types + "variable and is not appropriate to use in " + operator + ".");
}
else
{
let appropriate_vars = is_displayr ? 'Number, Categorical, Ordered Categorical or Money': 'Numeric, Nominal or Ordinal';
log("All of the selected variables are either Text or Date variables and are not appropriate to use in " + operator +
". Please select " + appropriate_vars + " to use in " + operator + ".");
}
} else
{
let var_msg = invalid_variables.length === 1 ? " variable selected is" : " variables selected are";
log("The " + invalid_types + var_msg + " not appropriate to use in " + operator +
" and have been removed from the calculation.");
}
}
return selected_variables;
}
function determineVariableName(operator, variable_names, data_file)
{
let operator_name;
switch(operator)
{
case 'Sum':
operator_name = "sum";
break;
case 'Average':
operator_name = "avg";
break;
case 'Divide':
operator_name = "division";
break;
case 'Multiply':
operator_name = "multiplication";
break;
case 'Subtract':
operator_name = "subtraction";
break;
}
operator_name += "_of_";
let new_variable_name = variable_names.join("_");
new_variable_name = new_variable_name.startsWith(operator_name) ? new_variable_name : operator_name + new_variable_name;
new_variable_name = preventDuplicateVariableName(data_file, new_variable_name, "_");
return new_variable_name;
}
function determineVariableLabel(operator, labels, data_file)
{
let new_variable_label = "";
let operator_char;
switch(operator)
{
case "Divide":
operator_char = " / ";
break;
case "Multiply":
operator_char = " * ";
break;
case "Subtract":
operator_char = " - ";
break;
}
switch(operator)
{
case 'Sum':
new_variable_label = labels.join(" + ");
break;
case 'Average':
new_variable_label = 'Average of ' + labels.join("; ");
break;
case 'Divide':
case 'Multiply':
case 'Subtract':
new_variable_label += labels.length === 2 ? labels.join(operator_char) : labels[0] + " divided by a single value";
break
}
new_variable_label = preventDuplicateQuestionName(data_file, new_variable_label);
return new_variable_label;
}
function checkQuestionDimensionsValid(questions, operator, is_displayr)
{
if (questions.length < 2)
{
return true;
}
let n_variables = questions.map(question => question.variables.length);
if (n_variables[0] == n_variables[1])
{
return true;
}
if (n_variables.some(n_vars => n_vars === 1))
{
return true;
}
let structure_name = is_displayr ? "Variable Set" : "Question";
log("The selected " + structure_name + "s do not have the same number of variables. To use " + operator +
" the same number of variables are required in each " + structure_name + ".");
return false;
}
function appendCalculatedQuestionsToDataSet(operator, selected_questions, is_displayr, max_n_valid = Infinity)
{
let valid_questions = extractQuestionsWithValidStructure(selected_questions, operator, is_displayr, max_n_valid);
let n_valid_questions = valid_questions.length;
if (n_valid_questions > 0)
{
let data_files = valid_questions.map(question => question.dataFile);
let datafile_names = uniq(data_files.map(datafile => datafile.name));
let number_observations;
if (datafile_names.length > 1)
{
let nobs_data = uniq(data_files.map(data_file => data_file.totalN));
if (nobs_data.length > 1)
{
log("The selected variables are from different datasets (" + datafile_names.join(", ") + ") " +
"with " + nobs_data.join(", ") + " number of cases respectively. It is not possible to conduct " +
"the calculation if the input variables have a different number of cases. Please choose variables " +
"from the same dataset.");
return false;
}
number_observarions = nobs_data[0];
} else
{
number_observations = data_files[0].totalN;
}
if (!checkQuestionDimensionsValid(valid_questions, operator, is_displayr))
{
return false;
}
let guids = valid_questions.map(question => question.guid);
let data_file = valid_questions[0].dataFile;
let question_names = valid_questions.map(question => question.name);
let new_question_name = determineVariableLabel(operator, question_names, data_file);
let new_variable_base_name = determineVariableName(operator, question_names, data_file);
new_variable_base_name = cleanVariableName(new_variable_base_name);
let r_code = calculateVariableRCode(operator, number_observations);
let js_code = calculateVariableJSCode(operator);
let last_question_variables = valid_questions[n_valid_questions - 1].variables;
let last_variable = last_question_variables[last_question_variables.length - 1];
let control_settings = determineControlSettings(operator, valid_questions);
let new_question = data_file.newRQuestion(r_code, new_question_name, new_variable_base_name, last_variable, js_code, control_settings);
insertAtHoverButtonIfShown(new_question);
return true;
}
return false;
}
function appendCalculatedVariablesToDataSet(operator, selected_variables, is_displayr, max_n_valid = Infinity)
{
let valid_variables = extractValidVariables(selected_variables, operator, is_displayr, max_n_valid);
let n_valid_variables = valid_variables.length;
if (n_valid_variables > 0)
{
let data_files = valid_variables.map(variable => variable.question.dataFile);
let datafile_names = uniq(data_files.map(datafile => datafile.name));
let number_observations;
if (datafile_names.length > 1)
{
let nobs_data = uniq(data_files.map(data_file => data_file.totalN));
if (nobs_data.length > 1)
{
log("The selected variables are from different datasets (" + data_filenames.join(", ") + ") " +
"with " + nobs_data.join(", ") + " number of cases respectively. It is not possible to conduct " +
"the calculation if the input variables have a different number of cases. Please choose variables " +
"from the same dataset.");
return false;
}
number_observarions = nobs_data[0];
} else
{
number_observations = data_files[0].totalN;
}
let guids = valid_variables.map(variable => variable.guid);
let data_file = valid_variables[0].question.dataFile;
let labels = valid_variables.map(variable => variable.label);
let names = valid_variables.map(variable => variable.name);
let new_variable_name = determineVariableName(operator, names, data_file);
let new_variable_label = determineVariableLabel(operator, labels, data_file);
let r_code = calculateVariableRCode(operator, number_observations);
let js_code = calculateVariableJSCode(operator);
let last_variable = valid_variables[n_valid_variables - 1];
let control_settings = determineControlSettings(operator, valid_variables);
let new_variable = data_file.newRVariable(r_code, new_variable_name, new_variable_label, last_variable, js_code, control_settings);
insertAtHoverButtonIfShown(new_variable.question);
return true;
}
return false;
}
function determineControlSettings(operator = "Sum", selections = null)
{
let control_settings = {};
if (selections == null)
return control_settings;
let guids = selections.map(selection => selection.guid);
let control_names;
switch(operator)
{
case "Divide":
control_names = ["Numerator", "Denominator"];
break;
case "Multiply":
control_names = ["Multiplicand", "Multiplier"];
break;
case "Subtract":
control_names = ["Minuend", "Subtrahend"];
break;
}
switch(operator)
{
case "Average":
case "Sum":
control_settings = {"formInputs": guids.join(";")};
break;
case "Average Each Column":
case "Average Each Row":
case "Sum Each Column":
case "Sum Each Row":
control_settings = {"formInput": guids[0]};
break;
case "Divide":
case "Multiply":
case "Subtract":
let keys = control_names;
let values = guids;
if (guids.length === 1)
{
keys[1] = "Combo" + control_names[1];
values.push("Single numeric value");
}
keys.forEach((key, i) => control_settings["form" + key] = values[i]);
break;
}
return control_settings;
}
function generateOperatorPastTenseName(operator_name)
{
let output_name;
switch(operator_name) {
case "Average":
output_name = "averaged";
break;
case "Sum":
output_name = "summed";
break;
case "Divide":
output_name = "divided";
break;
case "Multiply":
output_name = "multiplied";
break;
case "Subtract":
output_name = "subtracted";
break;
}
return(output_name);
}
function generateDefaultCalculationOutputName(operator_name, output_type)
{
let output_name = generateOperatorPastTenseName(operator_name);
return output_name + "." + output_type;
}
function validRInputOrTable(selection)
{
let valid_table = selection.type === "Table" && selection.primary != null;
if (valid_table)
{
return true;
}
let valid_r_output = selection.type === "R Output" && selection.error == null
valid_r_output = valid_r_output && selection.outputClasses.some(r_class => VALID_ROUTPUT_CLASSES.includes(r_class));
return valid_r_output;
}
function applyCalculationOnValidSelections(operator, selections, is_displayr)
{
includeWeb("QScript R Output Functions");
let group = determineGroup(is_displayr);
let controls = determineControlSettings(operator, selections);
let calculation_output = group.appendStandardR("Calculation - " + operator + " - Table(s)", controls);
project.report.setSelectedRaw([calculation_output]);
return true;
}
function applySingleDimensionCalculation(operator = "Sum", dimension = "Row", selection = null, is_displayr)
{
let operator_name = operator + " Each " + dimension;
applyCalculationOnValidSelections(operator_name, selection, is_displayr);
return true;
}
function applyCalculationOnSelections(operator = "Sum")
{
let selections = project.report.selectedRaw();
selections = selections.filter(selected => selected.type !== "ReportGroup");
let initial_n_selected = selections.length;
let selected_vars = project.report.selectedVariables();
let n_selected_vars = selected_vars.length;
let is_displayr = (!!Q.isOnTheWeb && Q.isOnTheWeb());
let mathematical_operator = ["Divide", "Multiply", "Subtract"].includes(operator);
let max_number_inputs = mathematical_operator ? 2 : Infinity;
if (initial_n_selected === 0 && n_selected_vars === 0)
{
applyCalculationOnValidSelections(operator, null, is_displayr);
return true;
}
if (initial_n_selected > 0)
{
selections = selections.filter(validRInputOrTable);
let n_selected = selections.length;
if (n_selected > 0)
{
if (n_selected < initial_n_selected)
{
log("Some selections were not a Q Table or an R Output appropriate for use in " + operator + " and are ignored.");
}
if (n_selected > max_number_inputs)
{
log("Only two inputs can be used in " + operator + ". " +
"The first two selections have been used in the output and the other selections ignored.");
selections = selections.filter((item, i) => i < 2);
}
applyCalculationOnValidSelections(operator, selections, is_displayr);
return true;
}
let max_number_text = mathematical_operator ? "two" : "more";
let operator_past_tense_name = generateOperatorPastTenseName(operator);
let prefix_msg = initial_n_selected === 1 ? "The selected item is not" : "None of the current selections are";
log(prefix_msg + " appropriate to use in " + operator + ". Before re-running this feature, " +
"select one or " + max_number_text + " Q Table(s) or valid R Output(s) containing numeric " +
"elements to create an R Output with " + operator_past_tense_name + " elements, or " +
"select one or " + max_number_text + " variables with numeric values from the Data Sets tree " +
"to create a new variable with the cases " + operator_past_tense_name + ".");
return false;
}
if (mathematical_operator)
{
let selected_questions = project.report.selectedQuestions();
return appendCalculatedQuestionsToDataSet(operator, selected_questions, is_displayr, max_number_inputs);
} else
{
return appendCalculatedVariablesToDataSet(operator, selected_vars, is_displayr, max_number_inputs);
}
}
function applySingleDimensionCalculationWithSelection(operator = "Sum", dimension = "Row")
{
let selections = project.report.selectedRaw();
selections = selections.filter(selected => selected.type !== "ReportGroup");
let initial_n_selected = selections.length;
let selected_vars = project.report.selectedVariables();
let n_selected_vars = selected_vars.length;
let is_displayr = (!!Q.isOnTheWeb && Q.isOnTheWeb());
// Calculation by Column not valid on variables since the output needs to have the same number
// of elements as the number of cases in the data.
if (initial_n_selected === 0 && (n_selected_vars === 0 || dimension === "Column"))
{
applySingleDimensionCalculation(operator, dimension, null, is_displayr);
return true;
}
if (initial_n_selected > 0)
{
selections = selections.filter(validRInputOrTable);
let n_selected = selections.length;
if (n_selected > 0)
{
if (n_selected > 1)
{
log("Only a single selected input can be used in " + operator + " Each " + dimension + ". " +
"The first valid selection has been used in the output and the other selections ignored.");
selections = selections.filter((selection, i) => i < 1);
}
applySingleDimensionCalculation(operator, dimension, selections, is_displayr);
return true;
}
let prefix_msg = initial_n_selected === 1 ? "The selected item is not" : "None of the current selections are"
log(prefix_msg + " appropriate to use in " + operator + " Each " + dimension + ". Before re-running this feature, " +
"select a single Q Table or valid R Output.");
return false;
}
return appendCalculatedVariablesToDataSet(operator, selected_vars, is_displayr);
}