QScript Functions for Combining Categories

From Q
Jump to: navigation, search

This page that contains functions that are helpful for combining categries, either by creating NETs on tables or by creating new variables.

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

createTopBoxQuestion(question, value_array)

Create a new Pick Any question from the input question. The new question will have the values specified in the value_array selected in the Count This Value column of the Values. The new question will take the same name as the input question with the labels of the categories that have been counted appended to the end of the name.

createBottomBoxQuestion(question, value_array)

Create a new Pick Any question from the input question. The new question will have the values specified in the value_array selected in the Count This Value column of the Values. The new question will take the same name as the input question with the labels of the categories that have been counted appended to the end of the name.

createKBoxNETs(selected_questions, k, bottom, position, name_override)

This function adds top or bottom box NET categories to the data reductions of the questions in the array selected_questions.

The arguments are:

  • selected_questions is the array of questions to change.
  • k is the number of categories to combine. This should be a positive integer.
  • bottom is a boolean flag. If true, then bottom box NETs will be created, else top box NETs will be created.
  • position is a string with the possible values: Beginning, End, Default position. This determines whether the new NET is move to the beginning (top or left) or end (bottom or right) of the data reduction, or if it is left in its initial position.

pickOneMultiToPickAnyFlattenAndMergeByRows(question, mergings, use_source)

Create a new Pick Any question from a Pick One - Multi question such that scale points are merged according to mergings, and the rows of the Pick One - Multi are flattened in the usual sense.

The mergings argument is an array whose elements are objects with properties:

  • name - a string corresponding to the name of the merged category.
  • values - an array containing the source values that are contained in the merged category.

use_source is a boolean flag. When set to true then the new variables will refer to source values of the variables in the input question, and otherwise they will refer to the present values in the input question.

mergePickOneExpression(var_name, values_to_merge, use_source)

Generates a JavaScript expression for merging the values_to_merge from the variable with name var_name. If use_source then source values are used, otherwise the present values are used.

topAndBottomBoxNETCreator(bottom)

This function presents a common user interface for adding Top/Bottom Box NETs to questions. Set bottom to true for Bottom Boxes, and false for Top Boxes. The user can specify the number of categories to combine, the position of the new NET, and the label of the new NET. The determination of highest (or lowest) is done based on the current values of the categories in the data reduction (so recoding affects this).

getTopOrBottomKRecodedCategories(question, k, bottom)

Find the k highest or lowest categories in the question according to the current values. Set bottom to true for the lowest categories, and false for the highest.

createTopOrBottomBoxVariables(k, is_bottom)

Function for creating top and bottom box variables. Provides the user prompts and creates variables.

  • k is the number of categories to combine. If you want the user to enter this number, set k as null.
  • is_bottom: set to true for bottom boxes and false for top boxes

mergeScales(mergings, merge_message)

Merge the points in a scale.

  • mergings is an array where each element has properties label, a string which determines the new label of the merged category, and values, an array of integers specifying the source values of the categories to be merged.
  • merge_message is a string describing the mergings that will be done.

mergeCategoriesInManyQuestions(selected_questions, nets)

tabulateMergedQuestions(new_net_questions, invalid_questions, merge_message)

applyCustomMerges(create_new_pick_any_question)

Source Code

includeWeb('QScript Utility Functions');
includeWeb('QScript Data Reduction Functions');
includeWeb('QScript Value Attributes Functions');

// TOP/BOTTOM BOXES



// Create a Top Box question from the input question, using the values in value_array as
// the top values to be selected. 
function createTopBoxQuestion(question, value_array) {
    checkQuestionType(question, ["Pick One", "Pick One - Multi"]);
    var unique_values = question.uniqueValues;
    var n_uniques = unique_values.length;
    var combined_labels = getLabelsForValues(question, value_array);
    var missing_values_indicators = valueAttributesMissingValueIndicatorArray(question);
    var name = question.name;
    var new_name = name + " - " + combined_labels.join(" + ");
    var new_question = question.duplicate(preventDuplicateQuestionName(question.dataFile, new_name));
    new_question.questionType = "Pick Any";
    for (var j = 0; j < n_uniques; j++) {
        setIsMissingDataForVariablesInQuestion(new_question, unique_values[j], missing_values_indicators[j] == 1)
        setCountThisValueForVariablesInQuestion(new_question, unique_values[j], value_array.indexOf(unique_values[j]) > -1)
    }
    return new_question;
}
 
 
// Create a Bottom Box question from the input question, using the values in value_array as
// the Bottom values to be selected. 
function createBottomBoxQuestion(question, value_array) {
    checkQuestionType(question, ["Pick One", "Pick One - Multi"]);
    var unique_values = question.uniqueValues;
    var n_uniques = unique_values.length;
    var combined_labels = getLabelsForValues(question, value_array);
    var missing_values_indicators = valueAttributesMissingValueIndicatorArray(question);
    var name = question.name;
    var new_name = name + " - " + combined_labels.join(" + ");
    var new_question = question.duplicate(preventDuplicateQuestionName(question.dataFile, new_name));
    new_question.questionType = "Pick Any";
    for (var j = 0; j < n_uniques; j++) {
        setIsMissingDataForVariablesInQuestion(new_question, unique_values[j], missing_values_indicators[j] == 1)
        setCountThisValueForVariablesInQuestion(new_question, unique_values[j], value_array.indexOf(unique_values[j]) > -1)
    }
    return new_question;
}

// Creates a top or bottom box NET in each of the questions in the array selected_questions
function createKBoxNETs(selected_questions, k, bottom, position, name_override) {
 
    var num_selected_questions = selected_questions.length;
    // Add NETs to the data reductions as specified
 
 
    var current_question;
    var current_data_reduction;
    var n_values;
    var values_for_net;
    var net_labels;
    var new_net_questions = [];
    var check_questions = [];
    var invalid_questions = [];
    var destination_label;
    var net_name;
 
 
    // Loop through selected questions, creating NETs where possible
    for (var j = 0; j < num_selected_questions; j++) {
        current_question = selected_questions[j];
        current_data_reduction = current_question.dataReduction;
        n_values = current_question.uniqueValues.length - numberMissingValues(current_question);
 
        if (bottom)
            values_for_net = getBottomKNonMissingValues(current_question, k);
        else
            values_for_net = getTopKNonMissingValues(current_question, k);
 
 
        net_labels = getLabelsForValues(current_question, values_for_net);
 
        // check that the top/bottom category labels still exist in the data reduction
        if (dataReductionContainsLabels(current_question, net_labels)) {
 
            // Determine the name for the NET
            if (name_override != null)
                net_name = name_override;
            else if (bottom)
                net_name = "Bottom " + k + " NET (" + net_labels.join(', ') + ")";
            else
                net_name = "Top " + k + " NET (" + net_labels.join(', ') + ")";
 
            // Create the NET
            current_data_reduction.createNET(net_labels, net_name);
 
            // Determine where to place the new NET row (or column)
            if (position == "Beginning")
                destination_label = null;
            else {
                var row_labels = current_data_reduction.rowLabels;
                var col_labels = current_data_reduction.columnLabels;
                var net_rows = current_data_reduction.netRows;
                var net_columns = current_data_reduction.netColumns;
                
                if (net_rows) {
                    if (net_rows.length > 0) {
                        if (net_rows[net_rows.length - 1] == row_labels.length - 1)
                            destination_label = row_labels[row_labels.length - 2];
                        else
                            destination_label = row_labels[row_labels.length - 1];
                    } else if (col_labels != null && net_columns.length > 0) {
                        if (net_columns[net_columns.length - 1] == col_labels.length - 1)
                            destination_label = col_labels[col_labels.length - 2];
                        else
                            destination_label = col_labels[col_labels.length - 1];
                    }
                } else {
                    if (row_labels.indexOf(net_name) > -1) {
                        if (row_labels.indexOf('NET') == row_labels.length - 1)
                            destination_label = row_labels[row_labels.length - 2];
                        else
                            destination_label = row_labels[row_labels.length - 1];
                    } else if (col_labels != null && col_labels.indexOf(net_name) > -1) {
                        if (col_labels.indexOf('NET') == col_labels.length - 1)
                            destination_label = col_labels[col_labels.length - 2];
                        else
                            destination_label = col_labels[col_labels.length - 1];
                    }                  
                }
            }
 
            // Move the NET
            if (position != "Default position")
                current_data_reduction.moveAfter(net_name, destination_label);
 
            new_net_questions.push(current_question);
        } else
            invalid_questions.push(current_question);
        if (questions_containing_dk.indexOf(current_question) > -1)
            check_questions.push(current_question);
    }
 
    return {netQuestions: new_net_questions, invalidQuestions: invalid_questions, checkQuestions: check_questions};
}

// Create a new Pick Any question from a Pick One - Multi question such that scale points are
// merged according to 'mergings', and the rows of the Pick One - Multi are flattened in the
// usual sense.
function pickOneMultiToPickAnyFlattenAndMergeByRows(question, mergings, use_source, tidy_variable_labels) {
    if (tidy_variable_labels == null)
        tidy_variable_labels = true;
    checkQuestionType(question, ["Pick One - Multi"]);
    var new_vars = [];
    var q_vars = question.variables;
    var last_var = q_vars[q_vars.length - 1];
    var n_mergings = mergings.length;
 
    // Generate new JavaScript variables for the merged categories
    q_vars.forEach(function (q_var) {
        var cur_var_label = q_var.label;
        var cur_var_name = q_var.name;
        var name_prefix = cur_var_name + '_flat_';
        for (var k = 0; k < n_mergings; k++) {
            var new_expression = mergePickOneExpression(cur_var_name, mergings[k].values, use_source);
            var new_label = cur_var_label + " - " + mergings[k].name;
            var new_var = question.dataFile.newJavaScriptVariable(new_expression, false, preventDuplicateVariableName(question.dataFile, name_prefix + (k + 1)), new_label, last_var);
            new_var.variableType = "Categorical";
            last_var = new_var;
            new_vars.push(new_var);
        }
    });
 
    // setting the question
    var new_q_name = preventDuplicateQuestionName(question.dataFile, question.name + " (flattened)");
    var new_q = question.dataFile.setQuestion(new_q_name, "Pick Any", new_vars);
    // creating the spans and simplifying the labels
    for (var j = 0; j < q_vars.length; j++){
        var labels_to_merge = [];
        for (var k = 0; k < n_mergings; k++)
            labels_to_merge.push(new_vars[k + j * n_mergings].label);
        new_q.dataReduction.span(labels_to_merge, q_vars[j].label);

        if (tidy_variable_labels) {
            // Upate row labels
            for (var k = 0; k < n_mergings; k++)
                new_vars[k + j * n_mergings].label = mergings[k].name;
        }
            
    }
    return new_q;
}
// Generate the expression for merging a set of values 
function mergePickOneExpression(var_name, values_to_merge, use_source) {
    var expression ="Q.IsMissing(" + var_name + ") ? NaN : ";

    for (var j = 0; j < values_to_merge.length; j++) {
        cur_val = values_to_merge[j];
        
        if (j > 0)
            expression += " || ";

        if (use_source) {
            if (isNaN(cur_val))
                expression += "isNaN(Q.Source(" + var_name + "))";
            else
                expression += "Q.Source(" + var_name + ") == " + cur_val;
        } else {
            if (isNaN(cur_val))
                expression += expression += "isNaN(" + var_name + ")";
            else    
                expression += var_name + " == " + cur_val;
        }
    }
    return expression + " ? 1 : 0;";
}

function topAndBottomBoxNETCreator(bottom) {
    includeWeb('QScript Utility Functions');
    includeWeb('QScript Questionnaire Functions');
    includeWeb('QScript Selection Functions');
    includeWeb('QScript Value Attributes Functions');
    includeWeb('QScript Data Reduction Functions');
    includeWeb('QScript Functions for Processing Arrays');
    includeWeb('JavaScript Utilities');
 
    // Creates a top or bottom box NET in each of the questions in the array selected_questions
    function createKBoxNETs(question_objects, k, bottom, position, name_override, cant_modify, remove_dk) {
 
        var new_net_questions = [];
 
        // Loop through selected questions and create NETs
        question_objects.forEach(function (obj) {
            var question = obj.question;
            var current_data_reduction = question.dataReduction;
 
            var net_labels = getTopOrBottomKRecodedCategories(question, k, bottom, { excludeDK: true });
 
            // Check that it is possible to create the NET. If labels are not present
            // then it is not possible to create the NET.
            var keep_going  = true;
            if (net_labels.length == 0) {
                cant_modify.push(question);
                keep_going = false;
            } else {
                net_labels.forEach(function (label) {
                    if (!current_data_reduction.contains(label)) {
                        keep_going = false;
                        cant_modify.push(question);
                    }
                });
            }
 
 
            if (keep_going) {
                // Determine the name for the NET
                var net_name;
                if (name_override != null)
                    net_name = name_override;
                else if (bottom)
                    net_name = "Bottom " + k + " NET (" + net_labels.join(', ') + ")";
                else
                    net_name = "Top " + k + " NET (" + net_labels.join(', ') + ")";
 
                // Create the NET
                current_data_reduction.createNET(net_labels, net_name);
 
                // Move the NET
                var destination_label;
                if (position != "Default position") {
                    // Determine where to place the new NET row (or column)
                    if (position == "Beginning")
                        destination_label = null;
                    else {
                        var row_labels = current_data_reduction.rowLabels;
                        var col_labels = current_data_reduction.columnLabels;
                        var net_rows = current_data_reduction.netRows;
                        var net_columns = current_data_reduction.netColumns;
 
                        if (net_rows) {
                            if (net_rows.length > 0) {
                                if (net_rows[net_rows.length - 1] == row_labels.length - 1)
                                    destination_label = row_labels[row_labels.length - 2];
                                else
                                    destination_label = row_labels[row_labels.length - 1];
                            } else if (col_labels != null && net_columns.length > 0) {
                                if (net_columns[net_columns.length - 1] == col_labels.length - 1)
                                    destination_label = col_labels[col_labels.length - 2];
                                else
                                    destination_label = col_labels[col_labels.length - 1];
                            }
                        } else {
                            if (row_labels.indexOf(net_name) > -1) {
                                if (row_labels.indexOf('NET') == row_labels.length - 1)
                                    destination_label = row_labels[row_labels.length - 2];
                                else
                                    destination_label = row_labels[row_labels.length - 1];
                            } else if (col_labels != null && col_labels.indexOf(net_name) > -1) {
                                if (col_labels.indexOf('NET') == col_labels.length - 1)
                                    destination_label = col_labels[col_labels.length - 2];
                                else
                                    destination_label = col_labels[col_labels.length - 1];
                            }                  
                        }
                    }
 
                    current_data_reduction.moveAfter(net_name, destination_label);
                }
 
                new_net_questions.push(question);
                if (remove_dk && obj.hasDK) {
                    var value_attributes = question.valueAttributes;
                    var unique_values = question.uniqueValues;
                    unique_values.forEach(function (x) {
                        if (isDontKnow(value_attributes.getLabel(x)))
                            value_attributes.setIsMissingData(x, true);
                    });
                } 
            }
        });
 
        return new_net_questions; 
    }
 
 
 
 
 
    // Specify k
    var k = -1;
    while (!isPositiveInteger(k) || k < 2) {
        k = prompt("How many categories do you wish to combine? For example, to combine the " + (bottom ? "bottom" : "top")  + " two categories type '2' and press OK.");
        if (!isPositiveInteger(k) || k < 2)
            alert("k must be an integer greater than 1.");
    }
 
    var web_mode = (!!Q.isOnTheWeb && Q.isOnTheWeb());
    // Ask the user to choose which data files to use
    if (!web_mode){
        var selected_datafiles = dataFileSelection();
 
        var override_name = prompt("Enter a label for the" + (bottom ? " bottom " : " top ") + k + " NET. To keep Q's default label leave this blank.");
        if (!/\S/.test(override_name))
            override_name = null;
 
        var relevant_questions;
        if (askYesNo("Q will now show you a list of questions to choose from. Would you like Q to show only questions that look like scales?"))
            relevant_questions = getAllScaleQuestions(selected_datafiles);
        else
            relevant_questions = getAllQuestionsByTypes(selected_datafiles, ["Pick One", "Pick One - Multi"]);
 
        relevant_questions = relevant_questions.filter(function(q) { return (q.uniqueValues.length - numberOfMissingOrNaNValues(q)) > k; } );
    }

    if (relevant_questions.length == 0) {
        log("No questions available.");
        return false;
    } 
 
    // Generate a list of applicable questions along with the highest and lowest category labels
    var question_label_strings = relevant_questions.map(function (q) {
                                                            highest_and_lowest_label = getHighestAndLowestValueAndLabel(q);
                                                            return truncateStringForSelectionWindow(q.name) 
                                                            + "  (" + highest_and_lowest_label.lowest + " ... " 
                                                            + highest_and_lowest_label.highest + ")";
                                                        });
 
    var selected_indices = selectMany("Please choose which questions you want to add NETs to:\r\n(highest and lowest value labels are shown in brackets)", question_label_strings);
    var selected_questions = getElementsOfArrayBySelectedIndices(relevant_questions, selected_indices);
 
    if (selected_questions.length == 0) {
        log("No questions selected.");
        return false;
    }
 
   // Check selected questions for 'Dont Know' options
    var question_objects = selected_questions.map(function (q) {
        return { question: q,
                 hasDK: nonMissingValueLabels(q).filter(isDontKnow).length > 0 }
    });
 
    var questions_containing_dk = question_objects.filter(function (obj) { return obj.hasDK; })
                                                  .map(function (obj) { return obj.question; });
 
 
 
    var dk_message = "Some of the selected questions contain 'Don't Know' categories. These categories will not be used to create the "
                        + (bottom ? "bottom" : "top") + " " + k + " boxes. Do you also want to remove the 'Don't Know' categories?";
    var remove_dk = false;
    if (question_objects.some(function (obj) { return obj.hasDK; }))
        remove_dk = askYesNo(dk_message);
 
    var position_options = ["Beginning", "End", "Default position"];
    var position = position_options[selectOne("Would you like the NETs to be placed at the beginning (top or left), at the end (bottom or right), or leave them in the default position?", position_options)];
 
    // Create the NETs
    var cant_modify = [];
    var new_questions = createKBoxNETs(question_objects, k, bottom, position, override_name, cant_modify, remove_dk);
 
    // Make a table for each new question
    var new_group_name = "Questions with " + (bottom ? "Bottom " : "Top ") + k + " NETs";
    var new_group;
    if (new_questions.length > 0) {
        new_group = generateGroupOfSummaryTables(new_group_name, new_questions);
        log("Questions with new NET categories have been added to the folder: " + new_group_name);
    } else {
        new_group = project.report.appendGroup()
        new_group.name = new_group_name;
    }
 
    if (cant_modify.length > 0) {
        var group_name = "Could not create NETs";
        log("Some questions could not be modified because the required categories are missing from the table. These have been added to the folder: " + group_name + ". To modify the questions you must first right-click on the table and select Revert.");
        generateSubgroupOfSummaryTables(group_name, new_group, cant_modify);
    }
 
    return true;
}


function getTopOrBottomKRecodedCategories(question, k, bottom, options) {

    // Set default legacy options if no options supplied
    if (!options)
        var options = { excludeDK: false };

    var remove_dk = options.excludeDK;

    var data_reduction = question.dataReduction;
    var value_attributes = question.valueAttributes;
    var values_object = getAllUnderlyingValues(question).filter(function (obj) { return obj.array.length == 1; });
    values_object = values_object.map(function (obj) { return { value: value_attributes.getValue(obj.array[0]), label: obj.label }; });
    values_object = values_object.filter(function (obj) { return !isNaN(obj.value); });
    if (remove_dk)
        values_object = values_object.filter(function (obj) { return !isDontKnow(obj.label); });
    values_object.sort(function (a,b) {
        return bottom ? a.value - b.value : b.value - a.value;
    });
    var k_labels = values_object.slice(0,k).map(function (obj) { return obj.label; });
    return k_labels;
}

// Function for creating top and bottom box variables. Provides
// the user prompts and creates variables.
//
// - k is the number of categories to combine. If you want the
//   user to enter this number, set k as null.
// - is_bottom: set to true for bottom boxes and false for top boxes
function createTopOrBottomBoxVariables(k, is_bottom) {
    includeWeb('QScript Utility Functions');
    includeWeb('QScript Questionnaire 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 Processing Arrays');
    includeWeb('JavaScript Utilities');
 
    var web_mode = (!!Q.isOnTheWeb && Q.isOnTheWeb());
    
    if (!web_mode) {
        var selected_datafiles = dataFileSelection();

        // Prompt the user to specify k if not already specified.
        if (k == null) {
            k = -1;
            while (!isPositiveInteger(k)) {
                k = prompt("How many categories do you wish to combine? For example, to combine the " + (is_bottom ? "bottom" : "top") + " two categories type '2' and press OK.");
                if (!isPositiveInteger(k))
                    alert(k + "is not a valid number of categories to combine.");
            }
        }

        var relevant_questions;
        if (askYesNo("Q will now show you a list of questions to choose from. Would you like Q to show only questions that look like scales?"))
            relevant_questions = getAllScaleQuestions(selected_datafiles);
        else 
            relevant_questions = getAllQuestionsByTypes(selected_datafiles, ["Pick One", "Pick One - Multi"]);
    }else {
	    var allowed_types = ["Nominal", "Nominal - Multi"];
    	var selected_questions = project.report.selectedQuestions();
	    var sorted_selection = splitArrayIntoApplicableAndNotApplicable(selected_questions, function (q) { return allowed_types.indexOf(q.variableSetStructure) != -1 && !q.isBanner; });
    	var relevant_questions = sorted_selection.applicable;

    	relevant_questions = relevant_questions.slice(0,1); // Only use the first selected question if multiple selected
	    var not_applicable_questions = sorted_selection.notApplicable;	
	    if (relevant_questions == null){
	        log("You must select a " + allowed_types.join(" or ") + " question from Data Sets to use this QScript.");
            return false;
	    }else
	        var selected_datafiles = relevant_questions.dataFile;
    }

    relevant_questions = relevant_questions.filter(function(q) { 
        var unique_values = q.uniqueValues;
        var value_attributes = q.valueAttributes;
        var values_to_keep = unique_values.filter(function (x) {
	    return !isNaN(x) && !value_attributes.getIsMissingData(x) && !isDontKnow(value_attributes.getLabel(x));})
        return values_to_keep.length > k; 
    } );
    
    if (relevant_questions.length == 0) {
        log("No appropriate questions found.");
        return false;
    }
 
    question_label_strings = relevant_questions.map(function (q) {
        var highest_and_lowest_label = getHighestAndLowestValueAndLabel(q);
        return truncateStringForSelectionWindow(q.name) + "  (" 
                + highest_and_lowest_label.lowest + " ... " 
                + highest_and_lowest_label.highest + ")";
    })
 
    if (!web_mode){
        var selected_indices = selectMany("Please choose which questions you want to create new variables for:\r\n(highest and lowest value labels are shown in brackets)", question_label_strings);
        selected_questions = getElementsOfArrayBySelectedIndices(relevant_questions, selected_indices);
 
        if (selected_questions.length == 0) {
            log("No questions selected.");
            return false;
        }
    }else
        selected_questions = relevant_questions;
    
    // Check selected questions for 'Dont Know' options
    var question_objects = selected_questions.map(function (q) {
        return { question: q,
                 hasDK: nonMissingValueLabels(q).filter(isDontKnow).length > 0 }
    });

    var questions_containing_dk = question_objects.filter(function (obj) { return obj.hasDK; })
                                                  .map(function (obj) { return obj.question; });



    var dk_message = "Some of the selected questions contain 'Don't Know' categories. These categories will not be used to create the "
                        + (is_bottom ? "bottom" : "top") + " " + k + " boxes. Do you want to set the 'Don't Know'"
                        + " categories as Missing Data (remove them from the base sample)?";
    var remove_dk = false;
    if (question_objects.some(function (obj) { return obj.hasDK; }))
        remove_dk = askYesNo(dk_message);


 
    // Generate a new Pick Any question for each input question with the highest or lowest
    // specified non-missing categories selected.
    var check_questions = [];
    var new_questions = question_objects.map(function (obj) {
        var q = obj.question;
        var new_question;
        if (is_bottom) {
            var bottom_values = getTopOrBottomKNonMissingValues(q, k, true, {excludeDK: true});
            new_question = createBottomBoxQuestion(q, bottom_values);
        } else {
            var top_values = getTopOrBottomKNonMissingValues(q, k, false, {excludeDK: true});
            new_question = createTopBoxQuestion(q, top_values);
        }
        // Remove Don't Know options if user has requested
        if (remove_dk && obj.hasDK) {
            var value_attributes = new_question.valueAttributes;
            var unique_values = new_question.uniqueValues;
            unique_values.forEach(function (x) {
                if (isDontKnow(value_attributes.getLabel(x)))
                    value_attributes.setIsMissingData(x, true);
            })
        }
        return new_question;
    });
 
    var new_group_name = (is_bottom ? "Bottom " : "Top ") + k + " category variables";
    var new_group = generateGroupOfSummaryTables(new_group_name, new_questions);
    if (!web_mode) {
        conditionallyEmptyLog("New variables have been added to your report in the folder called: " + (is_bottom ? "Bottom " : "Top ") + k + " category variables");
    }else
        log(makeWordList(new_questions.map(function(q) { return q.name; } )) + "Summary tables have been added to the bottom of your document.");
    return true;
}

function mergeScales(mergings, merge_message) { 
    var web_mode = (!!Q.isOnTheWeb && Q.isOnTheWeb());
    var all_scale_points = [];
    mergings.forEach(function (obj) {
        all_scale_points = all_scale_points.concat(obj.values);
    });
 
 
    // Ask the user to choose which data files to use
    var selected_datafiles = dataFileSelection();
    var relevant_questions;
    if (askYesNo("Q will now show you a list of questions to choose from. Would you like Q to show only questions that look like scales?"))
        relevant_questions = getAllScaleQuestions(selected_datafiles);
    else
        relevant_questions = getAllQuestionsByTypes(selected_datafiles, ["Pick One", "Pick One - Multi"]);
 
    relevant_questions = relevant_questions.filter(function(q) { 
        var unique_values = q.uniqueValues;
        var value_attributes = q.valueAttributes;
        var values_to_keep = unique_values.filter(function (x) { return !isNaN(x) && !value_attributes.getIsMissingData(x) && !isDontKnow(value_attributes.getLabel(x));})
        return values_to_keep.length == all_scale_points.length; 
    } );
 
    if (relevant_questions.length == 0) {
        log("Could not find any questions to use.");
        return false;
    }
 
    // Generate a list of applicable questions along with the highest and lowest category labels
    var num_questions = relevant_questions.length;
    var question_label_strings = [];
    var highest_and_lowest_label;
    for (var j = 0; j < num_questions; j++) {
        highest_and_lowest_label = getHighestAndLowestValueAndLabel(relevant_questions[j]);
        question_label_strings.push(truncateStringForSelectionWindow(relevant_questions[j].name) + "  (" 
            + highest_and_lowest_label.lowest + " ... " + highest_and_lowest_label.highest + ")");
    }
 
    // Prompt the user to select the questions that they want to use
    var selected_indices = selectMany("Please select the questions in which the categories are to be merged " + merge_message + ": \r\n(highest and lowest value labels are shown in brackets)", question_label_strings);
    var selected_questions = getElementsOfArrayBySelectedIndices(relevant_questions, selected_indices);
 
    if (selected_questions.length == 0) {
        log("No questions selected.");
        return false;
    }
 
    // Check selected questions for 'Dont Know' options
    var question_objects = selected_questions.map(function (q) {
        return { question: q,
                 hasDK: nonMissingValueLabels(q).filter(isDontKnow).length > 0 }
    });
 
    var questions_containing_dk = question_objects.filter(function (obj) { return obj.hasDK; })
                                                  .map(function (obj) { return obj.question; });
 
    // Prompt and remove don't know options
    if (questions_containing_dk.length > 0) {
        if (!web_mode){
            var keep_going = confirm("Some questions contain 'Don't Know' categories. These categories will be removed from the questions before merging.");
            if (!keep_going)
                return false;
        }
        questions_containing_dk.forEach(function (q) {
            var value_attributes = q.valueAttributes;
            var unique_values = q.uniqueValues;
            unique_values.forEach(function (x) {
                if (isDontKnow(value_attributes.getLabel(x)))
                    value_attributes.setIsMissingData(x, true);
            });
        });
    }
 
    // Create the NETs
    var box_obj = mergeCategoriesInManyQuestions(selected_questions, mergings);
  
    tabulateMergedQuestions(box_obj.netQuestions, box_obj.invalidQuestions, merge_message);
 
    return true;
}


// Creates a NET in each of the questions in the array selected_questions
function mergeCategoriesInManyQuestions(selected_questions, nets) {

    var n_nets = nets.length;
    // Add NETs to the data reductions as specified

    var current_question;
    var current_data_reduction;
    var net_labels;
    var new_net_questions = [];
    var invalid_questions = [];
    var destination_label;

    // Loop through selected questions, creating NETs where possible
    selected_questions.forEach(function (current_question) {
        current_data_reduction = current_question.dataReduction;
        var invalid = false;
        nets.forEach(function (net) {
            if (!invalid) {
                var net_name = net.name;
                var values_for_net = net.values;
                net_labels = getLabelsForValues(current_question, values_for_net);

                // check that the top/bottom category labels still exist in the data reduction
                if (dataReductionContainsLabels(current_question, net_labels)) {
                    // Merge
                    if (values_for_net.length > 1)
                        current_data_reduction.merge(net_labels, net_name);
                    else
                        current_data_reduction.rename(net_labels[0], net_name);

                } else {
                    invalid = true; // Flag to put this question in the list of invalids later.
                }
            }
        });

        if (invalid)
            invalid_questions.push(current_question);
        else
            new_net_questions.push(current_question);
    });


    return {netQuestions: new_net_questions, invalidQuestions: invalid_questions};
}


function tabulateMergedQuestions(new_net_questions, invalid_questions, merge_message) {
    // Make a table for each new question
 
    var new_group;
    var top_group_name = "Merged Questions " + merge_message;
    if (new_net_questions.length > 0) {
        new_group = generateGroupOfSummaryTables(top_group_name, new_net_questions);
        conditionallyEmptyLog("Questions with merged scales " + merge_message+ " have been added to the folder: " + top_group_name);
    } else {
        new_group = project.report.appendGroup();
        new_group.name = top_group_name;
    }
 
    if (invalid_questions.length > 0) {
        var invalid_group_name = "Questions with problematic merges";
        generateSubgroupOfSummaryTables(invalid_group_name, new_group, invalid_questions);
        log("The categories of some questions were not able to be merged. This is most likely due to labels being changed on the table. These questions are shown in the folder: " + invalid_group_name);
    }
}

function applyCustomMerges(create_new_pick_any_question) {
 
    includeWeb('QScript Utility Functions');
    includeWeb('QScript Questionnaire 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 Processing Arrays');
    includeWeb('JavaScript Utilities');
    // Interpret comma-separated values entered by user
    function interpretValuesString(string) {
        var split_vals = string.split(",");
        split_vals = split_vals.map(function (x) { return parseFloat(x); });
        return split_vals;
    }
 
    // Format information about entered merging for message boxes
    function formatMerging(obj) {
        return obj.name + ": [" + obj.values.join(", ") + "]";
    } 
 
    // Return all unique values which are not NaN and have not been set as missing
    function getNonMissingValues(question) {
        var value_attributes = question.valueAttributes;
        var non_missing_values = question.uniqueValues.filter(function (x) {
                                                                    return !isNaN(x) && !value_attributes.getIsMissingData(x); 
                                                                });
        return non_missing_values;
    }
 
    // Return all values whose labels look like "Don't Know" options
    function getDKValues(question) {
        var first_value_attributes = question.valueAttributes;
        return question.uniqueValues.filter(function (x) { return !isNaN(x) && !first_value_attributes.getIsMissingData(x) && isDontKnow(first_value_attributes.getLabel(x)); });    
    }
 
    var selected_datafiles = dataFileSelection();
    var relevant_questions = getAllQuestionsByTypes(selected_datafiles, ["Pick One", "Pick One - Multi"]);

    // Count scale points for each question so we can offer the user
    // the choice of which number of scale points to apply to.
    var scale_object = relevant_questions.map(function (question) {
        var non_missing_values = getNonMissingValues(question);
        return { question: question, points: non_missing_values.length };
    });
    scale_object = scale_object.filter(function (obj) { return obj.points > 2; });

    var num_scale_points = uniqueElementsInArray(scale_object.map(function (obj) { return obj.points; }));
    num_scale_points.sort(function (a,b) { return a-b; });

    var explanation_message = "This QScript allows you to merge categories in many questions at once.\r\n\r\n"
                             +"You first need to specify how many categories are contained in the questions that you want to modify.\r\n\r\n"
                             +"For example, if you would like to modify questions with 5 categories, you would choose 5 in the list below.\r\n\r\n"
                             +"Scales with these numbers of categories have been identified in your data:";

    if (create_new_pick_any_question) {
        explanation_message = "This QScript allows you to create new variables from the merged categories of many questions at once.\r\n\r\n"
                             +"You first need to specify how many categories are contained the questions you want to work with.\r\n\r\n"
                             +"For example, if you want to create new variables by merging categories from questions which have 5 categories, you would choose 5 in the list below.\r\n\r\n"
                             +"Scales with these numbers of categories have been identified in your data:";

    }

    var selected_num_points = num_scale_points[selectOne(explanation_message, num_scale_points)];

    var relevant_questions = scale_object.filter(function (obj) { return obj.points == selected_num_points}).map(function (obj) { return obj.question});

    var question_label_strings = relevant_questions.map(function (q) {
        var highest_and_lowest_label = getHighestAndLowestValueAndLabel(q);
        return truncateStringForSelectionWindow(q.name) + "  (" 
               + highest_and_lowest_label.lowest + " ... " 
               + highest_and_lowest_label.highest + ")";
    });

    var selected_indices = selectMany("The following questions have " + selected_num_points + " categories. Choose the questions whose categories you want to merge:\r\n(highest and lowest value labels are shown in brackets)", question_label_strings);
    var selected_questions = getElementsOfArrayBySelectedIndices(relevant_questions, selected_indices);

    // check selected questions for consistency

    //return false;

    if (selected_questions.length == 0) {
        log("No question selected.")
        return false;
    }
 
    var first_question = selected_questions[0];
 
    var first_dk_values = getDKValues(first_question);    
 
    // Determine questions which match
    var first_nm_values = getNonMissingValues(first_question);

    var values_inconsistent = selected_questions.some(function (question) {
        if (question.equals(first_question))
            return false;
        var current_values = getNonMissingValues(question);
        if (current_values.some(function (x) { return first_nm_values.indexOf(x) == -1; } ) )
            return true;
        else
            return false;
    });

    if (values_inconsistent) {
        log("The selected questions have different sets of source values. Their categories can't be merged consistently.");
        return false;
    }

    var dk_inconsistent = selected_questions.some(function (question) {
        var dk_values = getDKValues(question);
        if (dk_values.length != first_dk_values.length)
            return true;
        else
            return false;
    })

    if (dk_inconsistent) {
        log("Some of the selected questions appear to have 'Don't know' options while others do not. Their categories can't be merged consistently.");
        return false;
    }

 
    // Check DK
 
    if (first_dk_values.length > 0) {
        var remove_dks  = askYesNo("The selected questions contain 'Don't Know' options. Would you like to remove them (i.e. set them as Missing Data) before continuing?");
        if (remove_dks) {
            selected_questions.forEach(function (question) {
                var value_attributes = question.valueAttributes;
                first_dk_values.forEach(function (x) {
                    value_attributes.setIsMissingData(x, true);
                });
            });
            first_nm_values = getNonMissingValues(first_question);
        }
    }
 
    // Cycle through prompts for mergings
    var remaining_values = first_nm_values;
    var mergings = [];
 
    var current_merging_text = "";
    var values_prompt_text = "Enter the values to merge. Separate multiple values with commas (e.g. 1, 2, 3, 4). If you have finished specifying categories to be merged, just click OK.";
    var number_regex = /^((-)?[0-9]+(\.[0-9]+)?)?(,((-)?[0-9]+(\.[0-9]+)?))*$/;
    while (remaining_values.length > 0) {
        if (mergings.length > 0) {
            current_merging_text = "These mergings have been specified so far:\r\n"
                            + mergings.map(formatMerging).join("\r\n") + "\r\n\r\n";
        }
        var remaining_values_text = "The remaining values to be merged are: " + remaining_values.join(", ") + "\r\n\r\n";
 
        var entered_vals = prompt(current_merging_text + remaining_values_text + values_prompt_text);
        if (entered_vals.length == 0)
            break;
        var interpreted_vals = interpretValuesString(entered_vals);
        while (!number_regex.test(entered_vals) || interpreted_vals.some(isNaN) || interpreted_vals.length == 0) {
            alert(entered_vals + " is not a valid entry.\r\n\r\nEntered values must be numbers that are seprated with commas (e.g. 1,2,3).");
            entered_vals = prompt(current_merging_text + remaining_values_text + values_prompt_text);
            var interpreted_vals = interpretValuesString(entered_vals);
        }
 
        // CHECK ENTERED VALUES
 
        remaining_values = difference(remaining_values, interpreted_vals);
 
        var label_prompt_text = "Enter a label for the merged category for values (" + interpreted_vals.join(", ") + ")";
        var entered_label = prompt(current_merging_text + label_prompt_text);
 
        mergings.push({ name: entered_label, values: interpreted_vals });
    }
 
    // No Duplicate Codes Allowed
    var all_values = [];
    mergings.map(function (obj) {return obj.values; }).forEach(function (a) { all_values = all_values.concat(a); });
    if (arrayHasDuplicateElements(all_values)) {
        log("Some of the entered values are repeated - each value must appear in a single merged category.");
        return false;
    }
 
    // Label to use when presenting tables with merged categories 
    var merge_message = "(" + mergings.map(function (obj) { return obj.name; }).join(", ") + ")"
 
    // Perform mergings
    if (create_new_pick_any_question) {
        var merge_object;
        var net_questions = [];
        selected_questions.forEach(function (question) {
            mergings.forEach(function (obj) {
                var new_name = preventDuplicateQuestionName(question.dataFile, question.name + " " + obj.name);
                var new_question = question.duplicate(new_name);
                new_question.questionType = "Pick Any";
                new_question.uniqueValues.forEach(function (x) {
                    if (obj.values.indexOf(x) > -1)
                        setCountThisValueForVariablesInQuestion(new_question, x, true);
                    else
                        setCountThisValueForVariablesInQuestion(new_question, x, false);
                })
                net_questions.push(new_question);
            });
        });
        tabulateMergedQuestions(net_questions, [], merge_message);
    } else {
        var merge_object = mergeCategoriesInManyQuestions(selected_questions, mergings);
        tabulateMergedQuestions(merge_object.netQuestions, merge_object.invalidQuestions, merge_message);
    } 
 
 
    return true;
}

See also