QScript Questionnaire Functions

From Q
Jump to navigation Jump to search

The functions below have been designed to help identify common labeling in English-language questionnaires and make manipulations based on common conventions in the labeling. For example, the function looksLikeScale(question) is used to try to identify when the labels in a question suggest that the question represents a likelihood-to-recommend, Likert, or other scale. Similarly, the function checkQuestionsForDK(selected_questions) is used to search an array of questions to identify those questions which contain a Don't Know response.

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

looksLikeScale(question)

This function returns true if the input question looks like a scale question. This is determined by looking at the category labels in the question, ignoring those categories that have been set as Missing Data and those categories that look like 'Don't Know' style responses (according to the function isDontKnow).

Questions are considered to be scale questions if:

  1. All the labels are numbers, and they form a 1-5, 1-7, 1-9, 1-10, or 0-10 scale.
  2. The labels can all be converted into numbers and where the label for the lowest value contains a '0' or a '1'.
  3. The labels for the lowest or highest values contain strings commonly found in scale questions, including: agree, definite, and satis (for satisfaction).
  4. The labels for the lowest and highest values both contain strings commonly found in scale questions, including: likely, recomm, different, familiar, fit, favourabl, favorabl, important, associate, probabl, average, happy, and interested.
  5. More than one label contains a range of numbers (which means that it contains the string ' to ' or ' - ' and at least part of the label can be interpreted as a number, eg one to four ).

Questions that are considered not to be scale questions if:

  1. There are fewer than 3 non-missing categories.
  2. There are more than 12 non-missing categories.
  3. The labels contain numbers and the last label is open-ended, e.g. 5 or more.
  4. The first labels are numbers but the last labels don't contain numbers and cannot be converted into numbers.

getAllScaleQuestions(data_file_array)

This function returns an array of questions which have the Question Type of Pick One or Pick One - Multi and are scale questions according to the function looksLikeScale. This only looks at the data files specified in data_file_array.

questionHasDontKnowOption(question)

This function returns true if the input question has any value labels that look like 'Don't Know' responses according to the function isDontKnow.

questionHasNonMissingDontKnowOption(question)

This function returns true if the input question has any non-missing value labels that look like 'Don't Know' responses according to the function isDontKnow.

computeMidpointsInQuestion(question, delimiters)

This function searches the value labels for a question for ranges of numbers (e.g.: "Ages 18 to 24") and computes a mid-point for each. The function then returns an array of objects containing information about the mid-points that have been found. Each object in the array has the properties:

  • sourceValue - the source value for the label corresponding to this mid-point.
  • inferredValue - the midpoint that has been calculated for this source value.
  • genuineMidPoint - a boolean value that indicates whether the mid-point was computed using a pair of numeric values.

Where the label contains a range of numbers, for example 18 to 24 then the midpoint value will be used (for example 21 in this case). If a question is recoded according to mid-points and it contains a lower label like Less than 18 then the midpoint will be half-way between zero and the number in the label (in this example 9. When the question is recoded according to mid-points and it contains an upper label like 55 or more then the midpoint will be the number in the label plus half of the previous interval - so if the previous interval was 50 to 54 this midpoint will be set to 57. If no midpoint for a label can be determined then a value of NaN will be assigned.

If more than half of the labels in the question contain text in brackets then extract that text and use it rather than using the full label.

extractStringsFromBrackets(labels)

If the string has open brackets of any kind, eg (, [, [[, {, etc then return what comes between the first open bracket and first closed bracket, or between the first open bracket and the end of the string (in case the closed brackets got truncated).

numberOfMidpoints(midpoints)

Given an array of midpoint objects that has been generated by computeMidpointsInQuestion, this function counts the number that are considered to be genuine midpoints.

getInferredValueFromMidpoints(midpoints, source_value)

Given an array of midpoints generated by computeMidpointsInQuestion, and a source_value, this function returns the inferred midpoint value for that source value. If the source value is not found the this function returns a NaN.

highestLabelIsDK(question)

Returns true if the label corresponding to the largest unique value in the input question is a Don't Know style response according to the function isDontKnow.

otherSourceValues(question)

This function returns the source values from the input question that correspond to Other/Specify type responses according to the function isOther.

tidyOtherLabel(question, other_questions, possible_other_questions)

This function attempts to identify and relabel Other style value labels in the input question. The source values corresponding to Other style responses are identified using the function otherSourceValues.

If a single Other style response is identified, then it is relabeled as Other. The question is added to the input array of other_questions. This array contains questions that are known to have a single category corresponding to Other.

If more than one Other style response is identified, then no labels are changed and the question is then added to the input array of possible_other_questions. These questions can then be assigned for the user to check and determine which response corresponds to a true Other response.

If the question contains no Other responses then this function does nothing with the input question.

numericLabelsWithUnquantifiableTopScalePoints(labels)

This function returns true when the first labels are numbers but the last labels are not convertible into numbers.

numericLabelsWithOpenEndedTopPoints(labels)

This function returns true if the input labels all contain numbers but the last one is open-ended. Open-ended words indicate that the category refers to all numbers larger, and the strings that are used to determine this are described in openEndedTopLabel

An example of a set of labels that satisfies this criteria is:

1, 2, 3, 4, 5 or more

It is best to remove any labels that correspond to missing values before checking labels with this function.

validNumericScale(labels)

This function returns true if the input labels are all numbers and form a 1-5, 1-7, 1-9, 1-10, or 0-10 scale.

It is best to remove any labels that correspond to missing values before checking them with this function.

checkQuestionsForDK(selected_questions)

This function checks the array of selected_questions for Don't Know categories. If any such categories are found then the user is prompted to continue, or to cancel and run a script to remove the Don't Know categories.

This function returns an object with two elements:

  • questionsContainingDK - an array of question objects where Don't Know categories have been identified.
  • userCancelled - a boolean flag which is true if the user opted not to continue with the current script.

getDKQuestionsWithUserEnteredLabels(quetion_types, extra_dk_strings)

Present the user with a list of questions containing Don't Know options. If extra labels are to be considered Don't Know options, then these can be passed in via the array extra_dk_strings.

midpointAndQuantificationRecoding(add_new_vars)

Function to do recoding of questions based on midpoints and numbers in labels. This function generates the dialogues for the user, does the recoding, and generates the output. The parameter add_new_vars should be true if you want to generate new variables in addition to recoding the original questions.

isLikelihoodScale(question, require_likely_in_label)

This function inspects the values, labels, and question name to determine if a question is a likelihood-scale question.

Source Code

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

// Identify Scale Questions
 
// This script contains functions which work based on assumptions
// about common labeling conventions in questionnaires. For example
// what types of words are typically found in five-point scale
// questions?
 
// These functions are kept in the one place to prevent the user from
// having to hunt around in different files in order to change the
// conventions.
 
// checks to see if a labels resemble a scale of some kind by looking at the value attributes
// Only looks at labels for non-missing values, and labels that don't look like a 'Don't Know'
// or N/A response.
function looksLikeScale(question) {
 
    var labels = valueLabels(question);
    var num_labels = labels.length;
    var value_attributes = question.valueAttributes;
 
    // Determine which labels in the array of labels correspond to missing values
    // or 'Dont Know' options
    var missing_value_indicators = valueAttributesMissingValueIndicatorArray(question);
    var dk_indicators = labels.map(function (x) { return isDontKnow(x); } );
 
    // Only pick out labels that are not selected as Missing Data
    // and not a 'Don't Know' type response.
    var candidate_labels = [];
    for (var j = 0; j < num_labels; j++)
        if (missing_value_indicators[j] == 0 && dk_indicators[j] == 0)
            candidate_labels.push(labels[j]);
 
    return labelsLookLikeScale(candidate_labels);
}
 
 
// Function to determine if the question looks like a scale question
function labelsLookLikeScale(candidate_labels) {
    var num_candidate_labels = candidate_labels.length;
    var highest_label = candidate_labels[num_candidate_labels - 1]; //highest value's label
    var lowest_label = candidate_labels[0]; //lowest value's label
 
    if (num_candidate_labels > 2 && num_candidate_labels <= 12) {
 
        var labels_are_numeric = candidate_labels.filter(isNumber).length == num_candidate_labels;
 
        // If all of the labels are numbers then return true if the numbers are a valid scale
        // and return false otherwise
        if (labels_are_numeric)
            if (validNumericScale(candidate_labels))
                return true;
            else
                return false;
 
        var quantified_labels = candidate_labels.map(quantify);

        var labels_are_quantifiable = quantified_labels.filter( function (x) { return !isNaN(x); }).length == num_candidate_labels;

        if (labels_are_quantifiable) {
            // If lower labels are numbers but the higest labels aren't quanitfiable
            if (numericLabelsWithUnquantifiableTopScalePoints(candidate_labels))
                return false;
            else if (numericLabelsWithOpenEndedTopPoints(candidate_labels))
                return false;
            //e.g., How do you feel about ... '0 will definitely not buy', '1 might buy'
            else if (lowest_label.indexOf('0 ') != -1 
                        || lowest_label.indexOf(' 0') != -1 
                        || lowest_label.indexOf('(0)') != -1
                        || highest_label.indexOf('0 ') != -1 
                        || highest_label.indexOf(' 0') != -1 
                        || highest_label.indexOf('(0)') != -1)
                return true;
            //e.g., How do you feel about ... '1 will definitely not buy', '2 might buy'
            else if (lowest_label.indexOf('1 ') != -1 
                || lowest_label.indexOf(' 1') != -1 
                || lowest_label.indexOf('(1)') != -1
                || highest_label.indexOf('1 ') != -1 
                || highest_label.indexOf(' 1') != -1 
                || highest_label.indexOf('(1)') != -1)
                return true;
        }
 
        // Check to see if any of the most commonly-used scale labels are used in the first or
        // last label
 
        var possible_scale_label_parts = ['agree','definite','satis'];
        for (var i in possible_scale_label_parts ) {
            var possible_part = possible_scale_label_parts[i];
            if (lowest_label.toLowerCase().indexOf(possible_part) != -1 || highest_label.toLowerCase().indexOf(possible_part) != -1) {
                return true;
            }
        }
 
        //checks if the same word appears in the first and last label, where the word is the type used in scales
        var possible_scale_label_parts = ['likely','recomm','different','familiar','fit','favourabl','favorabl','important','associate','probabl','average', 'happy', 'interested'];
        for (var i in possible_scale_label_parts ) {
            var possible_part = possible_scale_label_parts[i];
            if (lowest_label.toLowerCase().indexOf(possible_part) != -1 && highest_label.toLowerCase().indexOf(possible_part) != -1)
                return true;
        }
 
        // Check the highest and lowest labels for oppositional words.
        // Both the highest label and the lowest label should contain a word from this list
        var oppositional_words = ["like", "love", "hate"];
        var high_found = false;
        var low_found = false;
        oppositional_words.forEach( function(word) {
            if (lowest_label.toLowerCase().indexOf(word) > -1)
                low_found = true;
            if (highest_label.toLowerCase().indexOf(word) > -1)
                high_found = true;
        });
        if (high_found * low_found)
            return true;
 
        //checks to see if the question contains numbers, or, ranges in at least 2 items
        var counter = 0;
        candidate_labels.forEach(function(entry) {
            if (isRange(entry) && !isNaN(quantify(entry))) {
                counter ++;
            }
        });
 
        if (counter >=2)
            return true;
    }
    return false;
}
 
function getAllScaleQuestions(data_file_array) {
    var candidates = getAllQuestionsByTypes(data_file_array, ["Pick One", "Pick One - Multi"]);
    return candidates.filter(looksLikeScale);
 
}
 
// Returns true if the input question has a 'Dont Know' option among any of its value labels
function questionHasDontKnowOption(question) {
    return valueLabels(question).filter(isDontKnow).length > 0;
}

// Returns true if the input question has a 'Dont Know' option among any of its value labels
function questionHasNonMissingDontKnowOption(question) {
    return nonMissingValueLabels(question).filter(isDontKnow).length > 0;
}
 
 
// computes the midpoints for the value labels of a question.
// The labels are split up by the specified array of delimiters.
function computeMidpointsInQuestion(question, delimiters) {
    var midpoints = [];
    var num_vals = question.uniqueValues.length;
    var last_range = 0;
    var source_values = question.uniqueValues;
    var all_labels = valueLabels(question);
 
    // If more than half of the labels contain strings in brackets eg (...)
    // Then grab the string from inside the brackets rather than looking at
    // the full labels/
    var labels_in_brackets = extractStringsFromBrackets(all_labels);
    var num_in_brackets = labels_in_brackets.filter(function (label) { return label.length > 0; }).length;
    if (num_in_brackets > all_labels.length / 2)
        all_labels = labels_in_brackets; // Enough of the labels contain brackets, so use the contents of the brackets in place of the original labels
 
    all_labels = all_labels.map(function (x) { return x.toLowerCase(); } );
    for (var value_i = 0; value_i < num_vals; value_i++) {
        var source_value = source_values[value_i];
        var label = all_labels[value_i];
        var array_of_values = quantifyArray(labelAsArray(label, delimiters, 2));
        if (containsSubstring(label.toLowerCase(), ["under", "less", "lower", "up to", "below"]) 
            && !isNaN(quantify(label))
            && array_of_values.filter(function (x) { return !isNaN(x); }).length == 1 
            && (value_i == 0 || value_i  == 1 || isNaN(quantify(all_labels[value_i -1])))) //Lower limit rule
            midpoint = quantify(label) / 2.0;
        else if (containsSubstring(label.toLowerCase(), ["over", "more", "greater", "\+", "plus", "above"]) 
            && !isNaN(quantify(label)) 
            && (value_i == num_vals -1 || value_i == num_vals - 2 || isNaN(quantify(all_labels[value_i + 1])))) //Upper limit rule
            midpoint = quantify(label) + last_range / 2.0;
        else 
            var midpoint = computeMidpoint(array_of_values);
        if (isNumber(midpoint)) { 
            midpoints.push({
                sourceValue: source_value,
                inferredValue: midpoint,
                genuineMidPoint: array_of_values.length == 2});
            if (array_of_values.length == 2)
                last_range = Math.abs(array_of_values[1] - array_of_values[0]);
        }
    }
    return midpoints;
}

// If the string has open brackets of any kind, eg (, [, [[, {, etc
// then return what comes between the first open bracket and first
// closed bracket, or between the first open bracket and the end
// of the string (in case the closed brackets got truncated)
function extractStringsFromBrackets(labels) {
    var br = /[\[\(\{]+[^\]\)\}]*[\]\)\}]*/;
    return labels.map(function (label) {
        var matches = label.match(br);
        if (matches != null) {
            return matches[0].replace(/^[\[\(\{]+/, "")
                             .replace(/[\]\)\}]+$/, ""); 
        } else
            return "";
    });
}
 
// counts up the number of genuine midpoints in an array of midpoints
// generated by computeMidpointsForQuestion
function numberOfMidpoints(midpoints) {
    var counter = 0;
    for (var midpoint_i in midpoints) {
        var midpoint = midpoints[midpoint_i];
        if (midpoint.genuineMidPoint)
            counter++;
    }
    return counter;
}
 
 
// Looks up the source value and returns the inferred midpoint value
function getInferredValueFromMidpoints(midpoints, source_value){
    for (var midpoint_i in midpoints) {
        var midpoint = midpoints[midpoint_i];
        if (midpoint.sourceValue == source_value)
            return midpoint.inferredValue;
    }
    return NaN;
}
 
// checks to see if the highest value is a don't know
function highestLabelIsDK(question) {
    var highest_label = highestLabel(question);
    return isDontKnow(highest_label);
}
 
// Identifies 'Other' values
function otherSourceValues(question) {
    var others_values = [];
    for (var value_i = question.uniqueValues.length - 1; value_i >= 0; value_i --) {//loops through the value labels
        var source_value = question.uniqueValues[value_i];
        var label = question.valueAttributes.getLabel(source_value).toLowerCase();
        if (isOther(label))
            others_values.push(source_value); //saving the values of other labels
    }
    return others_values;
}
 
// relables others
function tidyOtherLabel(question, other_questions, possible_other_questions){
    var other_values = otherSourceValues(question);
    if (other_values.length > 0) {
        if (other_values.length == 1) {
            question.valueAttributes.setLabel(other_values[0],"Other");
            other_questions.push(question);
        } else {
            possible_other_questions.push(question);
        }
    }
}
 
 
// This function checks a set of labels and returns true if:
//
// - The first two labels are both numbers
// - The last two labels are not both quantifiable to numbers
//
// It is best to supply only those labels which do not correspond to missing values
function numericLabelsWithUnquantifiableTopScalePoints(labels) {
    var num_labels = labels.length;
    return isNumber(labels[0]) && isNumber(labels[1]) && (isNaN(quantify(labels[num_labels-2])) || isNaN(quantify(labels[num_labels-1])));
}
 
 
// This function checks a set of labels and returns true if:
//
// - All of the labels are quantifiable (ie '5' or 'five');
// - The last label is open-ended, like '5 or more' or '8 +'
function numericLabelsWithOpenEndedTopPoints(labels) {
    var num_labels = labels.length;
    var quantifiable_labels = labels.filter(function (x) { return !isNaN(quantify(x))});
    var last_label_open = openEndedTopLabel(labels[num_labels-1]);
    return quantifiable_labels.length == num_labels && last_label_open;
}
 
 
 
// This function returns true if the labels are integers that increase by 1 for each
// subsequent label, and if there are 5, 7, 9, 10, or 11 points in the scale.
function validNumericScale(labels) {
    var num_labels = labels.length;
    if (labels[0] != 1 && labels[0] != 0)
        return false;
 
    var valid_lengths = [5, 7, 9, 10, 11];
    if (valid_lengths.indexOf(num_labels) == -1)
        return false;
 
    if (labels.filter(isNumber).length != num_labels)
        return false;
 
    var valid_scale = true;
    // Testing to see if all labels have the same distance from their position 
    var first_label_minus = labels[0];
    for (var j = 1; j < num_labels; j++)
        if (labels[j] - j != first_label_minus)
            return false;    
 
    return valid_scale;
}
 
 
 
// This function checks the selected questions for 'Don't Know' categories.
// If any are found, the user is prompted to continue to to cancel
// The function returns an object containing an array of the questions with
// 'Don't know' categories and a flag telling whether the user cancelled or not
function checkQuestionsForDK(selected_questions) {
    var num_selected_questions = selected_questions.length;
    var user_cancelled = false;
    // Check selected questions for 'Dont Know' options
    var questions_containing_dk = [];
    for (var j = 0; j < num_selected_questions; j++) {
        current_question = selected_questions[j];
        var dk_labels = nonMissingValueLabels(current_question).filter(isDontKnow);
        if (dk_labels.length > 0)
            questions_containing_dk.push(current_question);
    }
 
    // Warn user that their questions contain 'Dont Know' options
    if (questions_containing_dk.length > 0) {
        var dk_message = "Some of the questions that you have selected contain 'Don't Know' categories (for example: ";
        for (var j = 0; j < Math.min(questions_containing_dk.length, 2); j++) {
            if (j > 0) {
                dk_message += ", "
            }
            dk_message += questions_containing_dk[j].name;
        }
        dk_message += ").\r\nPress Ok if you wish to proceed, otherwise press cancel and use the QScript 'Modifying Rows and Columns "
            + "- Remove Don't Know Categories From Selected Questions' to remove these catgeories.";
        if(!confirm(dk_message)) {
            selected_questions = [];
            user_cancelled = true;
        }
    }
 
    return {questionsContainingDK: questions_containing_dk, userCancelled: user_cancelled}
}

// This function returns true when the supplied array contains only ineger values,
// and each set of values is either increasing or deacreasing and each subsequent value
// increases or deacreses by 1 compared with the previous value..
function orderedIntegerSequenceWithNoGaps(array) {
    var first = parseInt(array[0]);
    if (first != array[0])
        return false;
    if (array.length < 2)
        return true;
    var second = parseInt(array[1]);
    if (second != array[1])
        return false;
    // Check the elements incread or deacrease by '1' 
    if (second > first) {
        for (var j = 1; j < array.length; j++) {
            var current_as_integer = parseInt(array[j]);
            if (current_as_integer != array[j])
                return false;
            if (current_as_integer != first + j)
                return false;
        }
    } else {
        for (var j = 1; j < array.length; j++) {
            var current_as_integer = parseInt(array[j]);
            if (current_as_integer != array[j])
                return false;
            if (current_as_integer != first - j)
                return false;
        }
    }
    return true;
}


function getDKQuestionsWithUserEnteredLabels(question_types, extra_dk_strings) {
    // Ask the user to choose which data files to use
    let selected_datafiles = dataFileSelection();
    let selected_questions = getAllQuestionsByTypes(selected_datafiles, question_types);
    return getDKQuestionsFromSelection(selected_questions, extra_dk_strings);

}

function getDKQuestionsFromSelection(selected_questions, extra_dk_strings) {
    relevant_questions = selected_questions.filter(function (q) {
        if (questionHasNonMissingDontKnowOption(q))
            return true;
 
        var value_labels = nonMissingValueLabels(q).map(function (s) { return s.toLowerCase(); });
        let contains_dk_string = value_labels.some(function (label) {
            return containsSubstring(label, extra_dk_strings);
        });
        return contains_dk_string;
    });
    return relevant_questions;    
}

// Recode the values of data based on labels.
// Looks for ranges of values and recodes midpoints of those ranges,
// or looks for single values and recodes to those values.
// If add_new_vars is true, new variables numeric variables are created
// and then recoded. If false, the data is recoded without copying.
function midpointAndQuantificationRecoding(add_new_vars) {
    includeWeb('QScript Utility Functions');
    includeWeb('QScript Selection Functions');
    includeWeb('QScript Value Attributes Functions');
    includeWeb('QScript Functions to Generate Outputs');
    includeWeb('QScript Data Reduction Functions');
    includeWeb('QScript Functions for Combining Categories');
    includeWeb('QScript Functions for Processing Arrays');
 
    function labelsReferToUnits(label_array, max_number) {
        return label_array.filter(labelRefersToUnits).length > max_number;
    }
 
    function labelRefersToUnits(label) {
        return containsSubstring(label, ["metres", "litres", "grams", "pound", "stone"])
    }
 
    function labelsReferToRates(label_array, max_number){
        return label_array.filter(labelRefersToRates).length > max_number;
    }
 
    function labelRefersToRates(label) {
        return containsSubstring(label, ["per", "each", "every"]);
    }
 
 
    // Recode the question as appropriate.
    // Return an object containing:
    // - question - the original question
    // - incorrect - if we think the recoding is likely to be incorrect because the labels are ambiguous
    // - midpoints - flag is true if we used midpoint coding
    // - quantified - flag is true if we did not use midpoint coding but instead quantified the labels
    function recodeQuestionByMidpointsAndQuantification(question) {
 
        let coded_by_midpoints = false;
        let could_be_incorrect = false;
        let coded_by_quantification = false;
 
        let labels = valueLabels(question);
 
 
        let current_values = question.uniqueValues;
 
        let midpoints = computeMidpointsInQuestion(question, 
            [" to ", 
             /[\u002D\u058A\u05BE\u1400\u1806\u2010-\u2015\u2053\u207B\u208B\u2212\u2E17\u2E1A\u2E3A\u2E3B\u301C\u3030\u30A0\uFE31\uFE32\uFE58\uFE63\uFF0D]/]);
        if (numberOfMidpoints(midpoints) >= 3 ) {
            // Try to recode with midpoints first
            if (labelsReferToTime(labels, 1) || labelsReferToRates(labels, 1) || labelsReferToUnits(labels, 1))
                could_be_incorrect = true;
 
            coded_by_midpoints = true;
 
            current_values.forEach(function (value) {
                setValueForVariablesInQuestion(question, value, getInferredValueFromMidpoints(midpoints, value));
            });
        } else {
            // Code by quantification
 
            // If more than half of the labels contain strings in brackets eg (...)
            // Then grab the string from inside the brackets rather than looking at
            // the full labels/
            let labels_in_brackets = extractStringsFromBrackets(labels);
            let num_in_brackets = labels_in_brackets.filter(function (label) { return label.length > 0; }).length; 
            let quantified_labels;
            if (num_in_brackets > labels.length / 2)
                quantified_labels = labels_in_brackets.map(quantify);
            else
                quantified_labels = labels.map(quantify);
 
            if (labelsReferToTime(labels, 1) || labelsReferToRates(labels, 1) || labelsReferToUnits(labels, 1))
                could_be_incorrect = true;   
 
            coded_by_quantification = true;
 
            for (var k = 0; k < current_values.length; k++)
                setValueForVariablesInQuestion(question, current_values[k], quantified_labels[k]);
        }
 
        return { question: question, incorrect: could_be_incorrect, midpoints: coded_by_midpoints, quantified: coded_by_quantification };
    }

    function makeNumericCopy(question) {
        let new_q = question.duplicate(preventDuplicateQuestionName(question.dataFile, question.name + " xRECODED"));
        if (question.questionType.indexOf("Number") == -1)    
            new_q.questionType = question.questionType == "Pick One" ? "Number" : "Number - Multi";
        return new_q;
    }

    // For Number and Number - Multi, do the values match the labels?
    function valuesAndLabelsMatch(q) {
        let value_attributes = q.valueAttributes;
        let unique_values = q.uniqueValues;
        for (let j = 0; j < unique_values.length; j++) {
            let val = value_attributes.getValue(unique_values[j])
            if (!isNaN(val) && val.toString() != value_attributes.getLabel(unique_values[j])) {
                return false;
            }
        }
        return true;
    }

    const is_displayr = inDisplayr(); 
    const allowed_types = ["Nominal", "Ordinal", "Numeric", "Nominal - Multi", 
        "Ordinal - Multi", "Numeric - Multi"];
    let selected_questions;
    if (is_displayr) {
        let user_selections = getAllUserSelections();
        selected_questions = user_selections.selected_questions;
        if (!add_new_vars) // When modifying in place, also accept question selected in table
            selected_questions = selected_questions.concat(user_selections.questions_in_rows_of_selected_tables);
        selected_questions = selected_questions.filter(function (q) { return allowed_types.indexOf(q.variableSetStructure) > -1});
        if (selected_questions.length == 0) {
            log(correctTerminology("No appropriate data selected. Select one or more " 
                + printTypesString(allowed_types) + " variable sets " 
                + " and run this option again"));
        }
    } else
        selected_questions = selectInputQuestions(allowed_types);
        
    if (!selected_questions)
        return false;

    if (!areQuestionsValidAndNonEmpty(selected_questions))
        return false;
 
    // Either recode existing questions if add_new_vars is false.
    // Otherwise make numeric copies of the questions and recode those.
    let recoding_objects;
    if (!add_new_vars) {
        recoding_objects = selected_questions.map(recodeQuestionByMidpointsAndQuantification);
    } else {
        selected_questions = selected_questions.map(makeNumericCopy);
        recoding_objects = selected_questions.map(recodeQuestionByMidpointsAndQuantification);
        recoding_objects.forEach(function (obj) {
            let question = obj.question;
            if (obj.midpoints) {
                question.name = preventDuplicateQuestionName(question.dataFile, 
                    question.name.replace("xRECODED", "RECODED WITH MIDPOINTS" + (obj.incorrect ? " - PLEASE CHECK VALUES" : "")));
            } else if (obj.quantified) {
                question.name = preventDuplicateQuestionName(question.dataFile, 
                    question.name.replace("xRECODED", "RECODED WITH NUMBERS FROM LABELS" + (obj.incorrect ? " - PLEASE CHECK VALUES" : "")));
            }
        });
    }

    // Sort the recoded questions and new questions into arrays
    // based on how they've been recoded.
    let midpoint_questions = [];
    let quantified_questions = [];
    let incorrect_questions = [];
    recoding_objects.forEach(function (obj) {
        if (obj.incorrect)
            incorrect_questions.push(obj.question);
        else if (obj.midpoints)
            midpoint_questions.push(obj.question);
        else
            quantified_questions.push(obj.question);
    });
 
    let log_messages = [];
 
    // Create summary pages/tables and log messages for each type of recoding
    // In Displayr only display a message.
    if (!is_displayr && quantified_questions.length + midpoint_questions.length + incorrect_questions.length > 0) {
        let temporal_group = "Possibly incorrectly-recoded variables";
        let recoded_group = project.report.appendGroup();
        recoded_group.name = "Recoded Variables";
        if (quantified_questions.length > 0) {
            var quant_group_name = "Questions recoded with numbers from labels";
            generateSubgroupOfSummaryTables(quant_group_name, recoded_group, quantified_questions);
            log_messages.push("New questions that have been recoded using numbers from the labels have been added to the folder: '" + quant_group_name + "'.");
        }
        if (midpoint_questions.length > 0) {
            var midpoint_group_name = "Questions recoded with midpoints";
            generateSubgroupOfSummaryTables(midpoint_group_name, recoded_group, midpoint_questions);
            log_messages.push("New questions that have been recoded using midpoints have been added to the folder: '" + midpoint_group_name + "'.");
        } 
        if (incorrect_questions.length > 0) {
            generateSubgroupOfSummaryTables(temporal_group, recoded_group, incorrect_questions);
            log_messages.push("Recoded questions whose labels refer to time periods and other units have been added to the folder: " + temporal_group + "'. These recodings should be checked carefully.")
        }
 
        log_messages.push("Please check the Values of the new questions to ensure that you are happy with the computed values. This is done by right-clicking on the table and selecting Values.");
 
        conditionallyEmptyLog(log_messages.join("\r\n"));
        simpleHTMLReport(log_messages, "Recoded Variables", recoded_group, true, false);
    } else if (is_displayr && incorrect_questions.length > 0) {
        log("Some recoded variable sets have labels which include time periods and other units of measurement, " +
            "and as a result the coding may not be correct. Please check the recoded values by selecting the new " +
            "variables under Data Sets and then clicking Properties > DATA VALUES > Values on the right.\r\n");
        log(recoding_objects.filter(function (obj) { return obj.incorrect; }).map(function (obj) { return obj.question.name; }).join("\r\n")); 
    }
    return true;
}

// Inspect the values, labels, and question name to determine if a 
// question is a likelihood-scale question.  
function isLikelihoodScale(question, require_likely_in_label) {

    var q_type = question.questionType;
    if (q_type != "Pick One" && q_type != "Pick One - Multi")
        return false;
 
    // Get all non-missing labels that do not look like DK options
    var non_missing_labels = nonMissingValueLabels(question);
    non_missing_labels = non_missing_labels.filter(function (label) { return !isDontKnow(label)});
 
    var num_vals = non_missing_labels.length;
    if (num_vals < 10 || num_vals > 12)
        return false;
 
    if (require_likely_in_label) {
        var labels_contain_likely = non_missing_labels.filter(function (x) { return containsSubstring(x.toLowerCase(), ["likely"]); } ).length > 0;
        var name_contains_likely = containsSubstring(question.name.toLowerCase(), ["likely", "likeli"]);
 
        if (!labels_contain_likely && !name_contains_likely)
            return false;
    }
 
    // We ought to be able to quantify all of the labels,
    // with the possible exception of the first and last label.
    var quantified_labels = non_missing_labels.map(quantify);
 
    if (num_vals == 10) {
        for (var j = 1; j < 11; j++)
            if (quantified_labels[j-1] != j && !((j == 1 || j == 10) && isNaN(quantified_labels[j-1])))
                return false;
        return true;
    }
    else if (num_vals == 11) {
        for (var j = 0; j < 11; j++)
            if (quantified_labels[j] != j && !((j == 0 || j == 10) && isNaN(quantified_labels[j])))
                return false;
        return true;
    }
    else
        return false;
}

See also