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.

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).

Source Code

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


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 asks the user to select questions as input for the Qscript
// It can be used in either Q or Displayr. The allowed structures should be
// specified as VariableSetStructures. In Q, the error message will automatically
// convert this to QuestionTypes.
// If a question or table is already highlighted and is one of the 'allowed structures'
// it will be used without any further dialogue. If the a question or table is highlighted
// but not appropriate, we assume that this selection was unintentional. In this case
// the behaviour will be the same as if the no question was selected. It will ask the
// the user to select a data file, and then a variable of the appropriate type.
// Note that the user is never asked about the variable type.
// If the user is only allowed to select one question, set 'single' to true.

function selectInputQuestions(allowed_structures, single, select_scale_questions, add_levels) {

    if (typeof single == "undefined")
        single = false;
    if (typeof select_scale_questions == "undefined")
        select_scale_questions = false;
    if (typeof add_levels == "undefined")
        add_levels = false;

    if (!requireDataFile())
        return false;
    var is_displayr = (!!Q.isOnTheWeb && Q.isOnTheWeb());
    var structure_name = is_displayr ? "variable set" : "question";
    if (!single)
        structure_name = structure_name + "s";
    var selected_questions = project.report.selectedQuestions();

    if (selected_questions.length > 0) {
        // Remove and silently ignore questions that do not have allowed structures
        var sorted_selection = splitArrayIntoApplicableAndNotApplicable(selected_questions, 
            function (q) { return allowed_structures.indexOf(q.variableSetStructure) != -1 && !q.isBanner; });
        selected_questions = sorted_selection.applicable;
    }
    if (single && selected_questions.length == 1)
        return (selected_questions[0]);

    if (selected_questions.length == 0 || single) { 
        if (fileFormatVersion() < 13.05) {
            log("This QScript is not supported in this version of Q. Please use release version 6.4.1.0 or later to use this QScript.");
            return false;
        }
        var data_file = requestOneDataFileFromProject();
        var candidate_questions = getAllQuestionsByStructures([data_file], allowed_structures);
        if (candidate_questions.length === 0) {
            if (!is_displayr)
                convertStructureToType(allowed_structures);
            log("No appropriate " + printTypesString(allowed_structures) + " " + structure_name + " found in the data file. " + 
			"Please choose another data file or create " + structure_name + " with this structure.");
            return false;
        }
		// This section gives the user the option of only showing scale identified questions
		// so the user can have a pre-filtered selection. Used for the topAndBottomBoxNetCreator function
		if (select_scale_questions) {
			var question_looks_like_scale = getAllScaleQuestions([data_file]);
			if (question_looks_like_scale.length > 0) {
				if (askYesNo("You will now be shown a list of " + structure_name + " to choose from. Would you like to be shown only " + structure_name + " that look like scales?"))
					candidate_questions = question_looks_like_scale;
			}
		}
		// This section adds the lowest and highest value and labels to the question label 
		// so the user can inspect it during selection. Used for the topAndBottomBoxNetCreator function
		if (add_levels) {
			var question_label_strings = candidate_questions.map(function (q) {
            var highest_and_lowest_label = getHighestAndLowestValueAndLabel(q);
            return truncateStringForSelectionWindow(q.name) + "  (" 
                + highest_and_lowest_label.lowest + " ... " 
                + highest_and_lowest_label.highest + ")";
			})
			var selected_indices = selectMany("Please choose which " + structure_name + " you want to add NETs to:\r\n(highest and lowest value labels are shown in brackets)", question_label_strings);
			selected_questions = getElementsOfArrayBySelectedIndices(candidate_questions, selected_indices);
        } else {
			if (single)
				selected_questions = selectOneQuestion("Select " + structure_name + ":", candidate_questions, true);
			else
				selected_questions = selectManyQuestions("Select " + structure_name + ":", candidate_questions, true).questions;
		}
    }
    if (selected_questions.length == 0)
        return false;
    return(selected_questions);
}



// 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 (var i = 0; i < questions.length; i++) {
        if (!questions[i].isValid) {
            log("Question '" + questions[i].name + "' is invalid.");
            return false;
        }
        var isEmpty = true;
        for (var j = 0; isEmpty && j < questions[i].uniqueValues.length ; j++)
            isEmpty = !isFinite(questions[i].uniqueValues[j]);
        if (isEmpty) {
            log("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);
}

function reportNewRQuestion(questions, top_group_name) {

    var is_displayr = (!!Q.isOnTheWeb && Q.isOnTheWeb());

    // Currently nothing is reported inside Displayr
    if (!is_displayr){
        var is_single_q = typeof(questions.dataFile) !== 'undefined';
        if (is_single_q)
        {
            var data_file = questions.dataFile;
            var new_name = prompt("Enter a name for the new question : ", questions.name);
            if (new_name !== questions.name)
                questions.name = new_name;
            var new_group = generateGroupOfSummaryTables(top_group_name, [questions]);
        }
        else
        {
            var data_file = questions[0].dataFile;
            var new_group = generateGroupOfSummaryTables(top_group_name, questions);
        }
        
        // This currently does not allow the variable to be selected
        // which is why the summary tables are created instead
        // (can be updated after RS-6669 is completed)
        if (fileFormatVersion() > 8.65) 
            project.report.setSelectedRaw([new_group.subItems[0]]);
        else
        {
            if (is_single_q)
                log("The transformed question '" + questions.name + "' has been added to the dataset " + data_file.name);
            else
                log("Tables showing the new questions have been added into the folder " + top_group_name);
        }
    }
}


// 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++;
    }
}

// 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;
}

See also