QScript Functions for Calculations

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!
includeWeb('QScript Utility Functions');
includeWeb('QScript R Output 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 determineVariableSelectionTypes(selections) {
    if (selections.every(selection => selection.type === 'Variable'))
        return 'all variables';
    if (selections.every(selection => selection.type === 'Question'))
        return selections.length > 1 ? 'all variable sets' : 'single variable set';
    return 'mixed';
}

function determineMatchingArg(variable_selection_types, selections, operator) {
    if (variable_selection_types === 'all variable sets')
        return '"Yes - show unmatched"';
    if (variable_selection_types === 'single variable set')
        return '"No"';
    let possible_labels = selections.map(item => {
        return item.type === 'Question' ? deduceVariableLabelsInVariableSet(item) : [item.label];
    });
    let common_labels = possible_labels.reduce((first, second) => first.filter(item => second.includes(item)));
    if (common_labels.length === 0)
        return '"No"';
    return '"Yes - show unmatched"';
}

// This function should only be called if the selections are all multi variable variable sets or
// are a mix of single variables and multi variable variable sets
function matchVariableSetsInRCode(operator, selections, variable_selection_types) {
    let selection_names = selections.map(determineRVariableSetName);
    let boolean_operator = ['Count', 'AnyOf', 'NoneOf'].includes(operator);
    let missing_name =  boolean_operator ? 'ignore.missing' : 'remove.missing';
    let warn_code = boolean_operator ? '' : 'input.args[["warn"]] <- warn';
    let matching_arg = determineMatchingArg(variable_selection_types, selections, operator);
    let match_labels = matching_arg !== '"No"';
    let variable_sets = selections.filter(x => x.type === 'Question');
    let original_nets = uniq(variable_sets.map(deduceOriginalNets).flat());
    original_nets = original_nets.length > 1 ? 'c("' + original_nets.join('", "') + '")' : '"' + original_nets[0] + '"';
    let r_code = `\ninput.args <- list(${selection_names.join(', ')})`;
    if (variable_selection_types === 'all variable sets') {
        let var_labels = selections.map(item => deduceVariableLabelsInVariableSet(item, true));
        var_labels = var_labels.map(labs => 'c("' + labs.map(str => str.replace('"', '\\"')).join('", "') + '")');
        r_code += `
original.variable.labels <- list(${var_labels.join(',\n                                 ')})

input.args <- CheckInputVariableLabelsChanged(input.args, original.variable.labels, function.name = "${operator}")
`;
    } else {
        if (match_labels)
            r_code += `
singleVariableAsDataFrame <- function(x) {
    if (!verbs:::isVariable(x)) return(x)
    y <- as.data.frame(x)
    var.label <- attr(x, "label")
    if (is.null(var.label))
        var.label <- attr(x, "name")
    colnames(y) <- var.label
    y
}
input.args <- lapply(input.args, singleVariableAsDataFrame)
`;
        let variable_set_index = selections.findIndex(x => x.type === 'Question');
        let var_labels = deduceVariableLabelsInVariableSet(selections[variable_set_index]);
        var_labels = 'c("' + var_labels.join('", "') + '")';
        variable_set_index++;
        r_code += `
original.variable.labels <- ${var_labels}

if (!setequal(GetVariableSetLabels(input.args[[${variable_set_index}]]), original.variable.labels))
    stop("The original variable set, ${selection_names[variable_set_index - 1]}, used when the variable was ",
         "first created with ", sQuote("${operator}"), " has changed. This is likely due to variable labels ",
         "changing. Delete this constructed variable set and re-run the ", sQuote("${operator}"),
         " with the appropriate variables selected.")
input.args[[${variable_set_index}]] <- subset(input.args[[${variable_set_index}]], select = ${var_labels})
`;
    }
    r_code += `
${warn_code}
input.args[["${missing_name}"]] <- ${missing_name}
input.args[["match.elements"]] <- ${matching_arg}
input.args[["remove.columns"]] <- ${original_nets}
`;
    return r_code;
}

function variadicOperatorOnVariablesRCode(operator = 'Sum', selections)
{
    let variable_selection_types = determineVariableSelectionTypes(selections);
    let r_code = `library(verbs)

remove.missing <- startsWith(formRemoveMissing, "Yes")
warn <- if (endsWith(formRemoveMissing, "(show warning)")) TRUE else "MuffleMissingValueWarning"
`;
    let variance_op = operator.startsWith('Variance') || operator.startsWith('Standard Deviation');
    let sample_code = variance_op ? (`sample = formCalculationFormula == "Sample",`) : '';
    if (['all variables', 'single variable set'].includes(variable_selection_types)) {
        let function_name;
        switch(operator)
        {
            case 'Minimum':
            case 'Maximum':
                function_name = operator.slice(0,3) + 'EachRow';
                break;
            default:
                function_name = operator.replace(' ', '') + 'EachRow';
        }
        let function_call = generateDefaultCalculationOutputName(operator, 'variables');
        function_call += ` <- ${function_name}(`;
        let white_space = ' '.repeat(function_call.length);
        sample_code = `\n${white_space}${sample_code}`;
        r_code += `
n.variables <- length(formInputs)
all.variables <- if (n.variables > 1L) QDataFrame(formInputs, check.names = FALSE) else formInputs[[1L]]

${function_call}all.variables,${sample_code}
${white_space}remove.missing = remove.missing,
${white_space}remove.columns = NULL,
${white_space}warn = warn)`;
        return r_code;
    }
    r_code += matchVariableSetsInRCode(operator, selections, variable_selection_types);
    if (variance_op) {
        r_code += `input.args[["sample"]] <- formCalculationFormaula == "Sample"`;
    }
    if (['Minimum', 'Maximum'].includes(operator))
        operator = operator.slice(0,3);
    r_code += `do.call(${operator}, input.args)`;
    return r_code;
}

function mathOperatorOnVariablesRCode(operator = 'Divide', selections)
{
    let variable_name;
    let args;
    switch(operator)
    {
        case 'Divide':
            variable_name = 'divided.variable';
            args = ['numerator', 'denominator'];
            break;
        case 'Multiply':
            variable_name = 'multiplied.variable';
            args = ['multiplicand', 'multiplier'];
            break;
        case 'Subtract':
            variable_name = 'subtracted.variable';
            args = ['minuend', 'subtrahend'];
            break;
    }
    let function_call = variable_name + ' <- ' + operator + '(';
    let white_space = ' '.repeat(function_call.length) ;
    function_call += args.join(', ') + ',\n';
    let cap_names = args.map(name => name.charAt(0).toUpperCase() + name.slice(1));
    let r_code = 'library(verbs)\n';
    let variable_selection_types = determineVariableSelectionTypes(selections);
    if (variable_selection_types === 'all variables')
    {
        let combo_choices = cap_names.map(name => 'formCombo' + name).join(', ');
        n_cases = selections[0].type === 'Variable' ? selections[0].question.dataFile.totalN : selections[0].dataFile.totalN;
        r_code += `
${args[0]} <- if (formCombo${cap_names[0]} == "Single numeric value") as.numeric(formSingle${cap_names[0]} ) else form${cap_names[0]}
${args[1]} <- if (formCombo${cap_names[1]} == "Single numeric value") as.numeric(formSingle${cap_names[1]} ) else form${cap_names[1]}
combo.choices <- c(${combo_choices})
if(all(combo.choices == "Single numeric value"))
{
    warning("The same single value from the calculation was used in all cases in the output variable")
    ${args[0]} <- rep(${args[0]}, ${n_cases})
    ${args[1]} <- rep(${args[1]}, ${n_cases})
}

${function_call}
${white_space}remove.rows = NULL, remove.columns = NULL,
${white_space}match.elements = "No", warn = TRUE)`;
    } else
    {
        let selection_names = selections.map(determineRVariableSetName);
        if (!allSelectionNamesValid(selections, selection_names, operator))
        {
            return false;
        }
        let n_selections = selections.length;
        let two_variable_sets = n_selections === 2 && variable_selection_types === 'all variable sets';
	    let matching_setting = two_variable_sets ? '"Yes"' : '"No"';
        let r_arguments = selection_names.map((name, index) => args[index] + ' <- ' + name + '\n');
	    if (n_selections === 1)
        {
            r_arguments.push(`${args[1]} <- as.numeric(formSingle${cap_names[1]})\n`);
        }
        r_code += r_arguments.join('') + '\n';
        let variable_nets = '"NET", "SUM", "Total"';
        if (two_variable_sets)
        {
            let var_labels = selections.map(item => deduceVariableLabelsInVariableSet(item, true));
            var_labels = var_labels.map(labs => 'c("' + labs.map(str => str.replace('"', '\\"')).join('", "') + '")');
            r_code += `
input <- list(${args[0]} = ${args[0]}, ${args[1]} = ${args[1]})
original.variable.labels <- list(${args[0]} = ${var_labels[0]},
                                 ${args[1]} = ${var_labels[1]})
input <- CheckInputVariableLabelsChanged(input, original.variable.labels, function.name = "${operator}")
${args[0]} <- input[["${args[0]}"]]
${args[1]} <- input[["${args[1]}"]]
`;
            let potential_nets = selections.map(deduceOriginalNets).flat();
            if (potential_nets.length > 0)
                variable_nets = '"' + potential_nets.join('", "') + '"';
        }
        r_code += `
${function_call}
${white_space}remove.rows = NULL, remove.columns = c(${variable_nets}),
${white_space}match.elements = ${matching_setting}, warn = TRUE)`;
    }
    return r_code;
}

function stringContainsNBSP(text)
{
    let seen_nbsp = false;
    for (let x = 0; x < text.length; x++)
    {
        if (text.charCodeAt(x) === 160)
        {
            seen_nbsp = true;
            break;
        }
    }
    return seen_nbsp;
}

function allSelectionNamesValid(selections, selection_names, operator)
{
    let n = selections.length
    let all_valid = true;
    for (i = 0; i < n; i++)
    {
        if (stringContainsNBSP(selection_names[i]))
        {
            all_valid = false;
            let var_str = selections[i].type === 'Variable' ? 'Variable' : 'Variable Set';
            msg = `The selected ${var_str} labelled "${selection_names[i]}" has a non-breaking space ` +
                  `character (unusual space). The non-breaking space character(s) needs to be removed before ` +
                  `${operator} can be calculated. Rename the ${var_str} by replacing or removing the ` +
                  `non-breaking spaces before attempting to recalculate using the current selections again.`;
            log(correctTerminology(msg));
            break;
        }
    }
    return all_valid;
}

function countOperatorOnVariablesRCode(operator, selections)
{
    let r_code = `library(verbs)

numeric.values <- get0("formNumericValues", ifnotfound = NULL)
ignore.missing <- get0("formIgnoreMissing", ifnotfound = FALSE)
categorical.input <- get0("formCategoricalLabels", ifnotfound = NULL)
`;
    // Convert "Any of" to "AnyOf" and "None of" to "NoneOf" for the R function call.
    switch (operator)
    {
        case 'Any of':
            operator = 'AnyOf';
            break;
        case 'None of':
            operator = 'NoneOf';
            break;
    }
    let variable_selection_types = determineVariableSelectionTypes(selections);
    if (['all variables', 'single variable set'].includes(variable_selection_types)) {
        let function_call = `${generateDefaultCalculationOutputName(operator, 'variables')} <- ${operator}EachRow(`;
        let white_space = ' '.repeat(function_call.length);
        r_code += `
n.variables <- length(formInputs)
all.variables <- if (n.variables > 1L) QDataFrame(formInputs, check.names = FALSE) else formInputs[[1L]]
categorical.labels <- if (!is.null(categorical.input)) ParseCategoricalLabels(categorical.input, all.variables)

elements.to.count <- list(numeric = numeric.values,
                          categorical = categorical.labels)

${function_call}all.variables,
${white_space}elements.to.count = elements.to.count,
${white_space}ignore.missing = ignore.missing,
${white_space}warn = TRUE)`;
        return r_code;
    }
    r_code += matchVariableSetsInRCode(operator, selections, variable_selection_types);
    r_code += `
n.inputs <- ${selections.length}L
categorical.labels <- if (!is.null(categorical.input)) ParseCategoricalLabels(categorical.input, input.args[1:n.inputs])

elements.to.count <- list(numeric = numeric.values,
                          categorical = categorical.labels)
input.args[["elements.to.count"]] <- elements.to.count
`
    return r_code + `do.call(${operator}, input.args)`;
}

function calculateVariableRCode(operator = 'Sum', selections)
{
    includeWeb('QScript R Output Functions');
    let r_code;
    switch(operator)
    {
        case 'Sum':
        case 'Average':
        case 'Maximum':
        case 'Minimum':
        case 'Standard Deviation':
        case 'Variance':
            r_code = variadicOperatorOnVariablesRCode(operator, selections);
           break;
        case 'Any of':
        case 'Count':
        case 'None of':
            r_code = countOperatorOnVariablesRCode(operator, selections);
            break;
        case 'Divide':
        case 'Multiply':
        case 'Subtract':
            r_code = mathOperatorOnVariablesRCode(operator, selections);
            break;
    }
    return r_code;
}

function variadicOperatorOnVariablesJSCode(operator, selections)
{
    let js_code = '';
    let variable_selection_types = determineVariableSelectionTypes(selections);
    if (['all variables', 'single variable set'].includes(variable_selection_types)) {
        js_code += `
form.dropBox({name: 'formInputs',
              label: 'Variables',
              duplicates: true,
              types: ['Variable:Numeric,Categorical,OrderedCategorical,Money'],
              multi:true,
              prompt: 'Input variables that are not text or date variables'});
`;
    }
    js_code += `
form.comboBox({name: 'formRemoveMissing',
               alternatives: ['Yes (show warning)', 'No', 'Yes'],
               label: 'Calculate for cases with incomplete data',
               prompt: 'If set to \\'Yes\\', any missing values are removed from the data before the calculation occurs. ' +
                       'If set to \\'No\\', cases with any missing values will be assigned a missing value. ' +
                       'Cases whose values are entirely missing, will always be assigned a missing value ' +
                       'regardless of this setting.',
               default_value: 'Yes (show warning)'});
`;
    if (operator.startsWith('Variance') || operator.startsWith('Standard Deviation'))
    {
        let operator_label = operator.startsWith('Variance') ? 'Variance' : 'Standard Deviation';
        js_code += `
form.comboBox({name: 'formCalculationFormula',
               label: '${operator_label} formula',
               alternatives: ['Population', 'Sample'],
               prompt: 'Divides by n in the population formula or (n - 1) in the sample formula',
               default_value: 'Sample'});
`;
    }
    js_code += `form.setHeading('${operator}');`;
    return js_code;
}

function mathOperatorOnVariablesJSCode(operator = 'Divide', selections)
{
    let arg_names;
    let label_names;
    switch(operator)
    {
        case 'Divide':
            arg_names = ['Numerator', 'Denominator'];
            label_names = ['Divide the', 'by the'];
            break;
        case 'Multiply':
            arg_names = ['Multiplicand', 'Multiplier'];
            label_names = ['Multiply the', 'by the'];
            break;
        case 'Subtract':
            arg_names = ['Minuend', 'Subtrahend'];
            label_names = ['From the', 'Subtract the'];
            break;
    }
    let default_value = operator === 'Subtract' ? '0' : '1';
    let js_code;
    let all_variables = !!selections && selections.every(selection => selection.type === 'Variable');
    if (all_variables)
    {
        js_code = `
const ALLOWED_VAR_TYPES = ['Numeric', 'Categorical', 'Ordered Categorical', 'Money'];

let allowed_vars = ALLOWED_VAR_TYPES.join(', ');
let input_structure = {'names': ['${arg_names[0]}', '${arg_names[1]}'],
                       'labels': ['${label_names[0]}', '${label_names[1]}']};

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: ['Variable', 'Single numeric value'],
                                       default_value: 'Variable',
                                       prompt: 'Choose a variable in the dataset or specify a single value'});
    if (combo_control.getValue() === 'Single numeric value')
    {
        form.textBox({name: 'formSingle' + name, label: '', type: 'number',
                      default_value: ${default_value},
                      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: ['Variable: ' + allowed_vars],
                                               error: 'Please select an input variable.',
                                               multi: false, prompt: 'Input Variable'});
    }
}
form.setHeading('${operator}');`;
    } else if (selections.length === 1)
    {
        js_code = `
form.textBox({name: 'formSingle${arg_names[1]}',
              label: 'The single numeric value to ${operator} by', type: 'number',
              default_value: ${default_value},
              error: 'The ${arg_names[1]} here cannot be empty and must be a single numeric value',
              prompt: 'The single value to be used in the calculation'});

form.setHeading('${operator}');`;
    }
    return js_code;
}

function countOperatorOnVariablesJSCode(operator, selections)
{
    let js_code = `let user_inputs = [];
const ALLOWED_TYPES_FOR_LISTBOX = ['Nominal', 'Nominal - Multi',
                                   'Ordinal', 'Ordinal - Multi'];

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

let selected_variables = [];
let categorical_variables = [];
let numeric_variables = [];
let variables_selected = false;
`
    let operation_in_prompts;
    switch(operator)
    {
        case 'Any of':
            operation_in_prompts = 'included';
            break;
        case 'Count':
            operation_in_prompts = 'omitted';
            break;
        case 'None of':
            operation_in_prompts = 'counted';
            break;
    }
    let variable_set_selection_type = determineVariableSelectionTypes(selections);
    if (['all variables', 'single variable set'].includes(variable_set_selection_type)) {
        js_code += `
user_inputs = form.dropBox({name: 'formInputs',
                            label: 'Input Variable(s)',
                            duplicates: true,
                            types: ['Variable: Numeric, Categorical, Ordered Categorical, Money'],
                            multi: true,
                            prompt: 'Input Variables'}).getValues();
`;
    }
    let all_variables = selections.map(item => item.type === 'Question' ? item.variables.flat() : item).flat();
    let defaults = determineDefaultInputsForCountOperators(all_variables);
    if (variable_set_selection_type === 'all variables') {
        js_code += `
if (user_inputs.length > 0)
{
    let selected_guids = user_inputs.map(selection => selection.guid);
`
    } else {
        js_code += `
if (true)
{
    let selected_guids = ['${all_variables.map(variable => variable.guid).join("', '")}'];
`;
    }
    js_code += `
    var all_variables = project.dataFiles.map(dat => dat.variables).flat();
    selected_variables = all_variables.filter(variable => selected_guids.includes(variable.guid));
    selected_variables.forEach(variable => {
        if (ALLOWED_TYPES_FOR_LISTBOX.includes(variable.question.variableSetStructure))
            categorical_variables.push(variable);
        else
            numeric_variables.push(variable);
    });
}

let has_categorical_labels = categorical_variables.length > 0;
let categorical_listbox;
let counting_missing_values = false;
if (has_categorical_labels)
{
    form.group('Categories to count (Categorical Variables)');
    let categorical_textbox = form.textBox({name: 'formCategoricalLabels', label: 'Categorical labels',
                                            required: true,
                                            default_value: '${defaults["categorical"]}',
                                            prompt: 'Specify the categories to be ${operation_in_prompts} ' +
                                                    'when creating the output variable as a semi colon or ' +
                                                    'comma separated list. E.g. Apples; Oranges or '+
                                                    'Apples, Oranges.'}).getValue();
    categorical_textbox = categorical_textbox.split(',').map(txt => txt.trim());
    if (categorical_textbox.length > 0)
        counting_missing_values = categorical_textbox.includes('NA');
}
let has_numeric_variables = numeric_variables.length > 0;
let values_to_count = [];
let unique_values;
if (has_numeric_variables)
{
    form.group('Values to count (Numeric Variables)');
    values_to_count = form.textBox({name: 'formNumericValues',
                                    label: 'Values to count',
                                    default_value: '${defaults['numeric']}',
                                    prompt: 'Specify the values to be ${operation_in_prompts} when creating the ' +
                                            'output variable. E.g. NA, Inf, 1-3, 5, 6, <=-1, >15'}).getValue();
    if (!!values_to_count)
    {
        values_to_count = values_to_count.split(',');
        values_to_count = values_to_count.map(values => values.trim());
    }
    counting_missing_values = counting_missing_values || (!!values_to_count && values_to_count.includes('NA'));
}
if (!counting_missing_values)
{
    form.checkBox({name:'formIgnoreMissing',
                   label:'Calculate for cases that have incomplete data',
                   prompt:'Allow calculation to proceed even if there are cases with missing values',
                   default_value: true});
}

form.setHeading('${operator}');`;
    return js_code;
}

function calculateVariableJSCode(operator = 'Sum', selections = null)
{
    let js_code;
    switch(operator)
    {
        case 'Average':
        case 'Maximum':
        case 'Minimum':
        case 'Sum':
        case 'Standard Deviation':
        case 'Variance':
            js_code = variadicOperatorOnVariablesJSCode(operator, selections);
            break;
        case 'Any of':
        case 'Count':
        case 'None of':
            js_code = countOperatorOnVariablesJSCode(operator, selections);
            break;
        case 'Divide':
        case 'Multiply':
        case 'Subtract':
            js_code = mathOperatorOnVariablesJSCode(operator, selections);
            break;
    }
    return js_code;
}

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

function extractQuestionsWithValidStructure(selected_variable_sets, operator, max_n_valid)
{
    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_set);
        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' : 'Variable Set';
    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(`, `);
        let msg;
        if (all_invalid_variable_sets)
        {
            let appropriate_vars = `Number, Categorical, Ordered Categorical or Money`;
            let msg_suffix = `. Please select ${structure_name}s containing ${appropriate_vars} variables to use ${operator}.`;
            if (invalid_variable_sets.length == 1)
            {
                msg = `The selected ${structure_name} ${invalid_names} is a ${invalid_types} ${structure_name} and ` +
                      `is not appropriate to use in ${operator}${msg_suffix}`;
            }
            else
            {
                msg = `All of the selected ${structure_name}s are ${invalid_types} ${structure_name}s and are not ` +
                      `appropriate to use in ${operator}${msg_suffix}`;
            }
        } else
        {
            let single_invalid = invalid_variable_sets.length === 1;
            let is_are = single_invalid ? ` selected is` : `s selected are`;
            let has_have = single_invalid ? `has` : `have`
            msg = `The ${invalid_types} ${structure_name + is_are} not appropriate to use in ${operator} and `+
                  `${has_have} been removed from the calculation.`;
        }
        log(correctTerminology(msg));
    }
    if (n_valid_variable_sets > max_n_valid)
    {
        selected_variable_sets = [0, 1].map(i => selected_variable_sets[i]);
        warnAboutOnlyTwoSelections(structure_name, operator);
    }
    return selected_variable_sets;
}

function warnAboutOnlyTwoSelections(structure_name, operator)
{
    let msg = `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.`;
    log(correctTerminology(msg));
}

function extractValidVariables(selected_variables, operator)
{
    let invalid_vars = [];
    selected_variables = selected_variables.filter(variable => {
        let var_type = variable.variableType;
        let invalid_var = var_type  === 'Text' || var_type === 'Date';
        if (invalid_var)
            invalid_vars.push(variable);
        return !invalid_var;
    });
    let all_invalid_variables_selected = selected_variables.length === 0;
    if (invalid_vars.length > 0)
    {
        let invalid_types = uniq(invalid_vars.map(variable => variable.variableType)).join(' and ');
        let bad_names_and_labels = invalid_vars.map(variable => variable.label + ' (' + variable.name + ')').join(', ');
        let msg;
        if (all_invalid_variables_selected)
        {
            if (invalid_vars.selected == 1)
            {
                msg = `The selected variable ${bad_names_and_labels} is a ${invalid_types} variable and ` +
                      `is not appropriate to use in ${operator}.`;
            }
            else
            {
                msg = `All of the selected variables are either Text or Date variables and are not appropriate ` +
                      `to use in ${operator}. Please select 'Number, Categorical, Ordered Categorical or Money'` +
                      `to use in ${operator}.`;
            }
        } else
        {
            let var_msg = invalid_vars.length === 1 ? ' variable selected is' : ' variables selected are';
            msg = `The ${invalid_types + var_msg} not appropriate to use in ${operator} and have been removed ` +
                  `from the calculation.`;
        }
        log(correctTerminology(msg));
    }
    return selected_variables;
}

function determineVariableName(operator, variable_names, data_file)
{
    let operator_name;
    switch(operator)
    {
        case 'Any of':
            operator_name = 'any';
            break;
        case 'Average':
            operator_name = 'avg';
            break;
        case 'Count':
            operator_name = 'count';
            break;
        case 'Divide':
            operator_name = 'division';
            break;
        case 'Maximum':
            operator_name = 'max';
            break;
        case 'Minimum':
            operator_name = 'min';
            break;
        case 'Multiply':
            operator_name = 'multiplication';
            break;
        case 'None of':
            operator_name = 'none';
            break;
        case 'Standard Deviation':
            operator_name = 'stddev';
            break;
        case 'Subtract':
            operator_name = 'subtraction';
            break;
        case 'Sum':
            operator_name = 'sum';
            break;
        case 'Variance':
            operator_name = 'variance';
            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;
}

// Extract either the name or label property
// Expected input is either a Question or Variable.
// For questions it returns the question name.
// For a variable it will return the label except in the case the label is blank
// where the variable name is used instead.
function determineOutputLabel(selection)
{
    if (selection.type === 'Question')
        return selection.name
    // Selection should be a variable from this point.
    return selection.label !== '' ? selection.label : selection.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 'Any of':
        case 'None of':
            new_variable_label = operator + ' ' + labels.join('; ');
            break;
        case 'Average':
        case 'Count':
        case 'Minimum':
        case 'Maximum':
        case 'Standard Deviation':
        case 'Variance':
            new_variable_label = operator + ' of ' + labels.join('; ');
            break;
        case 'Sum':
            new_variable_label = (labels.length == 1) ? (operator + ' of ' + labels[0]) : labels.join(' + ');
            break;
        case 'Divide':
        case 'Multiply':
            operator_name = generateOperatorPastTenseName(operator);
        case 'Subtract':
            operator_name = operator === 'Subtract' ? 'minus' : operator_name + ' by';
            labs = labels.length === 2 ? labels.join(operator_char) : `${labels[0]} ${operator_name} a single value`;
            new_variable_label += labs;
            break;
    }
    new_variable_label = preventDuplicateQuestionName(data_file, new_variable_label);
    return new_variable_label;
}
function deduceVariableLabelsInVariableSet(variable_set, with_nets = true)
{
    let variable_labels;
    switch(variable_set.variableSetStructure)
    {
        case 'Binary - Multi':
        case 'Numeric - Multi':
        case 'Nominal - Multi':
        case 'Ordinal - Multi':
            return deduceMultiVariableLabels(variable_set, with_nets);
        case 'Numeric - Grid':
        case 'Binary - Grid':
            return deduceGridVariableLabels(variable_set, with_nets);
    }
    return variable_labels;
}

function deduceGridVariableLabels(variable_set, with_nets = true)
{
    let data_reduction = variable_set.dataReduction;
    let is_transposed = data_reduction.transposed;
    let prefix_labels = is_transposed ? data_reduction.rowLabels : data_reduction.columnLabels;
    let prefix_net_indices = is_transposed ? data_reduction.netRows : data_reduction.netColumns;
    let suffix_labels = is_transposed ? data_reduction.columnLabels : data_reduction.rowLabels;
    let suffix_net_indices = is_transposed ? data_reduction.netColumns : data_reduction.netRows;
    if (!with_nets)
    {
        if (!!prefix_net_indices)
            prefix_labels = prefix_labels.filter((item, i) => !prefix_net_indices.includes(i));
        if (!!suffix_net_indices)
            suffix_labels = suffix_labels.filter((item, i) => !suffix_net_indices.includes(i));
    }
    let labels = prefix_labels.map(prefix => suffix_labels.map(suffix => prefix + ', ' + suffix)).flat();
    return labels;
}

function deduceOriginalNets(variable_set) {
    let net_variable_labels;
    switch(variable_set.variableSetStructure) {
        case 'Nominal - Multi':
        case 'Ordinal - Multi':
            return null;
        case 'Binary - Multi':
        case 'Numeric - Multi':
            return deduceMultiVariableSetOriginalNets(variable_set);
        case 'Numeric - Grid':
        case 'Binary - Grid':
            return deduceGridVariableSetOriginalNets(variable_set);
    }
    return net_variable_labels;
}

function deduceMultiVariableSetOriginalNets(variable_set)
{
    let data_reduction = variable_set.dataReduction;
    let transposed = data_reduction.transposed;
    let labels = transposed ? data_reduction.columnLabels : data_reduction.rowLabels;
    let net_indices = transposed ? data_reduction.netColumns : data_reduction.netRows;
    if (net_indices.length === 0)
        return net_indices;
    return labels.filter((item, i) => net_indices.includes(i));
}

function deduceGridVariableSetOriginalNets(variable_set)
{
    let grid_variable_set_labels_with_nets = deduceGridVariableLabels(variable_set, true);
    let grid_variable_set_labels_without_nets = deduceGridVariableLabels(variable_set, false);
    return grid_variable_set_labels_with_nets.filter(x => !grid_variable_set_labels_without_nets.includes(x));
}

function deduceMultiVariableLabels(variable_set, with_nets = false)
{
    let data_reduction = variable_set.dataReduction;
    let labels_in_columns = variable_set.variableSetStructure === 'Nominal - Multi' && !data_reduction.transposed;
    let labels = labels_in_columns ? data_reduction.columnLabels : data_reduction.rowLabels;
    let net_indices = labels_in_columns ? data_reduction.netColumns : data_reduction.netRows;
    if (!with_nets && net_indices.length > 0)
    {
        labels = labels.filter((item, i) => !net_indices.includes(i));
    }
    return labels;
}

function someVariableLabelsMatch(variable_sets)
{
    let variable_set_labels = variable_sets.map(deduceMultiVariableLabels);
    let common_labels = variable_set_labels.reduce((first, second) => first.filter(item => second.includes(item)));
    return common_labels.length > 0;
}

function determineRVariableSetName(x)
{
    return checkDuplicateReferenceName(x.name, x.type) ? disambiguateReferenceName(x) : stringToRName(x.name);
}

function checkDuplicateReferenceName(selection_name, selection_type)
{
    let all_selections;
    if (selection_type === 'Question')
    {
        all_selections =  project.dataFiles.map(dat => dat.questions).flat();
    } else
    {
        all_selections =  project.dataFiles.map(dat => dat.variables).flat();
    }
    let selections = all_selections.filter(selection => selection.name === selection_name);
    return selections.length !== 1;
}

function disambiguateReferenceName(selection)
{
    let selection_name = selection.name;
    let selection_type = selection.type;
    let data_file = selection.type === 'Variable' ? selection.question.dataFile : selection.dataFile;
    let datafile_name = data_file.name;
    return stringToRName(datafile_name) + '$' + selection_type + 's$' + stringToRName(selection_name);
}

function invalidVariableSetsFound(selections, operator)
{
    selected_questions = selections.map(selection => selection.type === 'Question' ? selection : selection.question);
    let invalid_variable_sets_found = selected_questions.some(selection => !selection.isValid);
    if (invalid_variable_sets_found)
    {
        let invalid_variable_sets = selections.filter(selection => !selection.isValid);
        let invalid_names = uniq(invalid_variable_sets.map(vs => vs.name));
        let n_invalid = invalid_names.length;
        invalid_names = invalid_names.length === 1 ? invalid_names[0] : '(' + joinStrings(invalid_names) + ')';
        let tense = n_invalid === 1 ? 'is' : 'are';
        let msg = `The selected Variable Set, ${invalid_names}, ${tense} invalid and needs to be corrected ` +
                  `into a valid state. Select only valid Variable Sets before attempting to run ` +
                  `${operator} again.`;
        log(correctTerminology(msg));
        return true;
    }
    if (selected_questions.some(selection => selection.isHidden))
    {
        log('One or more of the selected variables are marked as \'Hidden\' and cannot be used. ' +
            'Unhide these variables and run this feature again.');
        return true;
    }
    return false;
}

function extractValidVariablesForOperation(selected_questions, selected_variables, operator)
{
    const mathematical_operator = ['Divide', 'Multiply', 'Subtract'].includes(operator);
    const SUPPORTED_SETS = ['Numeric - Multi', 'Nominal - Multi', 'Ordinal - Multi',
                            'Binary - Multi', 'Numeric - Grid', 'Binary - Grid'];

    let max_n_valid = mathematical_operator ? 2 : Infinity;
    selected_questions = extractQuestionsWithValidStructure(selected_questions, operator, max_n_valid);
    selected_variables = selected_variables.filter(variable => !['Text', 'Date'].includes(variable.variableType));
    let guids = selected_variables.map(variable => variable.guid);
    let relevant_items = selected_questions.map(question => question.variables.filter(v => guids.includes(v.guid)));
    let final_selections = [];
    let complete_variable_sets = [];
    let complete_selections = relevant_items.map(selection => {
        let n_variables = selection.length;
        if (n_variables === 1)
        {
            final_selections.push(selection[0]);
            return true;
        }
        if (n_variables === selection[0].question.variables.length)
        {
            complete_variable_sets.push(selection[0].question);
            final_selections.push(selection[0].question);
            return true;
        }
        return false;
    });
    if (invalidVariableSetsFound(relevant_items.flat(), operator))
    {
        return false;
    }
    if (complete_variable_sets.length > 0)
    {
        if (selectedVariableSetsInvalid(complete_variable_sets)) {
           return false;
        }
    }
    let n_selected_questions = selected_questions.length;
    let n_selected_variables = selected_variables.length;
    let all_complete_selections = complete_selections.every(i => i);
    if (n_selected_questions === 1 && (!all_complete_selections || (all_complete_selections && n_selected_variables === 2)))
    {
        final_selections = selected_variables;
    }
    let structure_name = 'Variable Set';
    let number_selected_variable_sets = final_selections.reduce((n_variable_sets, selection) => {
        return selection.type === 'Question' ? n_variable_sets + 1 : n_variable_sets;
    }, 0);
    if (number_selected_variable_sets > 1)
    {
        let unsupported_structures = [];
        final_selections = final_selections.filter(selection => {
            if (selection.type === 'Variable')
                return true;
            let current_structure = selection.variableSetStructure;
            let supported_structure = SUPPORTED_SETS.includes(current_structure);
            if (!supported_structure)
            {
                unsupported_structures.push(current_structure);
                return false;
            }
            return true;
        });
        if (unsupported_structures.length > 0)
        {
            let supported_sets = inDisplayr() ? SUPPORTED_SETS : SUPPORTED_SETS.filter(v => v != 'Ordinal - Multi');
            let all_supported_structures = joinStrings(supported_sets);
            let all_unsupported_structures = joinStrings(uniq(unsupported_structures));
            let msg = `Only ${all_supported_structures} Variable Sets are supported when using ${operator} on ` +
                      `more than one Variable Set. ${all_unsupported_structures} Variable Sets are not ` +
                      `supported in ${operator} when more than one Variable Set is selected. Select either `+
                      `supported Variable Sets or select other variables before running ${operator} again.`;
            log(correctTerminology(msg));
            return false;
        }
    }
    if (isFinite(max_n_valid) && final_selections.length > max_n_valid)
    {
        final_selections = [0, 1].map(i => final_selections[i]);
        if (final_selections.every(item => item.type === 'Variable'))
            structure_name = 'variable';
        warnAboutOnlyTwoSelections(structure_name, operator);
    }
    if (!all_complete_selections && n_selected_questions > 1)
    {
        let n_var_sets = (isFinite(max_n_valid) && max_n_valid == 2) ? 'two' : 'multiple';
        let msg = `Selecting some variables inside a ${structure_name} when ${n_var_sets} ${structure_name}s are ` +
                  `selected is not supported for ${operator}. Please select a single variable, a single variable ` +
                  `within a ${structure_name} or an entire ${structure_name} with all variables selected ` +
                  `before re-running ${operator}.`;
        log(correctTerminology(msg));
        return false;
    }
    let all_multi_variable_sets = final_selections.every(x => x.type === 'Question' && x.variables.length > 1);
    let two_multi_variable_sets = all_multi_variable_sets && final_selections.length === 2;
    if (two_multi_variable_sets && !someVariableLabelsMatch(final_selections))
    {
        let msg = `To use ${operator} with two ${structure_name}s, both ${structure_name}s need to contain variables ` +
                  `with at least some common labels so they can be matched. The selected ${structure_name}s, ` +
                  `${final_selections.map(vs => vs.name).join(' and ')}, do not have any variables with common ` +
                  `variable labels and cannot be matched. Select ${structure_name}s that have common labels before ` +
                  `using ${operator} again.`;
        log(correctTerminology(msg));
        return false;
    }
    let all_variables = final_selections.every(x => x.type === 'Variable');
    if (!mathematical_operator && final_selections.length > 2 && !(all_variables || all_multi_variable_sets)) {
        let msg = `It is not supported to select multiple ${structure_name}s and individual variables when using ` +
                  `${operator}. If more than 2 inputs are desired, they need to be all individual variable ` +
                  `selections or all entire ${structure_name}s. Select single variables or entire ${structure_name}s` +
                  `or use fewer than two inputs before using ${operator} again.`;
        log(correctTerminology(msg));
        return false;
    }
    return final_selections;
}

function joinStrings(string_array, last_separator = ' and ')
{
    return string_array.join(', ').replace(/, ((?:.(?!, ))+)$/, last_separator + '$1');
}

function calculateStandardRVariable(operator, selections)
{
    let valid_selections = extractValidVariablesForOperation(selections.questions, selections.variables, operator);
    if (!valid_selections) {
        return false;
    }
    let n_selections = valid_selections.length;
    if (n_selections > 0)
    {
        if (!dataFileIsValid(valid_selections))
        {
            return false;
        }
        let last_selection = valid_selections[valid_selections.length - 1];
        let last_question = last_selection.type === 'Question' ? last_selection : last_selection.question;
        let last_question_variables = last_question.variables;
        let last_variable = last_question_variables[last_question_variables.length - 1];
        let data_file = last_variable.question.dataFile;
        let selection_names = valid_selections.map(determineOutputLabel);
        let new_question_name = determineVariableLabel(operator, selection_names, data_file);
        let new_variable_base_name = determineVariableName(operator, selection_names, data_file);
        new_variable_base_name = cleanVariableName(new_variable_base_name);
        let is_variable_set_output = valid_selections.some(selection => selection.type === 'Question');
        let is_single_variable_set = valid_selections.length === 1 && valid_selections[0].type === 'Question';
        let is_mathematical_operator = ['Divide', 'Multiply', 'Subtract'].includes(operator);
        if (is_single_variable_set && !is_mathematical_operator)
            is_variable_set_output = false;
        if (is_variable_set_output)
            new_variable_base_name = preventDuplicateVariableBaseName(data_file, new_variable_base_name)
        else
            new_variable_base_name = preventDuplicateVariableName(data_file, new_variable_base_name);
        let js_code = calculateVariableJSCode(operator, valid_selections);
        let r_code = calculateVariableRCode(operator, valid_selections);
        if (typeof r_code === 'boolean')
        {
            return false;
        }
        let control_settings = determineControlSettings(operator, valid_selections);
        let new_question = data_file.newRQuestion(r_code, new_question_name, new_variable_base_name, last_variable,
                                                  js_code, control_settings);
        if (['Any of', 'None of'].includes(operator)) {
            if (new_question.variables.length > 1) {
                new_question.variableSetStructure = 'Binary - Multi';
                new_question.needsCheckValuesToCount = false;
            } else {
                new_question.variableType = 'Categorical';
            }
        }
        insertAtHoverButtonIfShown(new_question);
        if (new_question.variables.length > 1)
        {
            project.report.setSelectedRaw([new_question.variables[0]]);
        }
        return true;
    }
    return false;
}

function dataFileIsValid(selections)
{
    let data_files = selections.map(x => x.type === 'Question' ? x.dataFile : x.question.dataFile);
    let datafile_names = uniq(data_files.map(datafile => datafile.name));
    if (datafile_names.length === 1)
    {
        return true;
    }
    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(' and ')} 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;
        }
    }
    return true;
}

function appendCalculatedVariablesToDataSet(operator, selected_variables)
{
    let valid_variables = extractValidVariables(selected_variables, operator);
    let n_valid_variables = valid_variables.length;
    if (n_valid_variables > 0)
    {
        if (!dataFileIsValid(valid_variables))
        {
            return false;
        }
        if (invalidVariableSetsFound(valid_variables, operator))
        {
            return false;
        }
        let last_variable = valid_variables[n_valid_variables - 1];
        let data_file = last_variable.question.dataFile;
        let labels = valid_variables.map(determineOutputLabel);
        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, valid_variables);
        let js_code = calculateVariableJSCode(operator, valid_variables);
        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);
        if (['Any of', 'None of'].includes(operator))
            new_variable.variableType = 'Categorical';
        insertAtHoverButtonIfShown(new_variable.question);
        return true;
    }
    return false;
}

const COUNT_OPERATORS_THAT_NEED_TEXTBOX = ['Any of', 'Any of Each Row', 'Any of Each Column',
                                           'Count', 'Count Each Row', 'Count Each Column',
                                           'None of', 'None of Each Row', 'None of Each Column'];

function determineControlSettings(operator = 'Sum', selections = null)
{
    let control_settings = {};
    if (selections == null)
        return control_settings;
    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;
    }
    let some_variables_selected = !!selections && selections.some(selection => ['Variable', 'Question'].includes(selection.type));
    let all_variables_selected = some_variables_selected && selections.every(x => x.type === 'Variable');
    let dont_populate_formInputs = some_variables_selected && !all_variables_selected;
    if (selections[0].type === 'Question' && selections.length === 1) {
        all_variables_selected = true;
        selections = selections[0].variables;
        dont_populate_formInputs = false;
    }
    let guids = selections.map(selection => selection.guid);
    switch(operator)
    {
        case 'Any of':
        case 'Average':
        case 'Count':
        case 'Minimum':
        case 'Maximum':
        case 'None of':
        case 'Standard Deviation':
        case 'Sum':
        case 'Variance':
            if (dont_populate_formInputs)
                return null;
            control_settings = {'formInputs': guids.join(';')};
            break;
        case 'Any of Each Column':
        case 'Any of Each Row':
        case 'Average Each Column':
        case 'Average Each Row':
        case 'Count Each Column':
        case 'Count Each Row':
        case 'Minimum Each Column':
        case 'Minimum Each Row':
        case 'Maximum Each Column':
        case 'Maximum Each Row':
        case 'None of Each Column':
        case 'None of Each Row':
        case 'Sum Each Column':
        case 'Sum Each Row':
        case 'Standard Deviation Each Column':
        case 'Standard Deviation Each Row':
        case 'Variance Each Column':
        case 'Variance Each Row':
            if (dont_populate_formInputs)
                return null;
            control_settings = {'formInput': guids[0]};
            break;
        case 'Divide':
        case 'Multiply':
        case 'Subtract':
            let all_variable_sets = selections.every(selection => selection.type === 'Question');
            let some_variable_sets = all_variable_sets || selections.some(x => x.type === 'Question');
            if (all_variable_sets || (some_variable_sets && selections.length === 2))
            {
                return null;
            }
            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;
    }
    if (!['Variable', 'Question'].includes(selections[0].type) && COUNT_OPERATORS_THAT_NEED_TEXTBOX.includes(operator))
    {
        let table_defaults = selections.map(extractCategoricalOrNumericValuesForTableInputs).flat();
        let numeric_defaults = [];
        table_defaults.forEach(item => {
            if (item == null)
                return null;
            if (item['type'] === 'numeric')
            {
                numeric_defaults.push(item['values']);
            }
        });
        if (numeric_defaults.length > 0)
        {
            let range = numeric_defaults.reduce((x, y) => [Math.min(x[0], y[0]), Math.max(x[1], y[1])]);
            let infinity_found = range.some(x => !isFinite(x));
            if (infinity_found)
                default_range = convertToOpenInterval(range);
            else
            {
                if (range[0] === range[1])
                    range = [range[0]];
                range = range.map(convertNumToRString);
                default_range = range.join('-');
            }
            control_settings['formNumericValues'] = default_range;
        }
    }
    return control_settings;
}

function generateOperatorPastTenseName(operator_name)
{
    let output_name;
    switch(operator_name) {
        case 'Any of':
        case 'AnyOf':
            output_name = 'anyof';
            break;
        case 'Average':
            output_name = 'averaged';
            break;
        case 'Count':
            output_name = 'counted';
            break;
        case 'Divide':
            output_name = 'divided';
            break;
        case 'Multiply':
            output_name = 'multiplied';
            break;
        case 'None of':
        case 'NoneOf':
            output_name = 'noneof';
            break;
        case 'Minimum':
            output_name = 'minimized';
            break;
        case 'Maximum':
            output_name = 'maximized';
            break;
        case 'Multiply':
            output_name = 'multiplied';
            break;
        case 'Subtract':
            output_name = 'subtracted';
            break;
        case 'Sum':
            output_name = 'summed';
            break;
        case 'Standard Deviation':
        case 'StandardDeviation':
            output_name = 'standard.dev';
            break;
        case 'Variance':
            output_name = 'variance';
            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)
{
    let controls = determineControlSettings(operator, selections);
    let page_name = `Calculation - ${operator} - Table(s)`;
    addStandardRToCurrentPage(page_name, controls);
    return true;
}

function applySingleDimensionCalculation(operator = 'Sum', dimension = 'Row', selection = null)
{
    let operator_name = `${operator} Each ${dimension}`;
    applyCalculationOnValidSelections(operator_name, selection);
    return true;
}

function deduceAppropriateSelections()
{
    includeWeb('QScript Selection Functions');
    let user_selections = getAllUserSelections();
    let selected_items = user_selections.selected_items;
    let selected_variables = user_selections.selected_variables;
    let selected_questions = user_selections.selected_questions;
    let v_and_q_selected_questions = user_selections.v_and_q_selected_questions;
    let v_and_q_selected_variables = user_selections.v_and_q_selected_variables;
    return { outputs: selected_items,
             variables: selected_variables,
             questions: selected_questions,
             v_and_q_selected_questions: v_and_q_selected_questions,
             v_and_q_selected_variables: v_and_q_selected_variables};
}

function duplicatedVariableLabels(variable_set) {
    let variable_labels = variable_set.variables.map(v => v.label);
    let findDuplicates = arr => arr.filter((item, index) => arr.indexOf(item) != index)
    let duplicated_labels = uniq(findDuplicates(variable_labels));
    if (duplicated_labels.length > 0) {
        let msg = 'In the Variable Set \'' + variable_set.name + '\' there are variables with duplicated labels. ' +
                  'The Calculation feature cannot automatically match variables if variables with the same label ' +
                  'exist. Please remove the variables with the same label or change them so the labels are ' +
                  'unique and run this feature again. The variables with the same labels are \'' +
                  duplicated_labels.join('\', \'') + '\'.';
        log(correctTerminology(msg));
        return true;
    }
    return false;
}

function selectedVariableSetsInvalid(variable_sets, check_duplicates = true) {
    let all_variables = variable_sets.map(vs => vs.variables).flat();
    if (all_variables.some(x => x.isHidden)) {
        log('One or more of the selected variables are marked as \'Hidden\' and cannot be used. ' +
            'Unhide these variables and run this feature again.');
        return true;
    }
    if (variable_sets.length === 1 || !check_duplicates)
        return false;
    let variable_sets_invalid = variable_sets.some(duplicatedVariableLabels);
    return variable_sets_invalid;
}

function calculateStandardR(operator, selections) {
    const mathematical_operator = ['Divide', 'Multiply', 'Subtract'].includes(operator);
    const max_number_inputs = mathematical_operator ? 2 : Infinity;
    selections = selections.outputs;
    let initial_n_selected = selections.length;
    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 Table or a Calculation 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 = [0, 1].map(i => selections[i]);
        }
        applyCalculationOnValidSelections(operator, selections);
        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} Table(s) or valid Calculation(s) containing numeric elements to create a ` +
        `Calculation 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;
}

function applyCalculationOnSelections(operator = 'Sum')
{
    const in_displayr = inDisplayr();
    let selections = deduceAppropriateSelections();
    let selected_items = selections.outputs;
    let selected_variables = selections.variables;
    let selected_variable_sets = selections.questions;

    // Check variables and questions tab selections for Q
    let v_and_q_selected_questions = selections.v_and_q_selected_questions;
    let v_and_q_selected_variables = selections.v_and_q_selected_variables;

    let initial_n_selected = selected_items.length;
    let n_selected_vars = selected_variables.length;
    if (initial_n_selected === 0 && n_selected_vars === 0) // If nothing at all selected
    {
        return applyCalculationOnValidSelections(operator, null); // Create empty standard R item
    }
    if (!in_displayr && (v_and_q_selected_variables.length > 0 || v_and_q_selected_questions.length > 0)) {
        return calculateStandardRVariable(operator, selections);
    }
    item_selected = initial_n_selected > 0;
    return item_selected ? calculateStandardR(operator, selections) : calculateStandardRVariable(operator, selections);
}

function calcSingleDimStandardR(operator, dimension, selections) {
    selections = selections.outputs;
    let initial_n_selected = selections.length;
    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[0]];
        }
        applySingleDimensionCalculation(operator, dimension, selections);
        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 Table or Calculation.`);
    return false;
}

function calcSingleDimStandardRVariable(operator, selections) {
    let selected_variable_sets = selections.questions;
    if (selectedVariableSetsInvalid(selected_variable_sets, check_duplicates = false)) {
        return false;
    }
    return appendCalculatedVariablesToDataSet(operator, selections.variables);
}

function applySingleDimensionCalculationWithSelection(operator = 'Sum', dim = 'Row')
{
    const in_displayr = inDisplayr();
    let selections = deduceAppropriateSelections();
    let selected_items = selections.outputs;
    let selected_variables = selections.variables;
    let selected_variable_sets = selections.questions;

    // Check variables and questions tab selections for Q
    let v_and_q_selected_questions = selections.v_and_q_selected_questions;
    let v_and_q_selected_variables = selections.v_and_q_selected_variables;

    let initial_n_selected = selected_items.length;
    let n_selected_vars = selected_variables.length;
    // 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 || dim === 'Column'))
    {
        applySingleDimensionCalculation(operator, dim, null);
        return true;
    }
    if (!in_displayr && (v_and_q_selected_variables.length > 0 || v_and_q_selected_questions.length > 0)) {
        return calcSingleDimStandardRVariable(operator, selections);
    }
    item_selected = initial_n_selected > 0;
    return item_selected ? calcSingleDimStandardR(operator, dim, selections) : calcSingleDimStandardRVariable(operator, selections);
}

function nominalTypeCodedLabels(variable_set)
{
    let data_reduction = variable_set.dataReduction;
    let net_indices;
    if (!data_reduction.transposed)
    {
        coded_labels = data_reduction.rowLabels;
        net_indices = data_reduction.netRows;
    } else
    {
        coded_labels = data_reduction.columnLabels;
        net_indices = data_reduction.netColumns;
    }
    if (net_indices.length > 0)
        coded_labels.filter((item, i) => !net_indices.includes(i));
    return coded_labels;
}

function getCodedLabels(variable_set)
{
    let variable_set_structure = variable_set.variableSetStructure;
    let coded_labels;
    switch (variable_set_structure)
    {
        case 'Nominal':
        case 'Nominal - Multi':
        case 'Ordinal':
        case 'Ordinal - Multi':
            coded_labels = nominalTypeCodedLabels(variable_set);
            break;
    }
    return coded_labels;
}

const ALLOWED_TYPES_FOR_TEXTBOX = ['Nominal', 'Nominal - Multi', 'Ordinal', 'Ordinal - Multi'];

function determineDefaultInputsForCountOperators(variables)
{
    let categorical_variables = [];
    let numeric_variables = [];
    if (variables.length > 0)
    {
        let selected_guids = variables.map(selection => selection.guid);
        var all_variables = project.dataFiles.map(dat => dat.variables).flat();
        selected_variables = all_variables.filter(variable => selected_guids.includes(variable.guid));
        selected_variables.forEach(variable => {
            if (ALLOWED_TYPES_FOR_TEXTBOX.includes(variable.question.variableSetStructure))
                categorical_variables.push(variable);
            else
                numeric_variables.push(variable);
        });
    }
    let default_settings = {'categorical': '', 'numeric': ''};
    if (categorical_variables.length > 0)
        default_settings['categorical'] = determineCategoricalLabelsForCountDefaults(categorical_variables);
    if (numeric_variables.length > 0)
        default_settings['numeric'] = determineNumericValuesToCountDefaults(numeric_variables);
    return default_settings;
}

function determineCategoricalLabelsForCountDefaults(categorical_variables)
{
    let all_labels = categorical_variables.map(determineCategoricalLabelsForTextbox);
    let listbox_keys = Object.keys(all_labels[0]);
    let listbox_details = {};
    listbox_keys.forEach(key => {
        listbox_details[key] = uniq(all_labels.map(lo => lo[key]).flat());
    });
    let safe_delimiter = determineDelimiterToUse(listbox_details['labels']);
    // Escape single quotes here
    return listbox_details['labels'].join(safe_delimiter).replace("'", "\\'");
}

function determineDelimiterToUse(labels)
{
    let all_labels = labels.join('');
    if (!all_labels.includes(';'))
        return ';';
    return !all_labels.includes(',') ? ',' : ';';
}

function determineCategoricalLabelsForTextbox(variable)
{
    let output = {};
    let variable_set = variable.question;
    let unique_values = variable_set.variables[0].uniqueValues; // knownValues
    let data_red = variable_set.dataReduction;
    // Get unique set of labels and values from the data reduction
    let coded_labels = uniq(getCodedLabels(variable_set));
    let coded_values = coded_labels.map(vl => data_red.getUnderlyingValues(vl));
    coded_values = coded_values.map(item => item.length === 1 ? item[0] : item);
    coded_values = uniq(coded_values);
    let final_codes = [];
    let final_labels = [];
    let remaining_codes = coded_values;
    coded_values.forEach((coded_val, idx) => {
        // If a single code, add it to the final codes
        if (typeof(coded_val) === 'number')
        {
            final_codes.push(coded_val);
            remaining_codes = remaining_codes.filter(item => item !== coded_val);
            final_labels.push(coded_labels[idx]);
            return;
        }
        let other_codes = coded_values.filter((cv, ind) => ind !== idx);
        // If current codes are entirely contained in another set of codes then skip
        if (coded_val.every(code => other_codes.includes(code)))
            return;
        let not_seen_in_other_codes = !coded_val.some(code => other_codes.includes(code));
        let not_in_existing_final_codes = !coded_val.some(code => final_codes.flat().includes(code));
        if (not_seen_in_other_codes && not_in_existing_final_codes)
        {
            final_labels.push(coded_labels[idx]);
            remaining_codes = remaining_codes.filter(item => item !== coded_val);
            final_codes.push(coded_val)
            return;
        }
    });
    let flattened_final_codes = final_codes.flat();
    // Check if any values remain that havent been added to the final codes
    let last_levels = unique_values.filter(val => !flattened_final_codes.includes(val));
    let final_last_levels = [];
    let missing_data_levels = [];
    let value_attr = variable_set.valueAttributes;
    // Filter the the remaining levels into either missing data or other hidden codes.
    last_levels.forEach(level => {
        if (value_attr.getIsMissingData(level))
            missing_data_levels.push(level);
        else
            final_last_levels.push(level);
    });
    if (final_last_levels.length > 0)
    {
        final_labels = final_labels.concat(final_last_levels.map(lev => value_attr.getLabel(lev)));
    }
    // Create a copy of the labels and use map to avoid a shallow copy by reference
    let initial_labels = final_labels.map(element => element);
    let r_values = final_labels.map(element => element);

    output['labels'] = final_labels;
    output['initial'] = initial_labels;
    output['rlevels'] = r_values;
    return output;
}

function isMissing(value, only_nan = true)
{
    return only_nan ? isNaN(value) : isNaN(value) || value === -2147483648;
}

function determineRange(numeric_vector)
{
    numeric_vector = numeric_vector.filter(val => !isNaN(val) && val !== -2147483648);
    if (numeric_vector.length === 0)
    {
        return 'NA';
    }
    let min = numeric_vector.reduce((x, y) => Math.min(x, y));
    let max = numeric_vector.reduce((x, y) => Math.max(x, y));
    min = Math.floor(min);
    max = Math.ceil(max);
    return [min, max];
}

function convertNumToRString(numeric_value)
{
    if (isFinite(numeric_value))
        return numeric_value.toString();
    return numeric_value > 0 ? 'Inf' : '-Inf';
}

function convertToOpenInterval(range)
{
    let infinite_values = range.map(val => !isFinite(val));
    let n_infinite = infinite_values.reduce((x, i) => x + i, 0);
    if (n_infinite === 2)
    {
        let signs = range.map(val => Math.sign(val));
        if (signs[0] === signs[1])
            return signs[0] > 0 ? 'Inf' : '-Inf';
        return '>=0';
    }
    if (infinite_values[0])
        return '<=' + range[1];
    return '>=' + range[0];
}

function determineNumericValuesToCountDefaults(variables)
{
    const BINARY_STR = ['Binary - Multi', 'Binary - Grid'];
    let n_variables = variables.length;
    let numeric_variables = variables.filter(v => !BINARY_STR.includes(v.question.variableSetStructure));
    let unique_values = numeric_variables.map(v => {
        let va = v.valueAttributes;
        return v.uniqueValues.map(uv => va.getValue(uv));
    });
    var all_ranges = unique_values.map(determineRange);
    var all_values = {'singletons': [], 'intervals': []};
    all_ranges.forEach(range => {
        if (range[0] === range[1] && range[0] === 1)
            return all_values['singletons'].push(range[0]);
        return all_values['intervals'].push(range);
    });
    let numeric_value_keys = Object.keys(all_values);
    let default_numeric_value = [];
    numeric_value_keys.forEach(key => {
        if (all_values[key].length === 0)
        {
            delete all_values[key];
            return;
        }
        let output_string = [];
        if (key === 'intervals')
        {
            if (all_values[key].every(isNaN))
                output_string = 'NA';
            else {
                let range = all_values[key].reduce((x, y) => [Math.min(x[0], y[0]), Math.max(x[1], y[1])]);
                let infinity_found = range.some(x => !isFinite(x));
                if (infinity_found)
                    output_string = convertToOpenInterval(range);
                else
                {
                    if (range[0] === range[1])
                        range = [range[0]];
                    range = range.map(convertNumToRString);
                    output_string = range.join('-');
                }
            }
        } else {
            output_string = uniq(all_values[key]).join(', ');
        }
        default_numeric_value.push(output_string);
    });
    default_numeric_value = default_numeric_value.join(', ');
    if (numeric_variables.length !== n_variables)
        default_numeric_value = default_numeric_value === '' ? '1' : default_numeric_value.concat(', 1');
    return default_numeric_value;
}

function extractMinAndMaxTable(table)
{
    let table_values = NaN;
    let has_primary = table.primary != null;
    if (has_primary)
    {
        let table_output = table.calculateOutput();
        let statistics = table_output.statistics;
        table_values = statistics.map(statistic => table_output.get(statistic));
        table_values = table_values.filter(val => typeof(val[0][0]) === 'number');
        table_values = determineRange(table_values.flat(2));
    }
    return table_values;
}

function extractCategoricalOrNumericValuesForTableInputs(selection, variable_name = []) {
    let selection_data;
    if (selection.type === 'Table')
    {
        let text_input = selection.primary.variableSetStructure.startsWith('Text');
        return {'values': text_input ? [-Infinity, Infinity] : extractMinAndMaxTable(selection), 'type': 'numeric'};
    }
    let r_class;
    switch (selection.type)
    {
        case 'R Output':
            selection_data = selection.data;
            r_class = selection.outputClasses;
            variable_name = [];
            break;
        case 'QScriptROutputTranslator':
            selection_data = selection;
            if (variable_name.length === 0)
                return null;
            try
            {
                r_class = selection.getAttribute(variable_name, 'class');
            } catch(e)
            {
                let data_values = selection_data.get(variable_name).flat();
                if (typeof data_values[0] === 'string')
                    return null;
                return {'values': determineRange(data_values), 'type': 'numeric'};
            }
            break;
    }
    if (r_class.includes('NULL'))
        return null;
    if (r_class.includes('data.frame'))
    {
        if (selection.type !== 'R Output')
            return null;
        let all_variable_names = selection_data.getAttribute(variable_name, 'names');
        return all_variable_names.map(variable_name => extractCategoricalOrNumericValuesForTableInputs(selection_data, variable_name));
    }
    if (r_class.includes('factor'))
    {
        let levels = selection_data.getAttribute(variable_name, 'levels');
        if (typeof levels[0] === 'number')
            levels = levels.map(toString);
        let values = selection_data.get(variable_name).flat();
        let has_missing = values.some(value => isMissing(value, only_nan = false));
        return {'values': levels, 'has_missing': has_missing, 'type': 'categorical'};
    }
    r_values = selection.data.get([]).flat();
    if (r_class.includes('matrix') || r_class.includes('array'))
    {
        r_values = typeof(r_values[0]) === 'string' ? '' : determineRange(r_values);
        return {'values': r_values, 'type': 'numeric'};
    }
    if (r_class.includes('numeric') || r_class.includes('integer') || r_class.includes('table'))
    {
        r_values = determineRange(r_values);
        return {'values': r_values, 'type': 'numeric'};
    }
    return null;
}

function createEmptyRVariableSet(text = false) {
    const is_displayr = inDisplayr();

    // Get the data
    let data_file;
    const user_selections = getAllUserSelections()
    let selected_questions = user_selections.selected_questions;
    if (selected_questions.length > 0)
        data_file = project.report.selectedQuestions()[0].dataFile;
    else if (project.dataFiles.length == 1)
        data_file = project.dataFiles[0];
    else if (project.dataFiles.length == 0) {
        log("Please add a data set.");
        return false;
    } else if (!is_displayr) {
        data_file = dataFileSelection()[0];
    } else {
        log("Please select data from a single data set.")
        return false;
    }

    let test_v = data_file.variables[0];

    let test_name = disambiguateReferenceName(test_v);

    let prompt_message = "How many variables do you want to create?";
    let n_variables = NaN;
    while (isNaN(n_variables)) {
        n_variables = parseInt(prompt(prompt_message, "10"))
        if(isNaN(n_variables))
            prompt_message = "You must enter a number. How many variables do you want to create?";
    }

    let default_fill = text ? "''" : "NA"

    let expression = `
# Any variable to let the code know how many cases are in the file.
# You can remove this reference if you expression references other
# variables in this data set
test.variable = ${test_name}
n.cases = length(test.variable)

# Create an empty matrix
new.data = matrix(${default_fill}, nrow = n.cases, ncol = ${n_variables})
colnames(new.data) = paste0("Variable ", seq(1, ncol(new.data)))

# Enter your code here to fill in the new.data with your calculations.
# Note that:
# 1. Each column of data becomes a variable in your data set.
# 2. The expression for new.data should create either a matrix or a data frame.
# 3. You cannot change the number of columns.

new.data
    `


    let new_r_question = data_file.newRQuestion(expression,
                                                preventDuplicateQuestionName(data_file, "New R Variable Set"),
                                                "newRQ"+makeid(), null);
    if (text)
        new_r_question.questionType = "Text - Multi"
    insertAtHoverButtonIfShown(new_r_question);
    project.report.setSelectedRaw([new_r_question.variables[0]])
}