QScript Utility Functions

From Q
(Redirected from CheckQuestionType)
Jump to navigation Jump to search

This page contains functions that support the use of QScript but do not target a specific aspect of Q (e.g. questions or tables).


To make these functions available when writing a QScript or Rule see JavaScript Reference.

moveQuestionsToHoverButtonIfShown

Move one or more variable sets to the location of the hover button if the user has initiated the script from the Data Sets hover button in Displayr. Generalises insertAtHoverButtonIfShown. The only argument, questions, is an array of QScript Question objects to be moved. They are placed in the order in which they appear in this array.

insertAtHoverButtonIfShown(question)

This function has an affect only in Displayr. It is used in QScripts that create new questions so that the position they are inserted into the data tree can be controlled using the variable hover button popup. There are checks inside the function, so that no error is given in older versions of Q which do not support this feature.

selectInputQuestions(allowed_structures, single, select_scale_questions, add_levels)

This function is called by many of the QScripts that use a set of questions as the input. It provides a unified interface, which checks first if the user has already selected a set of questions. If the selection does not include questions that have an allowed variable set structure, then we assume those questions were not intentionally selected and begin a dialog in the same way as if no questions were selected. It asks the user to select a datafile if there are multiple data file, and then allows the user to select all questions of the appropriate type from the data file selected. The dialog automatically changes to use Q or Displayr terminology where appropriate. Returns an array of questions.

Note that the only required parameter allowed_structures is expected to be a variableSetStructure (not a questionType which is deprecated in Displayr).

reportNewRQuestion

Performs a series a series of operations that are commonly performed after a new question is created. In Q, a new group is created in the report tree and summary tables added to that group.

checkQObjectArrayType(array, type_name)

This function checks the elements of the input array and throws an error if any of them are not of the type specified by type_name.

checkQuestionType(question, type_array)

This function throws an error if the input question has a question type other than those specified in type_array, which should be an array of strings that are valid question types.

getAllQuestionsByTypes(data_file_array, array_of_types)

This function looks through each of the data files specified by data_file_array and returns an array of all non-hidden questions that are of the Question Types in array_of_types.

For example:

getAllQuestionsByTypes(project.dataFiles[0], ["Pick One", "Pick Any"])

will return an array containing all questions from the first data file in the project that are either Pick One or Pick Any and which are not hidden.

getAllQuestionsByStructures(data_file_array, array_of_types)

For use in Displayr, this function looks through each of the data files specified by data_file_array and returns an array of all non-hidden questions that are of the Question Types in array_of_types by examining the variableSetStructure property of each question.

For example:

getAllQuestionsByStructures(project.dataFiles[0], ["Numeric", "Binary - Multi"])

will return an array containing all questions from the first data file in the project that are either Numeric or Binary - Multi and which are not hidden.

preventDuplicateQuestionName(data_file, question_name)

This function can be used to prevent a QScript from trying to create a question with a name that already exists in the data file. Duplicate question names are not allowed.

This function looks in the specified data_file for a question named question_name. If a question with that name already exists then the function will add a number to the question name until it generates a name that does not exist in the project, and it returns the new name.

preventDuplicateVariableName(data_file, variable_name, separator)

This function can be used to prevent a QScript from trying to create a variable with a name that already exists in the data file. Duplicate variable names are not allowed.

This function looks in the specified data_file for a variable named variable_name. If a variable with that name already exists then the function will add a number to the variable name until it generates a name that does not exist in the project, and it returns the new name. There is an option to specify a separator between the variable name and the number suffix.

randomVariableName(len,prefix, suffix)

Returns a valid variable name made up of the given prefix (if provided), len random characters from a-z, followed by the suffix (if any).

questionHasDuplicateVariableLabels(question)

Returns true if any of the variables in the input question have duplicate Variable Labels.

pickOneMultiToPickAnyFlattenByRows(question)

This function accepts a Pick One - Multi question and returns a new question which is the flattened version. This is done according to Insert Ready-Made Formulas: Pick One - Multi -> Pick Any (flatten) and using the option to nest within rows.

makeid()

This function returns a random string that is five characters long which contains letters and digits.

createQuestionWithLinkedVariables(question_name, variables, data_file, question_type)

This function creates a question with the specified type from linked copies of the input variables. Linked copies of the variables (with a suffix "_Ld" where d is 0 or more digits) are created if they do not already exist.

getVariablesFromQuestions(questions)

This function returns an array of variables from the input array of questions.

getVariables(questions_or_variables)

This function returns an array of variables from the input array of questions and variables.

getNonlinkedVariablesFromQuestions(questions)

This function returns an array of variables that are not linked variables (with a suffix "_Ld" where d is 0 or more digits) from the input array of questions.

isQuestionNumCat(question)

This function returns true if the input question contains numeric or categorical data, otherwise false.

isQuestionNotHidden(question)

This function returns true if the input question is not hidden, otherwise false. Can be used with an array filter e.g.:

var questions_that_are_not_hidden = questions_array.filter(isQuestionNotHidden);

isQuestionValid(question)

This function returns true if the input question is valid, otherwise false. Can be used with an array filter e.g.:

var questions_that_are_valid = questions_array.filter(isQuestionValid);

getQuestionNameAndVariableLabel(variable)

Given an input variable, this function returns a string containing the question name and variable label. If the question name and variable label are the same, then only one is returned.

flattenSelectedQuestions(selected_questions)

This function examines the array of selected_questions, flattens any Pick One - Multi, Pick Any - Grid, or Number - Grid questions, and replaces the original version with the flattened version in the array. Flattened versions have a single column.

alertIfMeasurementsMissing(labels_1, lables_2, measure_name)

Checks two arrays of labels and provides an alert for those labels that are not common between the two arrays.

commonPrefix(label_1, label_2)

Find the common prefix of two strings.

longestCommonPrefix(labels)

Find the longest common prefix in an array of labels.

uniqueQObjects(array)

Given an array of Q objects (questions, variables, tables, etc) return an array with any duplicates removed.

requireDataFile()

Check that the project contains one or more data files. If it doesn't, a log message is generated and the function returns false.

selectInputQuestions(allowed_structures, single, select_scale_questions, add_levels)

Checks if the user has selected any appropriate questions/variable sets and if so, returns them. If not, the user is given prompts to select appropriate ones.

generateGuidStringForFormInput(array)

Given an array containing questions, variables, tables, or R outputs, return a string of their guids joined by semicolons. The string is required for setting the values of R item controls.

getDataFileFromItemDependants(item)

Given a report tree item, look at the variables and questions used by the item and return the data file for the first variable or question. For R outputs, it is recommended to use getDataFileFromROutputInput as often R outputs have inputs from multiple data files.

getDataFileFromROutputInput(r_output, control_name)

Given an R Output and a control name, look at the variables and questions passed as inputs and return the data file for the first variable or question. When using an R Output to generate new variables, this function allows you to work out which data file the items should go in.

quoteVariableNameForJavaScript(variable_name)

Wraps illegal JavaScript variable names, enabling them to be passed as the first argument to newJavaScriptVariable().

cleanVariableName(variable_name, blank_fallback)

Removes invalid characters from the variable name. A variable name may only start with a letter or @, and the remaining characters may only consist of letters, digits, or the characters @ # _ . $ blank_fallback is an optional parameter that will be used if variable_name consists entirely of invalid characters (defaults to var)

getLastVariable(variables)

Gets the last variable in variables based on the order in which they appear in the data file (they are assumed to be from the same data file).

sortQuestionsInDataFileOrder(questions)

Given an array of questions, this function will sort the array according to the order in which those questions currently appear in the data file. This is to help preserve the order of questions when changing their position in the data file (that is, regardless of the order in which the user selected them, or the order in which the QScript added them to the array).

sortVariablesInDataFileOrder(variables)

Given an array of variables, this function will sort the array according to the order in which those variables currently appear in the data file. This is to help preserve the order of variables when changing their position in the data file (that is, regardless of the order in which the user selected them, or the order in which the QScript added them to the array).

Source Code

includeWeb('JavaScript Text Analysis Functions');
includeWeb('JavaScript Utilities');
includeWeb('JavaScript Array Functions');
includeWeb('QScript Value Attributes Functions');

// Move one or more questions to the location of the hover button 
// if the user has initiated the script from the Data Sets hover button.
// Generalises insertAtHoverButtonIfShown.
// - questions is an array of question objects to be moved. Questions
//   are placed in the order in which they appear in this array.
function moveQuestionsToHoverButtonIfShown(questions) {

    // Do nothing if hover button not used 
    if (typeof(project.getDataInsertingAtItem) === "undefined")
        return;
    var inserting_at = project.getDataInsertingAtItem();
    if (inserting_at === null)
        return;

    var data_file = inserting_at.dataFile;
    
    // Identify the target variable to move after.
    // If inserting above, identify the last variable before the question
    var target_variable = inserting_at.variables[0];
    if (project.isInsertingAbove()) {
        var all_questions = data_file.questions;
        var index = all_questions.findIndex(function(q){ return(q.guid === inserting_at.guid); });
        if (index === 0)
            target_variable = null;
        else
            target_variable = all_questions[index - 1].variables[0];
    }

    // Move all the questions in order
    questions.forEach(function (q) {
        if (q != null) {
            var variables = q.variables;
            data_file.moveAfter(variables, target_variable);
            target_variable = variables[variables.length - 1];        
        }
    });
}

function insertAtHoverButtonIfShown(question) {
    if (typeof(project.getDataInsertingAtItem) === "undefined")
        return;
    var inserting_at = project.getDataInsertingAtItem();
    if (inserting_at === null)
        return;
    var data_file = inserting_at.dataFile;
    var variables = question.variables;
    if (question == null || variables == null)
        return;

    if (!project.isInsertingAbove()) {
        data_file.moveAfter(variables, inserting_at.variables[0]);
    } else {
		var all_questions = data_file.questions;
    	var index = all_questions.findIndex(function(q){ return(q.guid === inserting_at.guid); });
		if (index === 0)
			data_file.moveAfter(variables, null);
		else
        	data_file.moveAfter(variables, all_questions[index - 1].variables[0]);
    }
}




// This function returns false if any of the variables in any of the questions
// is invalid or is completely empty.
function areQuestionsValidAndNonEmpty (questions) {

    // Check if questions is single question
    if (typeof(questions.isValid) != 'undefined')
        questions = [questions];

    for (let i = 0; i < questions.length; i++) {
        if (!questions[i].isValid) {
            log(correctTerminology("Question '") + questions[i].name + "' is invalid.");
            return false;
        }
        let isEmpty = true;
        for (let j = 0; isEmpty && j < questions[i].uniqueValues.length ; j++)
            isEmpty = !isFinite(questions[i].uniqueValues[j]);
        if (isEmpty) {
            log(correctTerminology("Question '") + questions[i].name + "' is empty.");
            return false;
        }
    }
    return true;
}

function checkDuplicateVariable(variable_name) {
    var all_variables = project.dataFiles.map(function(d){return(d.variables)}).flat();
    var variables = all_variables.filter(function(v) {
        return v.name === variable_name || v.label === variable_name;
    })
    return variables.length !== 1;
}

function printTypesString(x, conjunction) {
    if (x.length <= 1)
        return x;
    var comma_separated = x.slice(0, x.length - 1);
    if(typeof(conjunction) === "undefined" || !conjunction) {
        conjunction = " or ";
    }
    return comma_separated.join(", ") + conjunction + x[x.length - 1];
}



function convertStructureToType(qtypes)
{
    for (var i = 0; i < qtypes.length; i++)
    {
        switch(qtypes[i])
        {
            case "Nominal": case "Ordinal":
                qtypes[i] = "Pick One";
                break;
            case "Nominal - Multi": case "Ordinal - Multi":
                qtypes[i] = "Pick One - Multi";
                break;
            case "Numeric":
                qtypes[i] = "Number";
                break;
            case "Numeric - Multi":
                qtypes[i] = "Number - Multi";
                break;
            case "Numeric - Grid":
                qtypes[i] = "Number - Grid";
                break;
            case "Binary - Multi":
                qtypes[i] = "Pick Any";
                break;
            case "Binary - Grid":
                qtypes[i] = "Pick Any - Grid";
                break;
        }
    }
    return(qtypes);
}



function onlyUnique(value, index, self) {
	return self.indexOf(value) === index;
}

function variableToLabels(variable) {
	var question = variable.question;
	var attributes = question.valueAttributes;
	var values = variable.uniqueValues;
	var relevantValues = values.filter(function(x) {
		return !isDontKnow(attributes.getLabel(x)) && !isNaN(attributes.getValue(x)) && !attributes.getIsMissingData(x);
	});
	return getLabelsForValues(question, relevantValues);
}

function arraysEqual(array_1, array_2) {
	var are_equal = true;
	array_1.forEach(function (label, ind) {
		if (label != array_2[ind])
			are_equal = false;
	});
	return are_equal;
}

function getDataFileFromQuestions (questions) {

    if (typeof(questions.dataFile) !== 'undefined')
        return(questions.dataFile);
    var data_file = questions[0].dataFile;

    // Make sure all questions are from the same data set
    if (!questions.map(function (q) { return q.dataFile.name; }).every(function (type) { return type == data_file.name; })) {
        throw new UserError("Selected questions or variables are from different Data Sets and cannot be combined.\n" +
            " Please select questions or variables from a single Data Set.");
    }
    return(data_file);
}




// Check that the input array only contains Q objects of type
// type_name
function checkQObjectArrayType(array, type_name) {
    var error_message = "Expected an array of: " + type_name;
    if (!Array.isArray(array))
        throw new CallerCallerError(error_message);
    array.forEach(function (o) {
        if (typeof o.type == "undefined")
            throw new CallerCallerError(error_message);
        if (o.type != type_name)
            throw new CallerCallerError("Expected a " + type_name + " but got " + o.type);
    });
}

// Throw this error when your function is called improperly.  The error will be
// reported at the line number of the first bit of code that we didn’t write.  This
// will only trigger a bug reports if the code calling your function was written internally.
try {
    CallerError = eval('class CallerError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; CallerError');
} catch (e) {
    CallerError = Error;  // <= Q5.3 (ES5)
}

// Throw this error for invalid calls two frames up in the stack.  The error will be
// reported at the line number of the caller’s caller.   These will not trigger bug reports.
try {
    CallerCallerError = eval('class CallerCallerError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; CallerCallerError');
} catch (e) {
    CallerCallerError = Error;  // <= Q5.3 (ES5)
}

// Throw this error when you never want to trigger a bug report.  The message will be
// shown to the user.  The error will be reported at the line number of the first bit
// of code that we didn’t write.  If we wrote everything then the user will not get an
// option to debug, or to send in a bug report.
try {
    UserError = eval('class UserError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; UserError');
} catch (e) {
    UserError = Error;  // <= Q5.3 (ES5)
}

// Check that the question is of one of the types in type_array
function checkQuestionType(question, type_array) {
    if (!question)
        throw new CallerCallerError("You must pass an input question");
    if (question.type != "Question")
        throw new CallerCallerError("Expected a question");
    if (type_array.indexOf(question.questionType) == -1)
        throw new CallerCallerError(question.name + " is not of type(s) " + type_array.join(", "));
}

// Return an array of questions from data files in data_file_array that are of any
// of the question types specified by array_of_types
function getAllQuestionsByTypes(data_file_array, array_of_types) {
    function questionIsType(question) {
        return array_of_types.indexOf(question.questionType) != -1;
    }
    var selected_questions = [];
    var num_files = data_file_array.length;
    for (var j = 0; j < num_files; j++) {
        selected_questions = selected_questions.concat(data_file_array[j].questions.filter(questionIsType));
    }
    return selected_questions.filter(function(q) { return !q.isHidden && !q.isBanner && q.isValid; }) ;
}

// As above, returns an array of questions from data files in data_file_array that are of any
// of the question types specified by array_of_types, except here the variableSetStructure
// property is used which corresponds to the question types used in Displayr
function getAllQuestionsByStructures(data_file_array, array_of_types) {
    function questionIsType(question) {
        return array_of_types.indexOf(question.variableSetStructure) != -1;
    }
    var selected_questions = [];
    var num_files = data_file_array.length;
    for (var j = 0; j < num_files; j++) {
        selected_questions = selected_questions.concat(data_file_array[j].questions.filter(questionIsType));
    }
    return selected_questions.filter(function(q) { return !q.isHidden && !q.isBanner && q.isValid; }) ;
}

// If the question_name is unique, this function returns it. If the question name already exists
// in the file then the function will add an integer to the end of the question name until
// it finds a unique question name.

function preventDuplicateQuestionName(data_file, question_name) {
    if (data_file.getQuestionByName(question_name) != null) {
        var qcounter = 1;
        var is_duplicate = true;
        var new_question_name;
        while (is_duplicate) {
            new_question_name = question_name + " " + qcounter;
            is_duplicate = data_file.getQuestionByName(new_question_name) != null;
            qcounter ++;
        }
        return new_question_name;
    } else {
        return question_name;
    }
}

// If the variable_name is unique, this function returns it. If the variable name already exists
// in the file then the function will add an integer to the end of the variable name until
// it finds a unique variable name.
function preventDuplicateVariableName(data_file, variable_name, separator) {
    var new_variable_name = variable_name;
    var c = 1;
    if (separator === undefined)
        separator = "";
    while(true) {
        is_duplicate = data_file.getVariableByName(new_variable_name) != null;
        if (!is_duplicate)
            return new_variable_name;
        new_variable_name = variable_name + separator + c;
        c++;
    }
}

// Used for checking if the base name provided to dataFile.newRQuestion is valid.
// The newRQuestion output will create new variables for each new variable in the Question
// These new variables will be of the format variable_base_name_i where i is a sequence
// from 1 to the number of variables required.
// This function checks to see if variable_base_name + "_1" exists. If it doesn't,
// variable_base_name is returned. If it does, a unique name is found and returned by incrementing the
// provided variable_base_name in the same manner as preventDuplicateVariable
function preventDuplicateVariableBaseName(data_file, variable_base_name, separator)
{
    let suffix = "_1";
    let new_variable_base_name = variable_base_name;
    var c = 1;
    if (separator === undefined)
        separator = "";
    while(true) {
        is_duplicate = data_file.getVariableByName(new_variable_base_name + suffix) != null;
        if (!is_duplicate)
            return new_variable_base_name;
        new_variable_base_name = variable_base_name + separator + c;
        c++;
    }
}

// generate a new
function randomVariableName(len,prefix, suffix) {
    len = len || 16;
    prefix = prefix || "";
    suffix = suffix || "";
    var charCodes = Array.apply(null, Array(len)).map(function() { return 'a'.charCodeAt(0) + Math.floor(26 * Math.random()) });
    return prefix + String.fromCharCode.apply(null, charCodes) + suffix;
}


// checking to see if a question contains variables with equal labels
function questionHasDuplicateVariableLabels(question) {
    var variables = question.variables;
    var k = variables.length;
    var labels = new Array(k);
    for (var i = 0; i < k; i++)
        labels[i] = variables[i].label;
    return arrayHasDuplicateElements(labels);
}

function pickOneMultiToPickAnyFlattenByRows(question) {

    checkQuestionType(question, ["Pick One - Multi"]);
    var new_vars = [];
    var new_var;
    var q_vars = question.variables;
    var last_var = q_vars[q_vars.length - 1];
    var num_q_vars = q_vars.length;
    var value_attributes = question.valueAttributes;
    var q_unique_values = question.uniqueValues;
    var value_labels = valueLabels(question);
    var num_vals = q_unique_values.length;
    // Don't try to flatten Pick One - Multi which are poorly set up
    if (num_vals < 2)
        return question;
    for (var j = num_vals - 1; j > -1; j--) {
        if (value_attributes.getIsMissingData(q_unique_values[j])) {
            q_unique_values.splice(j,1);
            value_labels.splice(j,1);
        }
    }
    num_vals = q_unique_values.length;
    var cur_var_label;
    var cur_value_label;
    var cur_var_name;
    var cur_val;
    var new_expression;
    var new_label;
    for (var j = 0; j < num_q_vars; j++) {
        cur_var_label = q_vars[j].label;
        cur_var_name = q_vars[j].name;
        var name_prefix = cur_var_name + makeid() +'_flat_';
        for (var k = 0; k < num_vals; k++) {
            cur_val = q_unique_values[k];
            cur_value_label = value_labels[k];
            if (isNaN(cur_val))
                new_expression = "Q.IsMissing('" + cur_var_name + "') ? NaN : isNaN(Q.Source('" + cur_var_name + "'));";
            else
                new_expression = "Q.IsMissing('" + cur_var_name + "') ? NaN : Q.Source('" + cur_var_name + "') == " + cur_val + " ? 1 : 0;";
            new_label = cur_var_label + " - " + cur_value_label;
            new_var = question.dataFile.newJavaScriptVariable(new_expression, false, name_prefix + (k + 1), new_label, last_var);
            new_var.variableType = "Categorical";
            last_var = new_var;
            new_vars.push(new_var);
        }
    }
    var new_q_name = preventDuplicateQuestionName(question.dataFile, question.name + " (flattened)");
    var new_q = question.dataFile.setQuestion(new_q_name, "Pick Any", new_vars);
    return new_q;
}

// From http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript
function makeid() {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    for( var i=0; i < 5; i++ )
        text += possible.charAt(Math.floor(Math.random() * possible.length));

    return text;
}

function createQuestionWithLinkedVariables(question_name, variables, data_file, question_type, include_question_name_in_label) {
    question_name = preventDuplicateQuestionName(data_file, question_name);
    var variables_linked = [];
    var v_linked = null;
    var single_question = fromSameQuestion(variables);
    for (var i = 0; i < variables.length; i++) {
        var v = variables[i];
        var linked_name = preventDuplicateVariableName(data_file, v.name + '_L');
        if (single_question && !include_question_name_in_label)
            var linked_label = v.label;
        else
            var linked_label = getQuestionNameAndVariableLabel(v);
        v_linked = data_file.newJavaScriptVariable(quoteVariableNameForJavaScript(v.name), false, linked_name, linked_label, v_linked);
        variables_linked.push(v_linked);
    }
    return data_file.setQuestion(question_name, question_type, variables_linked);
}

function getVariablesFromQuestions(questions) {
    var variables = [];
    questions.forEach(function (question) {
        question.variables.forEach(function (variable) {
            variables.push(variable);
        });
    });
    return variables;
}

function getVariables(questions_or_variables) {
    var variables = [];
    questions_or_variables.forEach(function (q_or_v) {
        if (q_or_v.type == "Question") {
            q_or_v.variables.forEach(function (variable) {
                variables.push(variable);
            });
        } else if (q_or_v.type == "Variable")
            variables.push(q_or_v);
    });
    return variables;
}

function getNonlinkedVariablesFromQuestions(questions) {
    var variables = [];
    for (var i = 0; i < questions.length; i++) {
        var q = questions[i];
        for (var j = 0; j < q.variables.length; j++) {
            var v = q.variables[j];
            if (!v.name.match(/_L\d*$/))
                variables.push(q.variables[j]);
        }
    }
    return variables;
}

function isQuestionNumCat(question) {
	var q_type = question.questionType;
	return q_type == 'Number' || q_type == 'Number - Multi' || q_type == 'Number - Grid' ||
           q_type == 'Pick One' || q_type == 'Pick One - Multi' || q_type == 'Pick Any' ||
           q_type == 'Pick Any - Compact' || q_type == 'Pick Any - Grid' ||
           q_type == 'Ranking' || q_type == 'Experiment';
}

function isQuestionNotHidden(question) {
    return !question.isHidden;
}

function isQuestionValid(question) {
    return question.isValid;
}

function getQuestionNameAndVariableLabel(variable)
{
    if (variable == "undefined")
        throw new CallerError("getQuestionAndVariableLabel: variable is not defined");
    if (variable == null)
        throw new CallerError("getQuestionAndVariableLabel: variable is null");
    if (variable.label == variable.question.name)
        return variable.label;
    else {
        var name = variable.question.name;
        if (name.length > 50)
            name = name.substring(0, 50) + '...';
        return name + ': ' + variable.label;
    }
}

function fromSameQuestion(variables) {
    for (var i = 1; i < variables.length; i++)
        if (variables[i - 1].question.name != variables[i].question.name)
            return false;
    return true;
}


// Examines the array of selected_questions, flattens any Pick One - Multi, Pick Any - Grid,
// or Number - Grid questions, and replaces the original version with the flattened version
// in the array. Flattened versions have a single column.
function flattenSelectedQuestions(selected_questions) {
    var q;
    var data_file;
    for (var i = 0; i < selected_questions.length; i++) {
        q = selected_questions[i];
        data_file = q.dataFile;
        var flattened_name = q.name + ' (flattened)';
        var q_flattened = data_file.getQuestionByName(flattened_name);
        if (q_flattened == null) {
            var flattened = true;
            if (q.questionType == 'Pick One - Multi') {
                q_flattened = pickOneMultiToPickAnyFlattenByRows(q);
            }
            else if (q.questionType == 'Pick Any - Grid') {
                q_flattened = q.duplicate(flattened_name);
                q_flattened.questionType = 'Pick Any';
            }
            else if (q.questionType == 'Number - Grid') {
                q_flattened = q.duplicate(flattened_name);
                q_flattened.questionType = 'Number - Multi';
            }else
                flattened = false;

            if (flattened && !q.needsCheckValuesToCount) {
                q_flattened.needsCheckValuesToCount = false;
                q_flattened.variables.forEach(function(v){ v.needsCheck = false;});
            }
        }
        if (q_flattened != null) {
            selected_questions.splice(i, 1, q_flattened);
        }
    }
}

function alertIfMeasurementsMissing(labels_1, lables_2, measure_name) {
    var labels_diff = difference(labels_1, lables_2);
    if (labels_diff.length > 0) {
        var message = 'There are no ' + measure_name + ' measurements for:\n\n';
        for (var i = 0; i < Math.min(labels_diff.length, 20); i++)
            message += labels_diff[i] + '\n';
        if (labels_diff.length > 20)
            message += '...\n';
        message += '\nDo you wish to continue?';
        alert(message);
    }
}

// Find the common prefix to two labels
function commonPrefix(label_1, label_2) {
    // search for substrings of label_2 at the start of label_1
    for (var end = 1; end < label_2.length; end++) {
    if (label_1.indexOf(label_2.substr(0, end)) != 0)
        break;
    }
    return label_2.substr(0, end-1);
}

// Find the longest common prefix in an array of labels.
function longestCommonPrefix(labels) {
    var longest_prefix = labels[0];

    labels.forEach(function (label, ind) {
        if (ind > 0) {
            var cur_common = commonPrefix(label, labels[0]);
            if (cur_common.length < longest_prefix.length)
                longest_prefix = cur_common;
        }
    });
    return longest_prefix;
}


function moveSelectedVariables(move_type) {

    // Determine the index of each of target_variables within all_variables
    function getVariableIndex(target_variable, all_variables) {
        var j = 0;
        var found = false;
        while (j < all_variables.length && !found)
            if (target_variable.equals(all_variables[j]))
                found = true;
            else j++;

        if (!found)
            return -1;
        else
            return j;
    }

    var valid_types = ["Top", "Bottom", "Up", "Down"];
    if (valid_types.indexOf(move_type) == -1)
        throw("Do not understand move_type == " + move_type);


    // If the user has selected multiple questions, select all
    // variables in all selected questions. If they have only
    // selected variables within a single question then do not
    // select all of the variables within that question.
    var selected_questions = project.report.selectedQuestions();
    var selected_variables = project.report.selectedVariables();

    if (selected_questions.length == 0 && selected_variables == 0) {
        log("No data selected.");
        return false;
    }

    var moving_questions = selected_questions.length > 1
                            || selected_questions[0].variables.length == selected_variables.length
                            || move_type == "Top"
                            || move_type == "Bottom";
    if (moving_questions) {
        selected_variables = [];
        selected_questions.forEach(function (q) {
            selected_variables = selected_variables.concat(q.variables);
        });
    }


    var selected_items = project.report.selectedItems();
    if (selected_variables.length == 0) {
        log("Nothing selected.");
        return false;
    }

    var data_file = selected_variables[0].question.dataFile;
    if (!selected_variables.map(function (v) { return v.question.dataFile.name; }).every(function (ff) { return ff == data_file.name; })) {
        log("Please select variables from the same data set");
        return false;
    }


    // if (selected_items.length != 0) {
    //     log("Select from Data only.");
    //     return false;
    // }

    var all_variables = data_file.variables;
    var all_questions = data_file.questions;
    var index_of_first_variable = getVariableIndex(selected_variables[0], all_variables);
    var index_of_first_question = getVariableIndex(selected_questions[0], all_questions);
    var index_of_last_variable = getVariableIndex(selected_variables[selected_variables.length - 1], all_variables);
    var index_of_last_question = getVariableIndex(selected_questions[selected_questions.length - 1], all_questions);

    //log(getVariableIndex(selected_variables[0], all_variables));


    // Figure out the variable to move the variables below
    var target_variable = null;
    if (move_type == "Bottom") {
        target_variable = all_variables[all_variables.length - 1];
    } else if (move_type == "Up") {
        if (moving_questions) {
            var index_above = index_of_first_question - 2;
            if (index_above > -1) {
                var question_above = all_questions[index_above];
                target_variable = question_above.variables[question_above.variables.length - 1];
            }
        } else {
            var index_above = index_of_first_variable - 2;
            // If we are moving variables within a question then stop moving when the variable above is in a different question
            if (index_above > -1 && all_variables[index_above + 1].question.equals(selected_variables[0].question)) {
                var variable_above = all_variables[index_above];
                if (variable_above.question)
                target_variable = variable_above;
            }
        }

    } else if (move_type == "Down") {
        if (moving_questions) {
            if (index_of_last_question < all_questions.length - 1) {
                var question_below = all_questions[index_of_last_question + 1];
                target_variable = question_below.variables[question_below.variables.length - 1];
            }
        } else {
            var q_vars = selected_questions[0].variables;
            var index_of_last_var_in_question = getVariableIndex(selected_variables[selected_variables.length - 1], q_vars);
            if (index_of_last_var_in_question == q_vars.length - 1) {
                var vars_not_selected = q_vars.filter(function (v) {
                    return getVariableIndex(v, selected_variables) == -1;
                });
                target_variable = vars_not_selected[vars_not_selected.length - 1];
            } else
                target_variable = q_vars[index_of_last_var_in_question + 1];
        }
    }

    data_file.moveAfter(selected_variables, target_variable);

    // Get all variables in all selected questions and move them too.


    //all_variables = data_file.variables;
    //log(getVariableIndex(selected_variables[0], all_variables));
}


// Given an array of Q objects (questions, variables, tables, etc) return
// an array with any duplicates removed.
function uniqueQObjects(array) {
    var uniques = [];
    array.forEach(function (obj) {
        function equalThis(x) {
            return obj.equals(x);
        }
        if (!uniques.some(equalThis))
            uniques.push(obj);
    });
    return uniques;
}

function requireDataFile() {
    if(project.dataFiles.length == 0){
        log("This QScript requires a project with one or more data files.");
        return false;
    } else
        return true;
}

// Gets the guid for each element in the array and joins them by semicolons
function generateGuidStringForFormInput(array) {
    var guids = array.map(function (x) { return x.guid; })
    return guids.join(";");
}


// Looks at the variables and questions which an item depends on and returns
// the data file that they live in.
function getDataFileFromItemDependants(item) {
    var item_dependants = item.dependants(false).filter(function (item) { return item.type == "Question" || item.type == "Variable" });
    if (item_dependants.length == 0)
        return null;
    if (item_dependants[0].type == "Question")
        return item_dependants[0].dataFile;
    if (item_dependants[0].type == "Variable")
        return item_dependants[0].question.dataFile;
}

// Returns the data file of the first item in the R output control.
function getDataFileFromROutputInput(r_output, control_name) {
    try { // RS-3471
        var inputs = r_output.getInput(control_name);
    }
    catch(err) {
        var inputs = null;
    }
    if (inputs == null)
        return null;
    if (inputs.length == null)
    {
        if (inputs.type == "Question")
            return inputs.dataFile;
        if (inputs.type == "Variable")
            return inputs.question.dataFile;
        return null;
    }
    inputs = inputs.filter(function (item) { return item.type == "Question" || item.type == "Variable" });
    if (inputs.length == 0)
        return null;
    if (inputs[0].type == "Question")
        return inputs[0].dataFile;
    if (inputs[0].type == "Variable")
        return inputs[0].question.dataFile;
}

function quoteVariableNameForJavaScript(variable_name) {
    // Reserved JavaScript words that should never be recognised as variable
    // names, or inserted into scripts without being wrapped in Q.GetValue().
    var reserved = {
        // Reserved words.
        "abstract": true,
        "as": true,
        "boolean": true,
        "break": true,
        "byte": true,
        "case": true,
        "catch": true,
        "char": true,
        "class": true,
        "continue": true,
        "const": true,
        "debugger": true,
        "default": true,
        "delete": true,
        "do": true,
        "double": true,
        "else": true,
        "enum": true,
        "export": true,
        "extends": true,
        "false": true,
        "final": true,
        "finally": true,
        "float": true,
        "for": true,
        "function": true,
        "goto": true,
        "if": true,
        "implements": true,
        "import": true,
        "in": true,
        "instanceof": true,
        "int": true,
        "interface": true,
        "is": true,
        "long": true,
        "namespace": true,
        "native": true,
        "new": true,
        "null": true,
        "package": true,
        "private": true,
        "protected": true,
        "public": true,
        "return": true,
        "short": true,
        "static": true,
        "super": true,
        "switch": true,
        "synchronized": true,
        "this": true,
        "throw": true,
        "throws": true,
        "transient": true,
        "true": true,
        "try": true,
        "typeof": true,
        "use": true,
        "var": true,
        "void": true,
        "volatile": true,
        "while": true,
        "with": true,

        // In-built classes.  I left out all the browser-related stuff and
        // stuff that our users will not use (e.g. Date).
        "Array": true,
        "Function": true,
        "Math": true,
        "Object": true,
        "String": true,
        "Q": true,

        // Global properties.
        "Infinity": true,
        "NaN": true,
        "undefined": true,
        "eval": true,
        "isFinite": true,
        "isNaN": true,
        "parseFloat": true,
        "parseInt": true,
    };

    if (!reserved[variable_name])
        return variable_name;
    return "Q.GetValue('" + variable_name + "')";
}

function cleanVariableName(name, blank_fallback) {
    var invalid_rest_name = /[^a-zA-Z0-9@#_\.\$]/g;
    var remove_start_name = /[^@a-zA-Z]/;

    var s = name.replace(invalid_rest_name, '');
    while (s.length > 0 && s[0].match(remove_start_name)) {
        s = s.substring(1);
    }

    // We must have something in the variable name should
    // everything else have been stripped off.
    if (s.length === 0)
        s = blank_fallback || 'var';

    return s;
}

function getLastVariable(variables) {
    var data_file = variables[0].question.dataFile;
    var data_file_vars = data_file.variables;
    var data_file_vars_names = [];
    for (var i = 0; i < data_file_vars.length; i++)
        data_file_vars_names.push(data_file_vars[i].name);
    var last_variable = variables[0];
    var last_index = data_file_vars_names.indexOf(last_variable.name);
    for (var i = 1; i < variables.length; i++)
    {
        var ind = data_file_vars_names.indexOf(variables[i].name);
        if (ind > last_index)
        {
            last_index = ind;
            last_variable = variables[i];
        }
    }
    return last_variable;
}


// Function to tell us if we are running in Displayr or not
function inDisplayr() {

    if (typeof _TEST_DISPLAYR_IN_Q !== 'undefined')
        return _TEST_DISPLAYR_IN_Q;

    return (!!Q.isOnTheWeb && Q.isOnTheWeb());
}

// Use to replace Displayr terms with appropriate Q terms
// (and vice versa) in a message based on the context.
function correctTerminology(input_string) {
    var in_displayr = inDisplayr();
    // Mapping between Q and Displayr terms
    var terminology_dictionary = [{displayr: "page", q: "folder"},
        {displayr: "pages", q: "folders"},
        {displayr: "document", q: "project"},
        {displayr: "variable set", q: "question"},
        {displayr: "Variable Set", q: "Question"},
        {displayr: "variable sets", q: "questions"},
        {displayr: "Variable Sets", q: "Questions"},
        {displayr: "Binary - Multi", q: "Pick Any"},
        {displayr: "Binary - Multi (Compact)", q: "Pick Any - Compact"},
        {displayr: "Nominal/Ordinal", q: "Pick One"},
        {displayr: "Nominal", q: "Pick One"},
        {displayr: "Ordinal", q: "Pick One"},
        {displayr: "Nominal - Multi / Ordinal - Multi", q: "Pick One - Multi"},
        {displayr: "Nominal - Multi", q: "Pick One - Multi"},
        {displayr: "Ordinal - Multi", q: "Pick One - Multi"},
        {displayr: "Numeric - Multi", q: "Number - Multi"},
        {displayr: "Binary - Grid", q: "Pick Any - Grid"},
        {displayr: "Numeric - Grid", q: "Number - Grid"},
        {displayr: "Calculation", q: "R Output"},
        {displayr: "Calculations", q: "R Outputs"},
        {displayr: "Anything > Advanced Analysis", q : "Automate > Browse Online Library"}];

    terminology_dictionary.forEach(function (obj) {
        // Convert target string to a global regex so that all instances are replaced at once.
        var target = in_displayr ? new RegExp("\\b" + obj.q + "\\b", "g") : new RegExp("\\b" + obj.displayr + "\\b", "g");
        var replacement = in_displayr ? obj.displayr : obj.q;
        input_string = input_string.replace(target, replacement);
    });

    return input_string;
}

// Determines the current page of the project in Displayr or the current
// If it doesn't exist (no pages in Displayr or no folders in Q), then
// some senisble defaults are used instead.
function determineCurrentPage() {
    let is_displayr = inDisplayr();
    let group = project.currentPage === undefined ? false : project.currentPage();
    if (!group)
        group = is_displayr ? project.report.appendPage('TitleOnly') : project.report;
    return group;
}

function addStandardRToCurrentPage(page_name, controls) {
    let group = determineCurrentPage();
    let standard_r_output = group.appendStandardR(page_name, controls);
    project.report.setSelectedRaw([standard_r_output]);
    return;
}

function rItemIsPlot(item) {
    let pattern = new RegExp("qChartType");
    return pattern.test(item.codeForGuiControls);
}

function rPlotHasNestedTable(item) {
    if (item.subItems == null || item.subItems.length == 0)
        return false;
    return item.subItems.filter(i => i.type == "Table").length > 0;
}


// Displayr wrapper for setQuestion()
// Allows the user to specify variable set structure names
// rather than question types.
combineVariablesAsSet = function(name, structure, variables) {
    let data_file = variables[0].question.dataFile;
    variables.forEach(function (v) {
        if (!v.question.dataFile.equals(data_file))
            throw "Variables to combine must come from a single Data Set";
    });

    
    // Translate structure to appropriate question type
    let question_type = structure;
    switch(structure) {
        case "Nominal":
            question_type = "Pick One";
            break;
        case "Ordinal":
            question_type = "Pick One";
            break;
        case "Numeric":
            question_type = "Number";
            break;
        case "Date/Time":
            question_type = "Date";
            break;
        case "Binary - Multi":
            question_type = "Pick Any";
            break;
        case "Nominal - Multi":
            question_type = "Pick One - Multi";
            break;
        case "Ordinal - Multi":
            question_type = "Pick One - Multi";
            break;
        case "Numeric - Multi":
            question_type = "Number - Multi";
            break;
        case "Binary - Grid":
            question_type = "Pick Any - Grid";
            break;
        case "Numeric - Grid":
            question_type = "Number - Grid";
            break;
        case "Binary - Multi (Compact)":
            question_type = "Pick Any - Compact";
            break;
    }

    // Check for duplicated name
    name = preventDuplicateQuestionName(data_file, name);

    // Set Question
    let question = data_file.setQuestion(name, question_type, variables);

    if (structure.indexOf("Ordinal") > -1) {
        variables.forEach(function (v) {
            v.variableType = "Ordered Categorical";
        });
    }

    return question;

}

checkFileHasMoreThanOneCase = function(data_file) {
    let number_cases = data_file.totalN;
    if (number_cases < 2)
        throw new UserError('It is not possible to add R variables to data sets with a single case. Consider using ' 
            + (inDisplayr() ? 'a Calculation' : 'an R Output') + ' instead.');
    return;
}

// Given a nominal/ordinal or nominal/ordinal - multi
// infer the factor levels that will be available when
// that question is referenced in R.
// The rule is, levels are:
// - Row labels which appear in the data reduction
// - MINUS any NET/merged categories where any of the
//   constituent categories still appear in the data
//   reduction.
// - PLUS any hidden categories which do not appear
//   as constituents of categories we have kept so far
// Categories set as Missing Data are always excluded.
getRFactorLevelsFromQuestionOrVariable = function(question) {

    if (question.type == "Variable") // passed a variable
        question = question.question;

    // Identify labels in the data reduction
    let data_reduction = question.dataReduction;
    let hidden_values = data_reduction.hiddenValues();
    let current_labels = (question.questionType == "Pick One - Multi" && data_reduction.transposed) ? data_reduction.columnLabels : data_reduction.rowLabels;

    // Map labels to underlying values
    let underlying_values = current_labels.map(function (x) {
        return { label: x, values: data_reduction.getUnderlyingValues(x)};
    });

    // Remove any composite categories whose constituents still appear in the
    // value attributes
    underlying_values.forEach(function (x) {
        let x_values = x.values;
        let initial_length = x_values.length;
        underlying_values.forEach(function(y) {
            let y_values = y.values;
            if (x.label != y.label && y_values.length < x_values.length && y_values.length > 0) {
                if (y_values.every(z => x_values.indexOf(z) > -1))
                    x_values = x_values.filter(z => y_values.indexOf(z) > -1);
            }
        });
        if (x_values.length < initial_length)
            x.values = [];
    });
    underlying_values = underlying_values.filter(x => x.values.length > 0);

    let remaining_non_hidden_values = [];
    underlying_values.forEach(function (x) {
        remaining_non_hidden_values = remaining_non_hidden_values.concat(x.values);
    });

    // Add in any hidden codes which have not been accounted for so far.
    let level_labels = underlying_values.map(x => x.label);
    hidden_values.forEach(function (x) {
        if (remaining_non_hidden_values.indexOf(x) == -1) {
            level_labels.push(x.originalLabel);
        }
    })

    return {labels: level_labels};
}

// Deduce the best possible delimiter to use to split a string
function determineDelimiterToUse(labels)
{
    let all_labels = labels.join('');
    if (!all_labels.includes(';'))
        return ';';
    return !all_labels.includes(',') ? ',' : ';';
}

// Identify if an item in the Report has any sub items.
// Note that the property of having sub items is no longer 
// restricted to the Report Group type as R items can
// be nested below plots.
function hasSubItems(item) {
    if (typeof item.subItems === "undefined")
        return false;
    return (typeof item.subItems !== null && item.subItems.length > 0);
}

See also