QScript Utility Functions

From Q
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 Array Functions");
includeWeb("JavaScript Text Analysis Functions");
includeWeb("JavaScript Utilities");
includeWeb("QScript Value Attributes Functions");

function isQuestion(item) {
    return typeof item === "object" && item.type === "Question";
}

function isVariable(item) {
    return typeof item === "object" && item.type === "Variable";
}

function isReportGroup(item) {
    return typeof item === "object" && item.type === "ReportGroup";
}

function isTable(item) {
    return typeof item === "object" && item.type === "Table";
}

function isPlot(item) {
    return typeof item === "object" && item.type === "Plot";
}

function isROutput(item) {
    return typeof item === "object" && item.type === "R Output";
}

// Move one or more questions to the location of the hover button
// if the user has initiated the script from the Data Sources 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;
    }

    const inserting_at = project.getDataInsertingAtItem();
    if (inserting_at === null) {
        return;
    }

    const data_file = inserting_at.dataFile;

    // Identify the target variable to move after.
    // If inserting above, identify the last variable before the question
    let target_variable = inserting_at.variables[0];
    if (project.isInsertingAbove()) {
        const all_questions = data_file.questions;
        const index = all_questions.findIndex(q => 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) {
            const variables = q.variables;
            data_file.moveAfter(variables, target_variable);
            target_variable = variables[variables.length - 1];
        }
    });
}

function insertAtHoverButtonIfShown(question) {
    if (typeof (project.getDataInsertingAtItem) === "undefined") {
        return;
    }

    const inserting_at = project.getDataInsertingAtItem();
    if (inserting_at === null) {
        return;
    }

    const data_file = inserting_at.dataFile;
    const variables = question.variables;
    if (question == null || variables == null) {
        return;
    }

    if (!project.isInsertingAbove()) {
        data_file.moveAfter(variables, inserting_at.variables[0]);
    }
    else {
        const all_questions = data_file.questions;
        const index = all_questions.findIndex(q => 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 (!isArray(questions)) {
        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 is_empty = true;
        for (let j = 0; is_empty && j < questions[i].uniqueValues.length; j++) {
            is_empty = !isFinite(questions[i].uniqueValues[j]);
        }

        if (is_empty) {
            log(correctTerminology("Question '") + questions[i].name + "' is empty.");
            return false;
        }
    }

    return true;
}

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

function printTypesString(x, conjunction) {
    if (x.length <= 1) {
        return x;
    }

    const 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 (let 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) {
    const question = variable.question;
    const attributes = question.valueAttributes;
    const values = variable.uniqueValues;
    const relevant_values = values.filter(x => {
        return !isDontKnow(attributes.getLabel(x)) && !isNaN(attributes.getValue(x)) && !attributes.getIsMissingData(x);
    });
    return getLabelsForValues(question, relevant_values);
}

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

function getDataFileFromQuestions(questions) {
    if (!isArray(questions)) {
        return questions.dataFile;
    }

    const data_file = questions[0].dataFile;

    // Make sure all questions are from the same data set
    if (!questions.map(q => q.dataFile.name).every(type => type == data_file.name)) {
        throw new UserError("Selected questions or variables are from different Data Sources 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) {
    const error_message = "Expected an array of: " + type_name;
    if (!Array.isArray(array)) {
        throw new CallerCallerError(error_message);
    }

    array.forEach(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.
let CallerError;
try {
    CallerError = eval('class CallerError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; CallerError');
}
catch {
    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.
let CallerCallerError;
try {
    CallerCallerError = eval('class CallerCallerError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; CallerCallerError');
}
catch {
    CallerCallerError = Error; // <= Q5.3 (ES5)
}

// Throw this error when you do want to trigger a bug report. The message will be
// shown to the user including a traceId and a budupId.
let BugButShowToUserError;
try {
    BugButShowToUserError = eval('class BugButShowToUserError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; BugButShowToUserError');
}
catch {
    BugButShowToUserError = 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.
let UserError;
try {
    UserError = eval('class UserError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; UserError');
}
catch {
    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) < 0) {
        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) {
    let selected_questions = [];
    const num_files = data_file_array.length;
    for (let j = 0; j < num_files; j++) {
        selected_questions = selected_questions.concat(data_file_array[j].questions.filter(question => {
            return array_of_types.indexOf(question.questionType) != -1;
        }));
    }

    return selected_questions.filter(q => !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) {
    let selected_questions = [];
    const num_files = data_file_array.length;
    for (let j = 0; j < num_files; j++) {
        selected_questions = selected_questions.concat(data_file_array[j].questions.filter(question => {
            return array_of_types.indexOf(question.variableSetStructure) != -1;
        }));
    }

    return selected_questions.filter(q => !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) {
        let qcounter = 1;
        let is_duplicate = true;
        let 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) {
    let new_variable_name = variable_name;
    let c = 1;
    if (separator === undefined) {
        separator = "";
    }

    // eslint-disable-next-line no-constant-condition
    while (true) {
        const 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) {
    const suffix = "_1";
    let new_variable_base_name = variable_base_name;
    let c = 1;
    if (separator === undefined) {
        separator = "";
    }

    // eslint-disable-next-line no-constant-condition
    while (true) {
        const 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 || "";

    // eslint-disable-next-line prefer-spread
    const char_codes = Array.apply(null, Array(len)).map(() => 'a'.charCodeAt(0) + Math.floor(26 * Math.random()));
    return prefix + String.fromCharCode.apply(null, char_codes) + suffix;
}


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

    return arrayHasDuplicateElements(labels);
}

function pickOneMultiToPickAnyFlattenByRows(question) {

    checkQuestionType(question, ["Pick One - Multi"]);
    const new_vars = [];
    let new_var;
    const q_vars = question.variables;
    let last_var = q_vars[q_vars.length - 1];
    const num_q_vars = q_vars.length;
    const value_attributes = question.valueAttributes;
    const q_unique_values = question.uniqueValues;
    const value_labels = valueLabels(question);
    let 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 (let 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;
    let cur_var_label;
    let cur_value_label;
    let cur_var_name;
    let cur_val;
    let new_expression;
    let new_label;
    for (let j = 0; j < num_q_vars; j++) {
        cur_var_label = q_vars[j].label;
        cur_var_name = q_vars[j].name;
        const name_prefix = cur_var_name + makeid() + '_flat_';
        for (let 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);
        }
    }

    const new_q_name = preventDuplicateQuestionName(question.dataFile, question.name + " (flattened)");
    const 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() {
    let text = "";
    const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    for (let 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);
    const variables_linked = [];
    let v_linked = null;
    const single_question = fromSameQuestion(variables);
    for (let i = 0; i < variables.length; i++) {
        const v = variables[i];
        const linked_name = preventDuplicateVariableName(data_file, v.name + '_L');
        let linked_label;
        if (single_question && !include_question_name_in_label) {
            linked_label = v.label;
        }
        else {
            linked_label = getQuestionNameAndVariableLabel(v);
        }

        v_linked = data_file.newJavaScriptVariable(quoteVariableNameForJavaScript(v.name), false, linked_name, linked_label, null);
        variables_linked.push(v_linked);
    }

    return data_file.setQuestion(question_name, question_type, variables_linked);
}

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

function getVariables(questions_or_variables) {
    const variables = [];
    questions_or_variables.forEach(q_or_v => {
        if (isQuestion(q_or_v)) {
            q_or_v.variables.forEach(variable => {
                variables.push(variable);
            });
        }
        else if (isVariable(q_or_v)) {
            variables.push(q_or_v);
        }
    });
    return variables;
}

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

    return variables;
}

function isQuestionNumCat(question) {
    const 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 {
        let name = variable.question.name;
        if (name.length > 50) {
            name = name.substring(0, 50) + '...';
        }

        return name + ': ' + variable.label;
    }
}

function fromSameQuestion(variables) {
    for (let 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.
//
// If `flattenForCrosstabs` is set it will only flatten questions if
// question.isFlattenedInCrosstabs is false.
function flattenSelectedQuestions(selected_questions, flattenForCrosstabs) {
    let q;
    let data_file;
    for (let i = 0; i < selected_questions.length; i++) {
        q = selected_questions[i];
        data_file = q.dataFile;

        const flattened_name = q.name + ' (flattened)';
        let q_flattened = data_file.getQuestionByName(flattened_name);
        if (q_flattened == null) {
            let flattened = true;
            if (flattenForCrosstabs && q.isFlattenedInCrosstabs) {
                q_flattened = null;
                flattened = false;
            }
            else 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) {
    const labels_diff = difference(labels_1, lables_2);
    if (labels_diff.length > 0) {
        let message = 'There are no ' + measure_name + ' measurements for:\n\n';
        for (let 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
    let end = 1;
    for (; 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) {
    let longest_prefix = labels[0];

    labels.forEach((label, ind) => {
        if (ind > 0) {
            const 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) {
        let j = 0;
        let 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;
        }
    }

    const 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.
    const selected_questions = project.report.selectedQuestions();
    let selected_variables = project.report.selectedVariables();

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

    const 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(q => {
            selected_variables = selected_variables.concat(q.variables);
        });
    }

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

    const data_file = selected_variables[0].question.dataFile;
    if (!selected_variables.map(v => v.question.dataFile.name).every(ff => 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;
    // }

    const all_variables = data_file.variables;
    const all_questions = data_file.questions;
    const index_of_first_variable = getVariableIndex(selected_variables[0], all_variables);
    const index_of_first_question = getVariableIndex(selected_questions[0], all_questions);
    const 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
    let target_variable = null;
    if (move_type == "Bottom") {
        target_variable = all_variables[all_variables.length - 1];
    }
    else if (move_type == "Up") {
        if (moving_questions) {
            const index_above = index_of_first_question - 2;
            if (index_above > -1) {
                const question_above = all_questions[index_above];
                target_variable = question_above.variables[question_above.variables.length - 1];
            }
        }
        else {
            const 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)) {
                const 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) {
                const question_below = all_questions[index_of_last_question + 1];
                target_variable = question_below.variables[question_below.variables.length - 1];
            }
        }
        else {
            const q_vars = selected_questions[0].variables;
            const 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) {
                const vars_not_selected = q_vars.filter(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) {
    const uniques = [];
    array.forEach(obj => {
        if (!uniques.some(o => obj.equals(o))) {
            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) {
    const guids = array.map(x => 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) {
    let item_dependants = item.dependants(false).filter(item => item.type == "Question" || item.type == "Variable");
    item_dependants = item_dependants.filter(removeErroneousDependant);
    if (item_dependants.length == 0) {
        return null;
    }

    const first_dependant = item_dependants[0];
    if (isQuestion(first_dependant)) {
        return first_dependant.dataFile;
    }

    if (isVariable(first_dependant)) {
        return first_dependant.question.dataFile;
    }

    return null;
}

function removeErroneousDependant(dependant) {
    const forbidden_dependent_names = ["c", "list"];
    if (["Question", "Variable"].indexOf(dependant.type) === -1) {
        return true; //Only care about variable set dependencies.
    }

    if (forbidden_dependent_names.indexOf(dependant.name) > -1) {
        return false; //Bad question or variable
    }

    return true;
}

// Returns the data file of the first item in the R output control.
function getDataFileFromROutputInput(r_output, control_name) {
    let inputs;
    try { // RS-3471
        inputs = r_output.getInput(control_name);
    }
    catch {
        inputs = null;
    }

    if (inputs == null) {
        return null;
    }

    if (!isArray(inputs)) {
        if (isQuestion(inputs)) {
            return inputs.dataFile;
        }

        if (isVariable(inputs)) {
            return inputs.question.dataFile;
        }

        return null;
    }

    inputs = inputs.filter(item => item.type == "Question" || item.type == "Variable");
    if (inputs.length == 0) {
        return null;
    }

    if (isQuestion(inputs[0])) {
        return inputs[0].dataFile;
    }

    if (isVariable(inputs[0])) {
        return inputs[0].question.dataFile;
    }

    return null;
}

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().
    /* eslint-disable @typescript-eslint/naming-convention */
    const 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,
    };
    /* eslint-enable @typescript-eslint/naming-convention */

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

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

    let 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) {
    const data_file = variables[0].question.dataFile;
    const data_file_vars = data_file.variables;
    const data_file_vars_names = [];
    for (let i = 0; i < data_file_vars.length; i++) {
        data_file_vars_names.push(data_file_vars[i].name);
    }

    let last_variable = variables[0];
    let last_index = data_file_vars_names.indexOf(last_variable.name);
    for (let i = 1; i < variables.length; i++) {
        const 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) {
    const in_displayr = inDisplayr();
    // Mapping between Q and Displayr terms
    const 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" },
        { displayr: "Data Source", q: "Data Set" },
        { displayr: "data source", q: "data set" },
        { displayr: "Data source", q: "Data set" }];

    terminology_dictionary.forEach(obj => {
        // Convert target string to a global regex so that all instances are replaced at once.
        const target = in_displayr ? new RegExp("\\b" + obj.q + "\\b", "g") : new RegExp("\\b" + obj.displayr + "\\b", "g");
        const 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() {
    const is_displayr = inDisplayr();
    const group = !project.currentPage ? false : project.currentPage();
    if (group) {
        return group;
    }

    if (!is_displayr) {
        const selected_items = project.report.selectedRaw();
        const selected_item = selected_items[0];
        if (selected_item == null) {
            return project.report;
        }

        return selected_item.type == 'ReportGroup' ? selected_item : selected_item.group;
    }

    return project.IsInsertingSingleOutput ? project.report : project.report.appendPage('TitleOnly');
}

function addStandardRToCurrentPage(page_name, controls, item_layout) {
    if (item_layout == null) {
        item_layout = determineItemLayout();
    }

    const group = determineCurrentPage();
    if (group) {
        const standard_r_output = group.appendStandardR(page_name, controls, project.IsInsertingSingleOutput, item_layout);
        project.report.setSelectedRaw([standard_r_output]);
        return standard_r_output;
    }

    return null;
}

function determineItemLayout() {
    return inDisplayr() ? showItemInserter() : null;
}

function rItemIsPlot(item) {
    const 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.
function combineVariablesAsSet(name, structure, variables) {
    const 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
    const question = data_file.setQuestion(name, question_type, variables);

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

    return question;
}

function checkFileHasMoreThanOneCase(data_file) {
    const 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.');
    }
}

// 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.
function getRFactorLevelsFromQuestionOrVariable(question) {
    var _a;
    if (isVariable(question)) {
        question = question.question;
    }

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

    // Map labels to underlying values
    let underlying_values = (_a = current_labels === null || current_labels === void 0 ? void 0 : current_labels.map(function (x) {
        return { label: x, values: data_reduction.getUnderlyingValues(x) };
    })) !== null && _a !== void 0 ? _a : [];

    // Remove any composite categories whose constituents still appear in the
    // value attributes
    underlying_values.forEach(x => {
        let x_values = x.values;
        const initial_length = x_values.length;
        underlying_values.forEach(y => {
            const 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(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.
    const level_labels = underlying_values.map(x => x.label);
    hidden_values.forEach(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) {
    const 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 (item.subItems != null && item.subItems.length > 0);
}

/** Helper: tidy up question name for hyperlink text of quoted items */
function quoteForLink(text) {
    if (!text) {
        return `""`;
    }

    // double-quotes is reserved
    return `"${text.replace('"', '`')}"`;
}

/** Sets the title of a page */
function setPageTitle(page, title, page_type) {
    page.name = title;
    try {
        if (page_type !== "Blank") {
            const title_text = page.subItems.filter(item => item.type === "Text" &&
                item.name === "Title placeholder")[0];
            // Check existence of title text as this depends on Page Master layout
            if (typeof (title_text) !== 'undefined' && Object.prototype.hasOwnProperty.call(title_text, 'text')) {
                title_text.text = title;
            }
        }
    }
    catch (error) {
        log(`Could not update page title for ${title}. Error was: ${error}`);
    }
}

See also