Filter - Filter One Question by Another

From Q
Jump to navigation Jump to search

Create a new version of a variable set, in which each variable has been filtered by a category from another variable set, or, with a single filter applied to all variables in the set

This QScript creates a new copy of a question, in which each variable has been filtered by a category from another question, or, with a single filter applied to all variables in the question.

Examples

You can work through the examples of the QScript below yourself in Q using this example data set.

Filtering a question using a filter

The table on the left shows a SUMMARY table. The table in the middle shows a table of a new question created using this QScript, where the an existing filter called Female has been used to create a new variable (in which cases that are not female have missing values). The table to the right shows a table of the original question, filtered in the usual way (i.e., by creating and applying a filter directly to the table); it gives the same result as applying this QScript.

FilteringQuestion.png

Filtering a question by each of the categories in another

The first table shows a Pick One - Multi question. The table underneath shows this same table, but with each category split by different age categories.

NestingPickOne.png

Filtering each variable of a question

The table in the top left shows a SUMMARY of Preferred Cola with the sample sizes of each of the cola brands. The table on the top right shows a SUMMARY of a Pick Any - Grid question, where the Base n for each cell is the total sample of 327 respondents. The table at the bottom shows the result when the grid is filtered by Preferred Cola - the cells of the table are now based only on respondents who prefer the cola brand for that row.

FilterMatchColaBrands1.PNG

Technical details

You will be prompted to select:

  1. The question that you want to filter.
  2. The question whose categories you want to use as filters.
  3. Whether you want to apply a single filter to each variable in the selected question, or to expand the selected question by making a new copy for each filter category.

When you choose to apply a single filter to each variable in the selected question, you will be given the option to choose the filter to apply to each variable manually, or to let Q try to match up the variables with the filters automatically. The automatic matching searches each variable label for the category labels from the filter question. If some variables cannot be matched correctly to filters then you will be given the option to match filters to variables manually.

When you choose to expand the question by creating a new filtered copy of each variable for each filter, the new variables will be combined into a new question. For example, if the input question is a Number question, then the new question will be a Number - Multi question with one variable for each filter category.

The choice of whether or not to expand the question is not available for Text, Number, or Pick One questions (which are always expanded), or for Pick Any - Grid or Number - Grid questions (which cannot be expanded). Filtering is not available for Experiment, Ranking, or Date questions because it does not make sense in these cases.

If the question that you select to filter with is a Pick One question that is tagged as a Filter (F.png) in the Variables and Questions tab then you will be given the option to apply this single filter to all of the variables in the question that you wish to filter (rather than filtering by each of the categories in the question).

A new folder will be created in your report that contains a table showing the new question, and a second item that contains a list of which variables have been filtered by each of the categories in the filter question.

The new variables are generated with JavaScript formulas that take the value of the original variable for respondents in the filter category, and a value of NaN for respondents who are not in that category. The values and labels are copied from the original question so that any recoding or relabeling that has been done in the original question will be carried through to the new copy.

Requires Q 4.8 or later.

How to apply this QScript

  • Start typing the name of the QScript into the Search features and data box in the top right of the Q window.
  • Click on the QScript when it appears in the QScripts and Rules section of the search results.

OR

  • Select Automate > Browse Online Library.
  • Select this QScript from the list.

Customizing the QScript

This QScript is written in JavaScript and can be customized by copying and modifying the JavaScript.

Customizing QScripts in Q4.11 and more recent versions

  • Start typing the name of the QScript into the Search features and data box in the top right of the Q window.
  • Hover your mouse over the QScript when it appears in the QScripts and Rules section of the search results.
  • Press Edit a Copy (bottom-left corner of the preview).
  • Modify the JavaScript (see QScripts for more detail on this).
  • Either:
    • Run the QScript, by pressing the blue triangle button.
    • Save the QScript and run it at a later time, using Automate > Run QScript (Macro) from File.

Customizing QScripts in older versions

  • Copy the JavaScript shown on this page.
  • Create a new text file, giving it a file extension of .QScript. See here for more information about how to do this.
  • Modify the JavaScript (see QScripts for more detail on this).
  • Run the file using Automate > Run QScript (Macro) from File.

JavaScript

// Create New Variables - Filter One Question by Another Question
 
// This QScript takes two questions - the primary question and the filter question and does
// one of two things, depending on the input questions and the options selected by the user.
 
// The two questions are
 
// Primary Question: Can be a Pick One - Multi, Number - Multi, Pick Any, or Pick Any - Grid
// Filter Question: Can be a Pick One question or a Pick Any question
 
// There are three different modes of generating new variables:
 
// 1. Creates new JavaScript variables for each variable in the primary question
// which are filtered by the corresponding categories in the filter question.
// - Called "Individual Mode".
 
// 2. Creates a new copy of all of the variables in the question for each filter category and
// combines them into a question of the appropriate type.
// - Called "Expansion Mode".
 
// 3. When the filter question is tagged as a filter then each variable in the primary question
// will be duplicated, and the duplicate variable will take a value of NaN for cases where the
// value in the filter variable is 0 or NaN. The Question Type is unchanged.
// - Called "Single Filter Mode".
 
 
// Input                Expansion               Individual
// --------------------------------------------------------------
// Text                 Text - Multi    
// Text - Multi                                 Text - Multi
// Number               Number - Multi  
// Number - Multi       Number - Grid           Number - Multi
// Number - Grid                                Number - Grid
// Pick One             Pick One - Multi    
// Pick One - Multi     Pick One - Multi        Pick One - Multi
// Pick Any             Pick Any - Grid         Pick Any
// Pick Any - Grid                              Pick Any - Grid
 
includeWeb('JavaScript Utilities');
includeWeb('QScript Selection Functions');
includeWeb("QScript Functions to Generate Outputs");


filterOneQuestionByAnother()
 
function filterOneQuestionByAnother() {
    try {
        const is_displayr = inDisplayr();
        const structure_name = is_displayr ? "variable set" : "question";

        // User selections of questions
        var data_file = requestOneDataFileFromProject(false);
        var candidate_types = ["Pick One", "Pick One - Multi", "Pick Any", "Pick Any - Grid", "Number", "Number - Multi", "Number - Grid", "Text", "Text - Multi"];
        var candidate_primary_questions = getAllQuestionsByTypes([data_file], candidate_types);
        var candidate_filter_questions = getAllQuestionsByTypes([data_file], ["Pick One", "Pick Any"]);
        if (candidate_primary_questions.length < 1)
            throw new SetupError("Did not find any appropriate " + structure_name + "s to filter. Only " + structure_name + "s of the following types are supported: " + candidate_types.join(", ") + ".");
        if (candidate_filter_questions.length < 1)
            throw new SetupError("Did not find any appropriate " + structure_name + "s to use as filters.");
 
        // This is one of the rare cases we need to keep the prompts in Displayr.
        // It is too hard to guide the user otherwise.
        let primary_question = selectOneQuestion("Select the " + structure_name + " that you want to filter.", candidate_primary_questions);
        let filter_question = selectOneQuestion("Select the " + structure_name + " whose categories you wish to use as filters.", candidate_filter_questions);

        if (primary_question == null || filter_question == null)
            return false;
 
        let expansion_mode;
        let single_filter_mode;
        if (filter_question.isFilter && filter_question.variables.length == 1) {
            if (is_displayr)
                single_filter_mode = true;
            else
                single_filter_mode = askYesNo("The selected filter " + structure_name + ", " + filter_question.name + ", is tagged as a filter. "
                                          + "Click Yes to create a new copy of " + primary_question.name + " with this filter applied. "
                                          + "Click No to create new variables for each of the categories in the filter " + structure_name + ".");
        }
 
 
        let filter_question_type = filter_question.questionType;
        let primary_variables = primary_question.variables;
        let primary_labels = primary_variables.map(function (v) { return v.label; });
        let primary_question_type = primary_question.questionType;
        let filter_matches;
        let filter_labels;
 
        if (!single_filter_mode) {
            // Determine the labels of the filters
            let filter_question_data_reduction = filter_question.dataReduction;
            
 
            filter_labels = filter_question_data_reduction.rowLabels;
            if (!filter_question_data_reduction.netRows) {
                filter_labels = filter_labels.filter(function (label) { return label != "NET"; });
            } else {
                let net_rows = filter_question_data_reduction.netRows;
                for (let j = filter_labels.length - 1; j >= 0; j--) {
                    if (net_rows.indexOf(j) > -1)
                        filter_labels.splice(j,1);
                }
            }
 
            if (unique(filter_labels).length != filter_labels.length)
                throw new SetupError("Cannot match filters to variables because the filter category labels are not unique.");
 
            // Determine if filtering in expansion mode or individual mode
            const always_expansion_mode = ["Text", "Number", "Pick One"];
            const always_individual_mode = ["Pick Any - Grid", "Number - Grid", "Text - Multi"];
            if (always_expansion_mode.indexOf(primary_question_type) > -1)
                expansion_mode = true;
            else if (always_individual_mode.indexOf(primary_question_type) > -1)
                expansion_mode = false;
            else
                expansion_mode = askYesNo("Select 'Yes' to split out " + primary_question.name + " by each category in " + filter_question.name + 
                    ".\r\nSelect 'No' to match labels between " + primary_question.name + " and " + filter_question.name + ", and create one filtered variable for each match.");
 
            if (expansion_mode) {
                filter_matches = [];
                primary_variables.forEach(function (v) {
                    filter_matches = filter_matches.concat(filter_labels.map(function (label) {
                        return { variable: v, filterLabel: label };
                    }));
                });
 
                filter_matches = generateExpansionModeVariableLabels(filter_matches, primary_question_type);
            } else {
                if (unique(primary_labels).length != primary_labels.length)
                    throw new SetupError("Cannot match filters to variables because the variable labels in the selected " + structure_name + " are not unique.");
                let auto_setup;
                if (is_displayr) 
                    auto_setup = true;
                else
                    auto_setup = askYesNo("Would you like " + (is_displayr ? "Displayr" : "Q") + " to try to match the filters to the variables in your " + structure_name + " automatically?");
                if (auto_setup) {
                    try {
                        filter_matches = automaticFilterMatch(primary_variables, filter_labels);
                    } catch (e) {
                        if (e instanceof AutoDetectError) {
                            let continue_manually = confirm(e.message + " Click OK to match the filters to the variables in your " + structure_name + " manually.");
                            if (continue_manually)
                                auto_setup = false;
                            else
                                return false;
                        } else
                            throw e;
                    }       
                }
                if (!auto_setup)
                    filter_matches = manualFilterMatch(primary_variables, filter_labels);
            }
        } else
            expansion_mode = false;
 
        // Create new variables and new question
        let new_vars;
        if (single_filter_mode)
            new_vars = applySingleFilter(filter_question, primary_question);
        else
            new_vars = createFilteredVariables(filter_matches, primary_question, filter_question, filter_labels, expansion_mode);
 
        let new_question_type = primary_question.questionType;
        if (expansion_mode) {
            if (primary_question_type == "Pick One")
                new_question_type = "Pick One - Multi";
            else if (primary_question_type == "Number")
                new_question_type = "Number - Multi";
            else if (primary_question_type == "Pick Any")
                new_question_type = "Pick Any - Grid";
            else if (primary_question_type == "Text")
                new_question_type = "Text - Multi";
            else if (primary_question_type == "Number - Multi")
                new_question_type = "Number - Grid";
            else if (primary_question_type == "Pick One - Multi")
                new_question_type = "Pick One - Multi";
            else
                throw primary_question_type;
        }
        let new_q = data_file.setQuestion(preventDuplicateQuestionName(data_file, primary_question.name + " filtered by " + filter_question.name), new_question_type, new_vars);
        insertAtHoverButtonIfShown(new_q);
 
        // If the new question is a grid, check that it is valid, and if it isn't then change
        // the question type.
        if (new_question_type == "Pick Any - Grid" || new_question_type == "Number - Grid") {
            if (!new_q.isValid) {
                new_question_type = new_question_type == "Pick Any - Grid" ? "Pick Any" : "Number - Multi";
                new_q.questionType = new_question_type;
            }
        }
 
        // If the new question is a Pick Any then copy the 'Count this value'
        // settings from the old question.
        if (new_question_type.indexOf("Pick Any") > -1) {
            let value_attributes = primary_question.valueAttributes;
            let unique_values = primary_question.uniqueValues;
            let target_value_attributes = new_q.valueAttributes;
            let target_unique_values = new_q.uniqueValues;
            unique_values.forEach(function (x) {
                target_value_attributes.setCountThisValue(x, value_attributes.getCountThisValue(x));
            });
            new_q.needsCheckValuesToCount = false;
        }
 
        // Try to add spans for expansion mode for Pick One - Multi
        // A check is added because previous scripts applied may cause problems
        if (expansion_mode && new_question_type == "Pick One - Multi" && primary_question_type == "Pick One - Multi") {
            let data_reduction = new_q.dataReduction;
            let new_labels = new_vars.map(function (v) { return v.label; });
 
            let label_prefixes = primary_variables.map(function (v) { return v.label; });
            let span_error = [];
            label_prefixes.forEach(function (prefix) {
                let codes = filter_matches.filter(function (obj) { 
                    return obj.newVariableLabel == prefix + " - " + obj.filterLabel; })
                    .map(function (obj) { return obj.newVariableLabel; });
                try {
                    data_reduction.span(codes, prefix);
                } catch (e) {
                    span_error.push(e.message);
                    use_spans = false;
                }
            });

            if (span_error.length == 0)
                filter_matches.forEach(function (obj) {
                    data_reduction.rename(obj.newVariableLabel, obj.filterLabel);
                });
            else
                log("There were problems creating a span from the input question: '" + 
                    unique(span_error) + "'");
        }
 
 
        // Add a new table for the new question and add a text item describing which variables
        // were filtered by each filter.        
        if (!is_displayr)
        {
            let new_group = project.report.appendGroup();
            new_group.name = "Filtered Question";
            let new_table = new_group.appendTable();
            new_table.primary = new_q;
     
            if (!single_filter_mode) {
                let new_text_item = new_group.appendText();
     
                // Text item title
                let title_builder = Q.htmlBuilder();
                title_builder.appendParagraph(primary_question.name + " filtered by " + filter_question.name, { font: 'Tahoma', size: 20 });
                new_text_item.title = title_builder;
                let html_report = Q.htmlBuilder();
                html_report.setStyle({ font: 'Lucida Console', size: 10 });
     
                let filters_used_paragraph = "The following filters have been applied to the variables in the question:";
                html_report.appendParagraph(filters_used_paragraph, { font: 'Lucida Console', size: 10 });
                html_report.appendParagraph(null);
     
                // Report table header
                let table_header = [["Filter Category", "Variables Filtered"]];
                html_report.appendTable(table_header, [20, 50], "", { font: 'Lucida Console', size: 10 });
     
                // Generate table to display the variables that matched each filter
                let filter_report_table = [];
                let filter_match_counts = filter_labels.map(function (filter_label) {
                    let count = 0;
                    filter_matches.forEach(function (match) {
                        if (filter_label == null && match.filterLabel == null)
                            count ++;
                        else if (filter_label != null && match.filterLabel != null)
                            if (match.filterLabel.trim() == filter_label.trim())
                                count ++;
                    });
                    return {label: filter_label, count: count};
                });
                let not_used_filters = filter_match_counts.filter(function (obj) { return obj.count == 0;})
                                                          .map(function (obj) { return obj.label; });
                let used_filters = difference(filter_labels, not_used_filters);
                used_filters.forEach(function (filter) {
                    let first_match = true;
                    filter_matches.forEach(function (obj) {
                        if (obj.filterLabel != null) {
                            if (obj.filterLabel.trim() == filter.trim()) {
                                if (first_match) {
                                    filter_report_table.push(["", ""]);
                                    filter_report_table.push([filter + ":", obj.variable.label]);
                                    first_match = false;
                                } else
                                    filter_report_table.push(["", obj.variable.label]);
                            }
                        }
                    });
                });
     
                // Add rows for variables included but not matched to a filter
                let not_filtered = filter_matches.filter(function (obj) { return obj.filterLabel == null;} );
                if (not_filtered.length > 0) {
                    let first_match = true;
                    not_filtered.forEach(function (obj) {
                        if (first_match) {
                            filter_report_table.push(["", ""]);
                            filter_report_table.push(["Not filtered:", obj.variable.label]);
                            first_match = false;
                        } else
                            filter_report_table.push(["", obj.variable.label]);
                    });
                }
     
                html_report.appendTable(filter_report_table, [20, 50], null, { font: 'Lucida Console', size: 10 });
     
                // Describe any variables not included
                let included_variables = filter_matches.map(function (obj) { return obj.variable.label;} );
                let not_used_variables = difference(primary_labels, included_variables);
                if (not_used_variables.length > 0) {
                    let not_included_paragraph = "The following variables were not included in the new question:"; 
                    html_report.appendParagraph(null);
                    html_report.appendParagraph(not_included_paragraph, { font: 'Lucida Console', size: 10 });
                    html_report.appendParagraph(null);
                    html_report.appendTable(not_used_variables.map(function (x) { return [x]; }), [50], null, { font: 'Lucida Console', size: 10 });
                }
     
                // Describe any filters not used
                if (not_used_filters.length > 0) {
                    let not_used_filters_paragraph = "The following categories were not used to filter any variables:";
                    html_report.appendParagraph(null);
                    html_report.appendParagraph(not_used_filters_paragraph, { font: 'Lucida Console', size: 10 });
                    html_report.appendParagraph(null);
                    html_report.appendTable(not_used_filters.map(function (x) { return [x]; }), [50], null, { font: 'Lucida Console', size: 10 });
                }
     
                new_text_item.content = html_report;
     
                conditionallyEmptyLog("A table showing the new filtered version of " + primary_question.name + " has been added to your report, along with a description of which variables match each filter.");
            } else {
                conditionallyEmptyLog("A table showing " + primary_question.name + " filtered by " + filter_question.name + " has been added to your report.");
            }
            // More recent Q versions can point the user to the new items.
            if (fileFormatVersion() > 8.65)
                project.report.setSelectedRaw([new_group.subItems[0]]);
        }
        return true;
 
    } catch (e) {
        if (e instanceof SetupError)
            log(e.message);
        else
            throw e;
    }
}
 
// Creates filtered variables for expansion mode and individual mode.
function createFilteredVariables(filter_matches, primary_question, filter_question, filter_labels, expansion_mode) {
    if (filter_matches.length == 0)
        throw new SetupError("No variables were selected for filtering.");

    let data_file = primary_question.dataFile; 
    let filter_variables = filter_question.variables;
    let filter_variable_names = filter_variables.map(function (v) { return v.name; } );
    let filter_question_type = filter_question.questionType;
    if (!filter_question.isValid || !primary_question.isValid)
        throw new SetupError("There are errors in the variables selected. Please fix before re-running.");
    
    let primary_question_type = primary_question.questionType;
    let is_text = primary_question_type.indexOf("Text") > -1;
    let filter_question_data_reduction = filter_question.dataReduction;
    let primary_question_variable_names = primary_question.variables.map(function (v) { return v.name;});
    let filter_array;

    // Get array of values or variable names to filter each primary variable by
    if (filter_question_type == "Pick One") {
        filter_array = filter_labels.map(function (label) {
            return filter_question_data_reduction.getUnderlyingValues(label);
        });
    } else {
        filter_array = filter_labels.map(function (label) {
            return filter_question_data_reduction.getUnderlyingVariables(label).map(function (v) { return v.name; });
        });
    }
 
    // Generate new variables
    let new_variables = [];
    let new_variable_data = [];
    let last_var = filter_matches[filter_matches.length - 1].variable;
    filter_matches.forEach(function (match) {
        let source_value_expression = "\tif(isNaN(Q.Source(" + match.variable.name + ")))\r\n\t\t" + match.variable.name + ";\r\n\telse\r\n\t\tQ.Source(" + match.variable.name + ");";
        let no_value_expression = is_text ? "\r\nelse \"\";" : "\r\nelse NaN;";
        let expression;
        if (match.filterLabel == null)
            expression = source_value_expression + ";";
        else {
            let filter_index = filter_labels.indexOf(match.filterLabel);
            expression = "if (";
            if (filter_question_type == "Pick One") {
                filter_array[filter_index].forEach(function (val, index) {
                    let val_exp;
                    if (isNaN(val))
                        val_exp = "isNaN(Q.Source(" + filter_variable_names[0] + "))";
                    else 
                        val_exp = "Q.Source(" + filter_variable_names[0] + ") == " + val;
                    if (index > 0 )
                        expression += " || ";
                    expression += val_exp
                });
 
            } else {
                filter_array[filter_index].forEach(function (name, index) {
                    if (index == 0)
                        expression += name;
                    else
                        expression += " || " + name;
                });
            }
            expression += "){\r\n" + source_value_expression + "\r\n}" + no_value_expression;
        }
        let short_filter_label = match.filterLabel == null ? "NoFilter" : match.filterLabel.replace(/\W/g, "");
        let new_var_label = expansion_mode ? match.newVariableLabel : match.variable.label;
        let new_var = data_file.newJavaScriptVariable(expression, 
                                                      is_text, 
                                                      preventDuplicateVariableName(data_file, match.variable.name + "_f_" + short_filter_label), 
                                                      new_var_label, last_var);
        new_var.variableType = match.variable.variableType;
        new_variables.push(new_var);
        new_variable_data.push({ variable: new_var, index: primary_question_variable_names.indexOf(match.variable.name) });
        last_var = new_var;
        if (!is_text)
            copyValueAttributesToFilteredVariable(new_var, match.variable);
    });

    // For grids it is important the the ordering of the variables is
    // the same for the new filtered question as for the old question.
    if (primary_question_type.indexOf("Grid") > -1) {
        new_variable_data.sort(function (a, b) { return a.index - b.index; });
        let last_var = new_variable_data[0].variable;
        new_variable_data.forEach(function (obj, ind) {
            if (ind > 0) {
                data_file.moveAfter([obj.variable], last_var);
                last_var = obj.variable;
            } 
        });
        new_variables = new_variable_data.map(function (obj) { return obj.variable; });
    }

    return new_variables;
}
 
// Creates filtered variables for Single Filter Mode
function applySingleFilter(filter_question, primary_question) {
    let new_variables = [];
    let data_file = primary_question.dataFile;
    let filter_var = filter_question.variables[0];
    let filter_var_name = filter_var.name;
    let is_text = primary_question.variables[0].variableType == "Text";
    let last_var = primary_question.variables[primary_question.variables.length - 1];
    primary_question.variables.forEach(function (v) {
        let source_value_expression = "\tif(isNaN(Q.Source(" + v.name + ")))\r\n\t\t" + v.name + ";\r\n\telse\r\n\t\tQ.Source(" + v.name + ");";
        let expression = "if (" + filter_var_name + " > 0){\r\n" + source_value_expression + "\r\n}";
        if (is_text)
            expression += "else '';";
        else
            expression += "else NaN;";
        let new_var = data_file.newJavaScriptVariable(expression,
                                                      is_text,
                                                      preventDuplicateVariableName(data_file, v.name + "_f_" + filter_var_name),
                                                      v.label, last_var);
        last_var = new_var;
        new_var.variableType = v.variableType;
        new_variables.push(new_var);
        if (!is_text)
            copyValueAttributesToFilteredVariable(new_var, v);
    });
    return new_variables;
}
 
function automaticFilterMatch(primary_variables, filter_labels) {
    // Match the variables to the filters based on the variable labels
    let filter_matches = primary_variables.map(function (variable) {
        return {variable: variable, filterLabel: matchVariableLabelToFilterLabel(variable, filter_labels)}
    })
 
    // Count the number of matches for each filter.
    let filter_match_counts = filter_labels.map(function (filter_label) {
        let count = 0;
        filter_matches.forEach(function (match) {
            if (match.filterLabel.trim() == filter_label.trim())
                count ++;
        });
        return {label: filter_label, count: count};
    });
    let max_filter_count = 0;
    let second_max_filter_count = 0;
    let max_label;
    let second_max_label;
    // Find largest filter count.
    filter_match_counts.forEach(function (match) {
        if (match.count > max_filter_count) {
            max_filter_count = match.count;
            max_label = match.label;
        }
 
    });
    // Find second-largest filter count.
    filter_match_counts.forEach(function (match) {
        if (match.count > second_max_filter_count && match.count < max_filter_count) {
            second_max_filter_count = match.count;
            second_max_label = match.label;
        }
    });
 
    // Throw errors when:
    // 1 - There are no matches.
    // 2 - Filters match different numbers of variables. This ignores any filter categories
    //     that do not match any variables.
    if (max_filter_count == 0)
        throw new AutoDetectError("The filters did not match any of the variables.");
    else if (second_max_filter_count > 0)
        throw new AutoDetectError("The filters match different numbers of variables. For example, the filter " 
                                  + max_label + " matches " + max_filter_count + " variables, but the filter "
                                  + second_max_label + " only matches " + second_max_filter_count + " variable" 
                                  + (second_max_filter_count == 1 ? "." : "s."));
    return filter_matches;
}
 
 
function manualFilterMatch(primary_variables, filter_labels) {
    let primary_labels = primary_variables.map(function (v) { return v.label; });
    let used_labels = [];
    let filter_matches = [];
    filter_labels.forEach(function (filter) {
        let remaining_labels = difference(primary_labels, used_labels);
        if (remaining_labels.length != 0) {
            let selected_indices = selectMany("Select the variables that should be filtered by \'" + filter + "\'", remaining_labels);
            let selected_labels = getElementsOfArrayBySelectedIndices(remaining_labels, selected_indices);
            selected_labels.forEach(function (label) {
                used_labels.push(label);
                filter_matches.push( {variable: primary_variables[primary_labels.indexOf(label)], filterLabel: filter} );
            });
        }
    });
    let remaining_labels = difference(primary_labels, used_labels);
    if (remaining_labels.length > 0) {
        let include_remaining = askYesNo("Some variables have not been matched to a filter:\r\n" + remaining_labels.join("\r\n") + "\r\nWould you like to include these variables without a filter? Click \'No\' to exclude these variables from the new question.");
        if (include_remaining)
            remaining_labels.forEach(function (label) {
                filter_matches.push( {variable: primary_variables[primary_labels.indexOf(label)], filterLabel: null} );
            });
    }
    return filter_matches;
}
 
// Copies the value attributes from one variable to another
// assuming that, apart from NaN, all of the source values in
// to_variable exist in from_variable. This is guaranteed
// when to_variable is a copy of from_variable by source value.
function copyValueAttributesToFilteredVariable(to_variable, from_variable) {
    let target_unique_values = to_variable.uniqueValues;
    let source_unique_values = from_variable.uniqueValues;
    let target_value_attributes = to_variable.valueAttributes;
    let source_value_attributes = from_variable.valueAttributes;
    // Copy labels, values, and missing data
    target_unique_values.forEach(function (x) {
        if (!isNaN(x)) {
            target_value_attributes.setLabel(x, source_value_attributes.getLabel(x));
            target_value_attributes.setValue(x, source_value_attributes.getValue(x));
            target_value_attributes.setIsMissingData(x, source_value_attributes.getIsMissingData(x));
        } 
    });
 
    // Sepcial handling for when NaN has been recoded to ensure that the recoded nan doesn't
    // get mixed in with the new missing values that are being introduced via the filter.
    if (source_unique_values.some(function (x) { return isNaN(x); })) {
        let recoded_nan = source_value_attributes.getValue(NaN);
        if (!isNaN(recoded_nan)) {
            target_value_attributes.setLabel(recoded_nan, source_value_attributes.getLabel(NaN))
            target_value_attributes.setIsMissingData(recoded_nan, source_value_attributes.getIsMissingData(NaN))
        }
    }
} 
 
function matchVariableLabelToFilterLabel(variable, filter_labels) {
    let variable_label = variable.label.trim();
    let label_matches = filter_labels.map(function (label) {
        return {label: label, 
                matchIndex: variable_label.indexOf(label.trim()), 
                length: label.trim().length};
    }).filter(function (obj) {
        return obj.matchIndex > -1;
    });
 
    if (label_matches.length == 0)
        throw new AutoDetectError("Could not find a filter to match variable with label \'" + variable_label + "\'.");
 
    if (label_matches.length == 1)
        return label_matches[0].label;
 
    // Where there are multiple matches, return the match with the longest label
    let largest_matching_label_length = -1;
    let largest_matching_label = null;
    label_matches.forEach(function (obj) {
        if (obj.length == largest_matching_label_length)
            throw new AutoDetectError("Found more than one filter to match variable with label \'" + variable_label + "\'.");
        else if (obj.length > largest_matching_label_length) {
            largest_matching_label_length = obj.length;
            largest_matching_label = obj.label;
        }
    });
 
    return largest_matching_label;
}
 
// Generate appropriate variable labels when filtering in expansion mode
function generateExpansionModeVariableLabels(filter_matches, primary_question_type) {
    let simple_types = ["Number", "Text", "Pick One"];
    if (simple_types.indexOf(primary_question_type) > -1)
        return filter_matches.map(function (obj) {
            return { variable: obj.variable, filterLabel: obj.filterLabel, newVariableLabel: obj.filterLabel };
        });
    else
        return filter_matches.map(function (obj) {
            return { variable: obj.variable, filterLabel: obj.filterLabel, newVariableLabel:  obj.variable.label + " - " + obj.filterLabel};
        });
}
 
// A custom error object so we can abort the setup of the filtered question,
// and catch this error, presenting the message to the user without
// causing the QScript to crash and show an error report.
function SetupError(message) {
    this.message = message;
}
 
// Custom error to tell the user that there is a problem detecting
// the filter matching automatically. Will be used to allow the
// user to proceed with manual filter matching.
function AutoDetectError(message) {
    this.message = message;
}

Prior to the 15th of December, 2015, this page was known as Create New Variables - Filter One Question by Another Question

See also