QScript Utility Functions

From Q
Jump to: navigation, 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.

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)

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.

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.

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');

// 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 error_message;
    array.forEach(function (o) {
        if (typeof o.type == "undefined")
            throw error_message;
        if (o.type != type_name)
            throw "Expected a " + type_name + " but got " + o.type;
    });
}

// Check that the question is of one of the types in type_array
function checkQuestionType(question, type_array) {
    if (question.type != "Question")
        throw "Expected a question";
    if (type_array.indexOf(question.questionType) == -1)
        throw 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; }) ;
}

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

// 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) {
    var new_variable_name = variable_name;
    var c = 1;
    while(true) {
        is_duplicate = data_file.getVariableByName(new_variable_name) != null;
        if (!is_duplicate)
            return new_variable_name;
        new_variable_name = variable_name + 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.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) {
            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 if (q_flattened.questionType != "Pick Any") {
            q_flattened = q_flattened.duplicate(preventDuplicateQuestionName(data_file, q_flattened.name))
            q_flattened.questionType = "Pick Any";
        }
        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.length == 1 && selected_questions[0].variables.length == 1
                            || 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();
    var data_file = selected_variables[0].question.dataFile;
 
    if (selected_variables.length == 0) {
        log("Nothing selected.");
        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