QScript Selection Functions

From Q
Jump to navigation Jump to search

The QScript functions selectOne and selectMany allow the user of the QScript to select from a list of strings. The functions on this page are designed to make it easier for the QScript writer to present menus of Q objects, like questions, variables, or data files, for the user to select from without the need to write extra code to extract the labels or names of these objects.

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

selectManyQuestions(message, question_array, use_preselections)

selectManyQuestions() allows you to provide an array of Q Question objects (obtained, for example, by getQuestionsByName()) and a message string. The function obtains the names of the questions and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

questions - is an array of the selected questions
names - is an array of the names of the selected questions

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectManyVariablesByName(message, variable_array, use_preselections)

selectManyVariablesByName() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByName()) and a message string. The function obtains the names of the variables and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

variables - is an array of the selected variables
names - is an array of the names of the selected variables

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectManyVariablesByLabel(message, variable_array, use_preselections)

selectManyVariablesByLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByLabel()) and a message string. The function obtains the labels of the variables and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

variables - is an array of the selected variables
labels - is an array of the labels of the selected variables

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectManyVariablesByNameAndLabel(message, variable_array, use_preselections)

selectManyVariablesByNameAndLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByLabel()) and a message string. The function obtains the names and labels of the variables and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

variables - is an array of the selected variables
names_labels - is an array of the names and labels of the selected variables

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectManyVariablesByQuestionNameAndLabel(message, variable_array, use_preselections)

selectManyVariablesByQuestionNameAndLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByLabel()) and a message string. The function obtains the question names and variable labels of the variables and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

variables - is an array of the selected variables
names_labels - is an array of the names and labels of the selected variables

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectManyVariablesByQuestionNameAndLabelAndDataFile(message, variable_array, use_preselections)

selectManyVariablesByQuestionNameAndLabelAndDataFile() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByLabel()) and a message string. The function obtains the question names and variable labels and data file names of the variables and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

variables - is an array of the selected variables
names_labels - is an array of the names and labels of the selected variables

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectManyDataFiles(message, datafile_array)

selectManyDataFiles() allows you to provide an array of Q Data File objects (obtained, for example, by getVariablesByName()) and a message string. The function obtains the names of the data files and presents them to the user in a box with your message. The user can make multiple selections. The function returns an object with two properties:

files - is an array of the selected files
names - is an array of the names of the selected files

selectOneDataFileFromSelectedQuestions()

Intended for use in Displayr only, selectOneDataFileFromSelectedQuestions() either returns the only dataFile in the report, or if there are multiple dataFiles, uses project.report.selectedQuestions() to determine which dataFile to report based on which one the variable set the user has selected from Data Sets. An error is thrown if the user has selected multiple questions from different data sets or if there are multiple data sets and nothing is selected.

selectOneQuestion(message, question_array, use_preselections)

selectOneQuestion() allows you to provide an array of Q Question objects (obtained, for example, by getQuestionsByName()) and a message string. The function obtains the names of the questions and presents them to the user in a box with your message. The user can only make a single selection. The function returns the question object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneQuestionByNameAndDataFile(message, question_array, use_preselections)

selectOneQuestionByNameAndDataFile() allows you to provide an array of Q Question objects (obtained, for example, by getQuestionsByName()) and a message string. The function obtains the names of the questions with the data file name and presents them to the user in a box with your message. The user can only make a single selection. The function returns the question object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneVariableByLabel(message, variable_array, use_preselections)

selectOneVariableByLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByLabel()) and a message string. The function obtains the labels of the variables and presents them to the user in a box with your message. The user can only make a single selection. The function returns the variable object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneVariableByName(message, variable_array, use_preselections)

selectOneVariableByName() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByName()) and a message string. The function obtains the names of the variables and presents them to the user in a box with your message. The user can only make a single selection. The function returns the variable object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneVariableByNameAndLabel(message, variable_array, use_preselections)

selectOneVariableByNameAndLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByName()) and a message string. The function obtains the names and labels of the variables and presents them to the user in a box with your message. The user can only make a single selection. The function returns the variable object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneVariableByQuestionNameAndLabel(message, variable_array, use_preselections)

selectOneVariableByQuestionNameAndLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByName()) and a message string. The function obtains the question names and variable labels of the variables and presents them to the user in a box with your message. The user can only make a single selection. The function returns the variable object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneVariableByQuestionNameAndLabelAndDataFile(message, variable_array, use_preselections)

selectOneVariableByQuestionNameAndLabel() allows you to provide an array of Q Variable objects (obtained, for example, by getVariablesByName()) and a message string. The function obtains the question names and variable labels and data file names of the variables and presents them to the user in a box with your message. The user can only make a single selection. The function returns the variable object that the user has selected.

The boolean flag use_preselections is used to specify whether or not the menu shown to the user should have items already selected, based on the user's selections in the Variables and Questions tab.

selectOneDataFile(message, datafile_array, default_index)

selectOneDataFile() allows you to provide an array of Q Data File objects (obtained, for example, by project.dataFiles) and a message string. The function obtains the names of the variables and presents them to the user in a box with your message. The user can only make a single selection. The function returns the data file object that the user has selected. The parameter default_index specifies the default selection to show.

truncateStringForSelectionWindow(string)

truncateStringForSelectionWindow() truncates the input string to 120 characters. This function is used by the selection functions above to ensure that the message box does not become very wide when question names and variable labels are very long.

dataFileSelection()

This function checks the project for multiple data files, and if more than one file exists in the project it presents the user with a list of files and asks them which files they would like to use. It then returns an array containing the selected data file objects.

selectManyTablesWithGroupNames(message, group_item)

This function presents the user with a list of all of the tables in the input group_item and allows them to select multiple items.

Each item in the list contains the name of the table, along with the name of the groups that contain the table, up to but not including the name of group_item.

The function returns an object containing two properties:

  • tables is an array of the selected table objects.
  • names is an array containing the names of the selected tables.

selectManyTablesAndPlotsWithGroupNamesAndTypes(message, group_item)

This function presents a list of all of the tables and charts in the input group_item and allows the user to make multiple selections.

Each item in the list contains the name of the table, along with the name of the groups that contain the table, up to but not including the name of group_item, and also whether the item it a table or a chart.

The function returns an object containing two properties:

  • items is an array of the selected items.
  • names is an array containing the names of the selected items.

selectOneTableWithGroupNames(message, group_item)

This function presents the user with a list of all of the tables in the input group_item and allows them to select one table. Each item in the list contains the name of the table, along with the name of the groups that contain the table, up to but not including the name of group_item. The table object selected by the user is returned.

requestOneDataFileFromProject(null_if_cancelled, last_as_default)

This function requests the user to select a data file if there is more than one data file in the project, and returns the selected data file. Otherwise returns the single data file in the project. The flag null_if_cancelled controls whether to return null if the dialog is cancelled. The flag last_as_default controls whether to show the last data file as the default selection.

selectedTablesAndPlots(report)

This function returns all tables and charts selected in the report tree in Q 4.8 or later. For older versions of Q, a listbox is shown which allows the user to select from tables and charts obtained from the report tree.

recursiveGetAllTablesInGroup(group_item, table_array)

This function adds all of the tables in group_item to the array table_array. The function is called recursive because it obtains tables from the subgroups of the specified group by applying itself to each subgroup.

You should always supply an existing array variable:

var group_1_tables = []; recursiveGetAllTablesInGroup(group_1, group_1_tables);

recursiveGetAllTablesAndPlotsInGroup(group_item, table_array)

This function adds all of the tables in group_item to the array table_array. The function is called recursive because it obtains tables from the subgroups of the specified group by applying itself to each subgroup.

You should always supply an existing array variable:

var group_1_tables_plots = []; recursiveGetAllTablesAndPlotsInGroup(group_1, group_1_tables_plots);

recursiveGetAllGroupsInGroup(group_item, group_array)

This function places all pages/QScript ReportGroups in the ReportGroup, group_item, into the supplied array, group_array.

recursiveGetAllItemsInGroup(group_item, table_array)

This function places all sub-items (regardless of type) of the ReportGroup, group_item, into the supplied array, group_array. Sub-items of ReportGroup sub-items are also included recursively.

filePromptWithVerification(message, override_file_name, options)

This function is used to add a data file to the Q Project by prompting the user to enter the path and name of the file. If the user mis-types the file name then they are given the opportunity to re-enter the file name. When using Q 4.9 and above it is better to use the function addDataFileDialog() instead.

oneOrMoreQuestions(questions)

Returns true if the array questions is not empty. If it is empty it returns false and returns a message for the user. This is intended to be used in a loop to give the user additional opportunity to select questions of they failed to select a question when prompted.

indicesOfQObjectsInArray(array_1, array_2)

Returns the indices of objects in array_1 which are also elements of array_2. The two input arrays should only contain Q objects, e.g. questions, variables, etc.

selectOneVariableOrQuestionUsingPreselections(message, object_array, label_array, use_preselections, help_page)

Generalizes the function selectOne() for questions and variables, allowing any selections made by the user to be pre-selected in the list that is shown to the user. Returns the index of the item selected by the user just like selectOne().

  • message: The message to show to user.
  • object_array: The array of questions or variables to be selected from.
  • label_array: The labels corresponding to the items in object_array, in the same order as the objects. These are shown to the user.
  • use_preselections: Boolean flag to toggle whether the user is shown preselected items in the menu.
  • help_page: help page to direct the user to.

selectManyVariablesOrQuestionsUsingPreselections(message, object_array, label_array, use_preselections, help_page)

Generalizes the function selectMany() for questions and variables, allowing any selections made by the user to be pre-selected in the list that is shown to the user. Returns the indices of the items selected by the user just like selectMany().

  • message: The message to show to user.
  • object_array: The array of questions or variables to be selected from.
  • label_array: The labels corresponding to the items in object_array, in the same order as the objects. These are shown to the user.
  • use_preselections: Boolean flag to toggle whether the user is shown preselected items in the menu.
  • help_page: help page to direct the user to.

promptUntilBlank(message)

Repeatedly prompts the user until they enter a blank selection. Returns an array of the entered strings.

promptForDKLabels()

Prompt the user to enter extra labels corresponding to Don't Know options in the questionnaire. Returns an array containing the entered labels.

getQuestionsSelectedInTables()

Search through all of the tables and charts that have been selected by the user at the time the script is run and return an array of objects, each of which has two properties:

  • question - The question object
  • item - the table or chart which contains the question
  • position - a string telling us if the questions is in Primary (blue), Secondary (brown), or Tertiary (the second blue menu on some charts).

replaceQuestionsInSelectedTablesAndPlots(question_pairs, replace_in_plots)

Go through each of the tables and charts that are selected at the time the QScript runs, and use the array question_pairs to replace the questions that are selected in those items.

question_pairs should be an array, where each element has two properties:

  • originalQuestion - the original question that we want to replace.
  • newQuestion - the new question that we want to replace originalQuestion with.

If it is not appropriate to make replacements in charts, then set replace_in_plots to false.

getSelectedROutputFromPage(required_class)

This function is designed to resolve ambiguity about which R Output the user has selected when running various Save Variables QScripts. It relaxes the restriction that the user has to specifically have the intended R Output selected, instead looking for an unambiguously appropriate output in the current page (folder). If there is a single appropriate R Output on the current page then this will be returned. When there are multiple appropriate R Outputs on the page, then a message will be returned prompting the user to select one of them.

The argument required_class is used to restrict the search to only R Outputs that have a specific R class. If you don't want to restrict to a particular class, then supply a value of null

questionsInSelectedItems()

This function returns an array of the questions that are used by all the selected tables and plots.


getAllUserSelections()

This function is designed to organize the information about which variables, questions, folders, tables, and other outputs, that the user currently has selected at the time the script is run. The point is to make it easier for the QScript author to identify what is selected without having to know the distinction between the various native selection functions, like project.report.selectedRaw(). It distiguishes between things that are directly selected, and things which are implicitly selected (for example tables may be implicitly selected by virtue of being in a subfolder of a folder that is selected directly). The returned value is a JavaScript object containing the following properties, most of which are arrays of items:

  • selected_data_sets: Data Sets currently selected
  • selected_questions: All questions directly selected by the user
  • selected_pages: All pages directly selected by the user (includes sub pages)
  • selected_tables: All tables directly selected by the user
  • selected_plots: All Q plots, excluding word clouds, directly selected by the user
  • selected_word_clouds: All word clouds directlt selected by the user
  • selected_r_outputs: All Calculations directly selected by the user
  • selected_top_level_pages: All selected pages at the top level only (that is, does not include subpages of selected pages)
  • selected_variables: All variables directly selected by the user
  • single_variable_questions_selected: All directly selected questions that only contain a single variable
  • multiple_variable_questions_selected: All directly selected questions that contain multiple variables
  • implicitly_selected_items: All items, including tables, plots, R outputs implicitly selected by the user
  • implicitly_selected_tables: Tables selected implicitly by the user
  • implicitly_selected_plots: Plots implicitly selected by the user
  • implicitly_selected_word_clouds: Word clouds implictly selected by the user
  • implicitly_selected_r_outputs: Calculations implicitly selected by the user
  • questions_in_selected_tables: Questions present in the tables which are directly selected by the user
  • questions_in_rows_of_selected_tables: Questions present in the rows of tables which are directly selected by the user
  • questions_in_columns_of_selected_tables: Questions present in the columns of tables which are directly selected by the user
  • questions_in_selected_plots: Questions present in the Q plots which are directly selected by the user
  • current_page: The page the user is currently on

NOrMoreSelected(user_selections, type, n)

This function is designed to make it easy to check if the user has selected a certain number (or more) of items of a particular type. This is typically to ensure that the user has selected appropriate inputs before continuing with the rest of the QScript, or to distinguish which mode the QScript should run in (e.g. should it apply to selected tables or selected variables). This function returns true or false based on the user's selections.

The arguments are:

  • user_selections - an object generated by the function getAllUserSelections() above.
  • type - a string identifying the type of item that you want to ensure that the user has selected. Options are:
    • "Group" - a folder in Q or a page in Displayr
    • "Page" - a folder in Q or a page in Displayr
    • "R Output" - an R Output (now referred to as a Calculation in Displayr)
    • "Plot" - a native Q plot
    • "Question" - a Question (referred to as a Variable Set in Displayr)
    • "Variable Set" - a Question (referred to as a Variable Set in Displayr)
    • "Variable" - a Variable
    • "WordCloud" - a Word Cloud
  • n - an integer specifying the minimum number of objects of the specified type which must be selected.

exactlyNSelected(user_selections, type, n)

As with NOrMoreSelected() above except that the user must have exactly the right number of the specified type of items selected.

Source Code

includeWeb('QScript Functions for Processing Arrays');
includeWeb('QScript Utility Functions');  // for inDisplayr, correctTerminology
includeWeb('JavaScript Array Functions');

// SELECTION

function selectManyQuestions(message, question_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let question_names = getNamesOfQuestionsInArray(question_array);
    let selected_indices = selectManyVariablesOrQuestionsUsingPreselections(message, question_array, question_names.map(truncateStringForSelectionWindow), use_preselections, null);
    let selected_questions = getElementsOfArrayBySelectedIndices(question_array, selected_indices);
    let selected_names = getElementsOfArrayBySelectedIndices(question_names, selected_indices);
    return {questions: selected_questions, names: selected_names};
}

// Generalization of selectMany() to array of variables, allowing the user
// to make selection by variable names.
function selectManyVariablesByName(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names = getNamesOfVariablesInArray(variable_array);
    let selected_indices = selectManyVariablesOrQuestionsUsingPreselections(message, variable_array, variable_names.map(truncateStringForSelectionWindow), use_preselections, null);
    let selected_variables = getElementsOfArrayBySelectedIndices(variable_array, selected_indices);
    let selected_names = getElementsOfArrayBySelectedIndices(variable_names, selected_indices);
    return {variables: selected_variables, names: selected_names};
}


// Generalization of selectMany() to array of variables, allowing the user
// to make selection by variable labels.
function selectManyVariablesByLabel(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_labels = getLabelsOfVariablesInArray(variable_array);
    let selected_indices = selectManyVariablesOrQuestionsUsingPreselections(message, variable_array, variable_labels.map(truncateStringForSelectionWindow), use_preselections, null);
    let selected_variables = getElementsOfArrayBySelectedIndices(variable_array, selected_indices);
    let selected_labels = getElementsOfArrayBySelectedIndices(variable_labels, selected_indices);
    return {variables: selected_variables, labels: selected_labels};
}

function selectManyVariablesByNameAndLabel(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names_labels = getNamesAndLabelsOfVariablesInArray(variable_array);
    let selected_indices = selectManyVariablesOrQuestionsUsingPreselections(message, variable_array, variable_names_labels.map(truncateStringForSelectionWindow), use_preselections, null);
    let selected_variables = getElementsOfArrayBySelectedIndices(variable_array, selected_indices);
    let selected_names_labels = getElementsOfArrayBySelectedIndices(variable_names_labels, selected_indices);
    return {variables: selected_variables, names_labels: selected_names_labels};
}

function selectManyVariablesByQuestionNameAndLabel(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names_labels = getQuestionNamesAndLabelsOfVariablesInArray(variable_array);
    let selected_indices = selectManyVariablesOrQuestionsUsingPreselections(message, variable_array, variable_names_labels.map(truncateStringForSelectionWindow), use_preselections, null);
    let selected_variables = getElementsOfArrayBySelectedIndices(variable_array, selected_indices);
    let selected_names_labels = getElementsOfArrayBySelectedIndices(variable_names_labels, selected_indices);
    return {variables: selected_variables, names_labels: selected_names_labels};
}

function selectManyVariablesByQuestionNameAndLabelAndDataFile(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names_labels = getQuestionNamesAndLabelsAndDataFilesOfVariablesInArray(variable_array);
    let selected_indices = selectManyVariablesOrQuestionsUsingPreselections(message, variable_array, variable_names_labels.map(truncateStringForSelectionWindow), use_preselections, null);
    let selected_variables = getElementsOfArrayBySelectedIndices(variable_array, selected_indices);
    let selected_names_labels = getElementsOfArrayBySelectedIndices(variable_names_labels, selected_indices);
    return {variables: selected_variables, names_labels: selected_names_labels};
}

// Generalization of selectOne() to accept an array of questions
function selectOneQuestion(message, question_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let question_names = getNamesOfQuestionsInArray(question_array).map(truncateStringForSelectionWindow);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, question_array, question_names, use_preselections, null);
    return question_array[selected_index];
}

function selectOneQuestionByNameAndDataFile(message, question_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let question_names = getNamesOfQuestionsAndDataFilesInArray(question_array);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, question_array, question_names, use_preselections, null);
    return question_array[selected_index];
}


// Generalization of selectOne() to accept an array of variables, allowing the user to
// make a selection by the variable labels
function selectOneVariableByLabel(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_labels = getLabelsOfVariablesInArray(variable_array);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, variable_array, variable_labels.map(truncateStringForSelectionWindow), use_preselections, null);
    return variable_array[selected_index];
}


// Generalization of selectOne() to accept an array of variables, allowing the user to
// make a selection by the variable names
function selectOneVariableByName(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names = getNamesOfVariablesInArray(variable_array);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, variable_array, variable_names.map(truncateStringForSelectionWindow), use_preselections, null);
    return variable_array[selected_index];
}

function selectOneVariableByNameAndLabel(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names = getNamesAndLabelsOfVariablesInArray(variable_array);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, variable_array, variable_names.map(truncateStringForSelectionWindow), use_preselections, null);
    return variable_array[selected_index];
}

function selectOneVariableByQuestionNameAndLabel(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names = getQuestionNamesAndLabelsOfVariablesInArray(variable_array);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, variable_array, variable_names.map(truncateStringForSelectionWindow), use_preselections, null);
    return variable_array[selected_index];
}

function selectOneVariableByQuestionNameAndLabelAndDataFile(message, variable_array, use_preselections) {
    if (use_preselections == undefined)
        var use_preselections = true;
    let variable_names = getQuestionNamesAndLabelsAndDataFilesOfVariablesInArray(variable_array);
    let selected_index = selectOneVariableOrQuestionUsingPreselections(message, variable_array, variable_names.map(truncateStringForSelectionWindow), use_preselections, null);
    return variable_array[selected_index];
}

// Generalization of selectOne() to accept an array of data files, allowing the user to
// make a selection by the data file names
function selectOneDataFile(message, datafile_array, default_index) {
    if (datafile_array.length === 0)
         throw new UserError('At least one Data Set is required to use this script. ' +
                             'Please add a Data Set before re-running this script.');
    if (datafile_array.length === 1)
         return datafile_array[0];
    let datafile_names = getNamesOfDataFilesInArray(datafile_array);
    let selected_index = selectOne(message, datafile_names.map(truncateStringForSelectionWindow), null, default_index);
    return datafile_array[selected_index];
}

// Generalization of selectMany() to array of data files, allowing the user
// to make selection by the current name of the file.
function selectManyDataFiles(message, datafile_array) {
    let datafile_names = getNamesOfDataFilesInArray(datafile_array);
    let selected_indices = selectMany(message, datafile_names.map(truncateStringForSelectionWindow));
    let selected_datafiles = getElementsOfArrayBySelectedIndices(datafile_array, selected_indices);
    let selected_names = getElementsOfArrayBySelectedIndices(datafile_names, selected_indices);
    return {files: selected_datafiles, names: selected_names};
}

// If string is longer than 120 characters, truncate it so that it fits nicely on the window.
function truncateStringForSelectionWindow(string) {
    let max_chars = 120;
    if (string.length < max_chars)
        return string;
    else
        return string.substring(0, max_chars - 3) + '...';
}

// If there is more than one data file in the current project, show the user a list
// and ask them which data files they want to use.
function dataFileSelection() {
    let datafiles = project.dataFiles;
    let selected_datafiles = datafiles;
    if (datafiles.length > 1)
        selected_datafiles = selectManyDataFiles('Please choose which data files you would like to use:', datafiles).files;
    return selected_datafiles;
}

function selectManyTablesWithGroupNames(message, group_item) {
    let all_tables = [];
    recursiveGetAllTablesInGroup(group_item, all_tables);
    let table_names = getGroupNamesForTablesInArray(all_tables, group_item.name);
    let selected_indices = selectMany(message, table_names);
    let selected_tables = getElementsOfArrayBySelectedIndices(all_tables, selected_indices);
    let selected_names = getElementsOfArrayBySelectedIndices(table_names, selected_indices);
    return {tables: selected_tables, names: selected_names};
}

function selectManyTablesAndPlotsWithGroupNamesAndTypes(message, group_item) {
    let all_items = [];
    recursiveGetAllTablesAndPlotsInGroup(group_item, all_items);
    let item_names = getGroupNamesForTablesAndPlotsInArrayWithType(all_items, group_item.name);
    let selected_indices = selectMany(message, item_names);
    let selected_items = getElementsOfArrayBySelectedIndices(all_items, selected_indices);
    let selected_names = getElementsOfArrayBySelectedIndices(item_names, selected_indices);
    return {items: selected_items, names: selected_names};
}

function selectOneTableWithGroupNames(message, group_item) {
    let all_tables = [];
    recursiveGetAllTablesInGroup(group_item, all_tables);
    let table_names = getGroupNamesForTablesInArray(all_tables, group_item.name);
    if (table_names.length == 0)
        return [];
    let selected_index = selectOne(message, table_names);
    return all_tables[selected_index];
}

function requestOneDataFileFromProject(last_as_default) {
    let is_displayr = inDisplayr();

    if (project.dataFiles.length === 0) {
        throw new UserError(correctTerminology('You first must add a Data Set to your document to use this feature.'));
    }

    if (project.dataFiles.length === 1) {
        return project.dataFiles[0];
    } else {
        let user_selections = getAllUserSelections();
        let selected_data_files = user_selections.selected_data_sets;
    if (selected_data_files.length === 1) {
            return selected_data_files[0];
        }

    if (is_displayr)
    {
        let error_msg = (selected_data_files.length === 0 ?
        'Please select data from any Data Set and run this feature again.' :
        ('There is more than one Data Set in your document. ' +
         'Please select only one from Data Sets and run this feature again.'));
        throw new UserError(error_msg);
    }else
    {
            let data_file_opts = project.dataFiles;
            let msg = 'There is more than one Data Set in your project. ' +
                      'Please select the Data Set to use:';
            let default_selection = last_as_default ? data_file_opts.length - 1 : undefined;
            return selectOneDataFile(msg, data_file_opts, default_selection);
    }
    }
}

function selectedTablesAndPlots(report) {
    if (!report.selectedItems)
        return selectManyTablesAndPlotsWithGroupNamesAndTypes('Select tables and plots', report).items;
    let selected_items = report.selectedItems();
    if (selected_items.length == 0) {
        if (confirm('You currently only have no tables and plots selected.  Would you like to have this script run on everything in the Report?')) {
            selected_items = [];
            recursiveGetAllTablesAndPlotsInGroup(report, selected_items);
        }
    } else if (selected_items.length == 1) {
        if (confirm('You currently only have one table/plot selected.  Would you like to have this script run on everything in the Report?')) {
            selected_items = [];
            recursiveGetAllTablesAndPlotsInGroup(report, selected_items);
        }
    }
    let selected_tables_and_plots = [];
    for (let i = 0; i < selected_items.length; i++) {
        let item = selected_items[i];
        if (item.type == 'Table' || item.type == 'Plot')
            selected_tables_and_plots.push(item);
    }
    return selected_tables_and_plots;
}

// Place all tables in the group into the supplied table_array.
// This function will get tables from all subgroups of the
// specified group_item
function recursiveGetAllTablesInGroup(group_item, table_array) {
    let cur_sub_items = group_item.subItems;
    for (let j = 0; j < cur_sub_items.length; j++) {
        if (cur_sub_items[j].type == 'ReportGroup') {
            recursiveGetAllTablesInGroup(cur_sub_items[j], table_array);
        }
        else if (cur_sub_items[j].type == 'Table')  {
            table_array.push(cur_sub_items[j]);
        }
    }
}

// Place all tables and plots in the group into the supplied table_array.
// This function will get tables from all subgroups of the
// specified group_item
function recursiveGetAllTablesAndPlotsInGroup(group_item, table_array) {
    let cur_sub_items = group_item.subItems;
    for (let j = 0; j < cur_sub_items.length; j++) {
        if (cur_sub_items[j].type == 'ReportGroup') {
            recursiveGetAllTablesAndPlotsInGroup(cur_sub_items[j], table_array);
        }
        else if (cur_sub_items[j].type == 'Table'  || cur_sub_items[j].type == 'Plot')  {
            table_array.push(cur_sub_items[j]);
        }
    }
}

// Place all pages in the group into the supplied table_array.
// This function will get pages from all subgroups of the
// specified group_item
function recursiveGetAllGroupsInGroup(group_item, group_array) {
    let cur_sub_items = group_item.subItems;
    for (let j = 0; j < cur_sub_items.length; j++) {
        if (cur_sub_items[j].type == 'ReportGroup') {
            group_array.push(cur_sub_items[j])
            recursiveGetAllGroupsInGroup(cur_sub_items[j], group_array);
        }
    }
}


// Place all items in the group into the supplied table_array.
// This function will get tables from all subgroups of the
// specified group_item
function recursiveGetAllItemsInGroup(group_item, table_array) {
    let cur_sub_items = group_item.subItems;
    for (let j = 0; j < cur_sub_items.length; j++) {
        if (cur_sub_items[j].type == 'ReportGroup') {
            recursiveGetAllItemsInGroup(cur_sub_items[j], table_array);
        } else {
            table_array.push(cur_sub_items[j]);
        }
    }
}


// Try to add a data file to the project by asking the user to type the path.
// If the file can't be found then tell the user that the name is invalid
// and allow them to try again.
function filePromptWithVerification(message, override_file_name, options) {
    let given_valid_file = false;
    while (!given_valid_file) {
        let filename = prompt(message);
        try {
            file = project.addDataFile(filename, override_file_name, options);
            given_valid_file = true;
        } catch (e) {
            alert('Invalid file name!');
        }
    }
    return file;
}

function oneOrMoreQuestions(questions) {
    if (questions.length == 0) {
        alert('Select one or more questions.');
        return false;
    } else
        return true;
}


// Returns the indices of the objects in array_1 that are contained in array_2
// Can be arrays of questions or arrays of variables.
function indicesOfQObjectsInArray(array_1, array_2) {
    if (array_1.length == 0 || array_2.length == 0)
        return [];

    return array_2.map(function (obj_2) {
        let ind = -1;
        array_1.forEach(function (obj_1, index) {
            if (ind == -1)
                if (obj_1.equals(obj_2))
                    ind = index;
        });
        return ind;
    }).filter(function (x) { return x >= 0; });
}

function allQObjectsWithSameType(array) {
    let target_type = array[0].type;
    return array.every(function (x) { return x.type == target_type; });
}




function selectOneVariableOrQuestionUsingPreselections(message, object_array, label_array, use_preselections, help_page) {
    if (object_array == 'undefined' || object_array.length == 0)
        throw new UserError('No questions of the appropriate type were found');
    let object_type = object_array[0].type;
    // No preselections for objects other than variables and questions
    if (object_type != 'Variable' && object_type != 'Question')
        throw('Expected array of variables or questions.');
    if (!allQObjectsWithSameType(object_array))
        throw('Expected an array of objects with the same type.');

    let selected_index;
    if (project.report.selectedQuestions && use_preselections) {
        let preselections;
        if (object_type == 'Variable')
            preselections = project.report.selectedVariables();
        else
            preselections = project.report.selectedQuestions();
        let preselected_index = null;
        if (preselections.length > 0)
            preselected_index = indicesOfQObjectsInArray(object_array, [preselections[0]])[0];
        selected_index = selectOne(message, label_array, help_page, preselected_index);
    } else
        selected_index = selectOne(message, label_array, help_page)
    return selected_index;
}

function selectManyVariablesOrQuestionsUsingPreselections(message, object_array, label_array, use_preselections, help_page) {
    if (object_array == 'undefined' || object_array.length == 0)
        throw new UserError('No questions of the appropriate type were found');
    let object_type = object_array[0].type;
    // No preselections for objects other than variables and questions
    if (object_type != 'Variable' && object_type != 'Question')
        throw('Expected array of variables or questions.');
    if (!allQObjectsWithSameType(object_array))
        throw('Expected an array of objects with the same type.');

    let selected_indices;
    if (project.report.selectedQuestions && use_preselections) {
        var preselections;
        if (object_type == 'Variable')
            preselections = project.report.selectedVariables();
        else
            preselections = project.report.selectedQuestions();
        var preselected_indices = null;
        if (preselections.length > 0)
            preselected_indices = indicesOfQObjectsInArray(object_array, preselections);
        selected_indices = selectMany(message, label_array, help_page, preselected_indices);
    } else
        selected_indices = selectMany(message, label_array, help_page)
    return selected_indices;
}


// Keep prompting the use for more entries until they enter a blank string.
// The message should tell them that they can stop if they enter a blank.
function promptUntilBlank(message) {
    let answers = [];
    while (true) {
        var new_answer = prompt(message);
        if (new_answer == '')
            break;
        else
            answers.push(new_answer);
    }
    return answers;
}

function promptForDKLabels() {
    // Allow the user to enter additional strings for don't know labels
    var extra_dk_strings = [];
    if (askYesNo("This tool will check for standard 'Don't Know' style responses, including: " + _DK_STRINGS_GLOBAL.join(', ')
                 + ", and labels that are NA. Do you wish to add additional 'Don't Know' response labels?"))
        extra_dk_strings = promptUntilBlank("Enter an extra 'Don't Know' label. If you have finished, click OK.");
    extra_dk_strings = extra_dk_strings.map(function (s) { return s.toLowerCase(); });
    return extra_dk_strings;
}

// Search through all the select tables and plots and return an array of objects, each of which has
// two properties:
// * question - The question object
// * item - the table or plot which contains the question
// * position - a string telling us if the questions is in Primary (blue), Secondary (brown)
//              or Tertiary (second bule on some plots)

function getQuestionsSelectedInTables() {
    var selected_questions = [];
    var selected_items = project.report.selectedItems();
    selected_items.forEach(function (item) {
        if (item.type == 'Table' || item.type == 'Plot') {
            // Primary
            selected_questions.push( {question: item.primary, item: item, position: 'Primary'} );
            // Secondary
            if (typeof item.secondary != 'string' && item.secondary != null)
                selected_questions.push({question: item.secondary, item: item, postion: 'Secondary'} );
            // Tertiary
            if (item.tertiary != null)
                selected_questions.push( {question: item.tertiary, item: item, position: 'Tertiary'} );
        }
    });
    return selected_questions;
}

// Given an array of question pairs (originalQuestion, newQuestion),
// search through the tables and plots that were selected by the user
// at run time and replace originalQuestion with newQuestion wherever found.
// In case it is not a good idea to modify plots, pass replace_in_plots = false.
// If it is not appropriate for the user to select plots then it might be
// better to catch this earlier in the QScript.
function replaceQuestionsInSelectedTablesAndPlots(question_pairs, replace_in_plots) {

    // Get all questions that are in any of the selected items,
    // aong with the link back to the table;
    var item_questions = getQuestionsSelectedInTables();

    question_pairs.forEach(function (pair) {

        // Get all item objects which contain the original question
        // in this pair.
        var relevant_items = item_questions.filter(function (obj) {
            return obj.question.equals(pair.originalQuestion);
        });

        // Loop through those objbects and replace with new question
        // where we can.
        relevant_items.forEach(function (obj) {
            var current_item = obj.item;
            var current_type = current_item.type;
            if (current_type == 'Table' || (current_type == 'Plot' && replace_in_plots)) {
                if (obj.position == 'Primary')
                    current_item.primary = pair.newQuestion;
                else if (obj.position == 'Secondary')
                    current_item.secondary = pair.newQuestion;
                else if (obj.position == 'Tertiary')
                    current_item.tertiary == pair.newQuestion;
            }
        })
    });
}

// Return either the selected item if it is an R output, or search the current page for a single
// R output and return that. If you want to restrict to only returning ROutputs of a specific class
// then set required_classes to an array of acceptable class names of the class as a string. If you
// don't want to restrict, supply an empty array instead.
function getSelectedROutputFromPage(required_classes) {

    // When a single item is identified, use this function to check it is valid and the right class
    function checkLonelyROutput(selected_item) {
        if (!checkROutputIsValid(selected_item))
            return false;
        if (required_classes.length > 0 && !required_classes.some(function (required_class) { return selected_item.outputClasses.indexOf(required_class) > -1; }))
            return false;

        return true;
    }

    var selected_items = project.report.selectedRaw()
    if (selected_items.length == 0)
        return null;

    var selected_item = selected_items[0];

    // If a single R Output is selected return it if it is the right class
    // If it is the wrong class, return null
    if (selected_items.length == 1 && selected_item.type == 'R Output') {
        if (checkLonelyROutput(selected_item))
            return selected_item;
        else
            return null;
    }

    // Not an ROutput selected. Search the group/page to see if
    // there is an ROutput
    var selected_group = selected_item.type == 'ReportGroup' ? selected_item : selected_item.group;
    var items_in_group = selected_group.subItems;
    items_in_group = items_in_group.filter(function (item) { return item.group.equals(selected_group); }); // Filter out items from further down the tree
    items_in_group = items_in_group.filter(function (item) { return item.type == 'R Output'; });

    // No R Outputs on the page
    if (items_in_group.length == 0)
        return null;

    // If there is a single R Output on the page, check it and return it if it is good to use
    if (items_in_group.length == 1) {
        if(checkLonelyROutput(items_in_group[0]))
            return items_in_group[0];
        else
            return null;
    }

    // Multiple R Outputs on the page, filter out any which are invalid or the wrong class
    items_in_group = items_in_group.filter(function (item) { return item.error === null && item.outputClasses !== null; } );

    if (items_in_group.length == 0)
        return null;

    // If a class has been specified, filter to that class
    if (required_classes.length > 0) {
        items_in_group = items_in_group.filter(function (item) {
            return required_classes.some(function (required_class) { return item.outputClasses.indexOf(required_class) > -1; });
        });
    }

    // If a single item remains, return it
    if (items_in_group.length == 1)
        return items_in_group[0];

    if (items_in_group.length > 1)
        log('There are multiple R Outputs available on this page, but this option requires a single R Output. ' +
            'Click the item you want to use and run this option again.');

    return null;
}

function questionsInSelectedItems() {
    var questions_in_selected_items = [];
    project.report.selectedItems().forEach(function (item) {
        if (item.type == 'Table' || item.type == 'Plot') {
            if (item.primary.type == 'Question')
                questions_in_selected_items.push(item.primary);
            if (item.secondary.type == 'Question')
                questions_in_selected_items.push(item.secondary);
            if (item.tertiary != null && item.tertiary.type == 'Question')
                questions_in_selected_items.push(item.tertiary);
        }
    });
    return questions_in_selected_items;
}


// Returns (all are arrays of Q objects):
// - selected_data_sets: Data Sets currently selected
// - selected_questions: All questions directly selected by the user
// - selected_pages: All pages directly selected by the user (includes sub pages)
// - selected_tables: All tables directly selected by the user
// - selected_plots: All Q plots, excluding word clouds, directly selected by the user
// - selected_word_clouds: All word clouds directlt selected by the user
// - selected_r_outputs: All Calculations directly selected by the user
// - selected_top_level_pages: All selected pages at the top level only (that is, does not include subpages of selected pages)
// - selected_variables: All variables directly selected by the user
// - single_variable_questions_selected: All directly selected questions that only contain a single variable
// - multiple_variable_questions_selected: All directly selected questions that contain multiple variables
// - implicitly_selected_items: All items, including tables, plots, R outputs implicitly selected by the user
// - implicitly_selected_tables: Tables selected implicitly by the user
// - implicitly_selected_plots: Plots implicitly selected by the user
// - implicitly_selected_word_clouds: Word clouds implictly selected by the user
// - implicitly_selected_r_outputs: Calculations implicitly selected by the user
// - questions_in_selected_tables: Questions present in the tables which are directly selected by the user
// - questions_in_rows_of_selected_tables: Questions present in the rows of tables which are directly selected by the user
// - questions_in_columns_of_selected_tables: Questions present in the columns of tables which are directly selected by the user
// - questions_in_selected_plots: Questions present in the Q plots which are directly selected by the user
// - v_and_q_selected_questions: Questions selected in the Variables and Questions tab. Will be empty if the user is not in this tab
// - v_and_q_selected_variables: Variables selected in the Variables and Questions tab. Will be empty if the user is not in this tab
// - current_page: The page the user is currently on
// Function to obtain all user selections.
function getAllUserSelections() {

    let is_displayr = !!Q.isOnTheWeb && Q.isOnTheWeb();
    var selected_questions = project.report.selectedQuestions();
    var selected_raw = project.report.selectedRaw();
    var selected_variables = project.report.selectedVariables();

    var implicitly_selected_items = project.report.selectedItems().filter(function (item) { return ['Table', 'Plot', 'WordCloud', 'R Output'].includes(item.type); });
    var selected_tables = [];
    var selected_plots = [];
    var selected_word_clouds = [];
    var selected_r_outputs = [];

    selected_raw.forEach(function(item) {
        switch (item.type) {
            case 'Table' : selected_tables.push(item); break;
            case 'Plot' : selected_plots.push(item); break;
            case 'Word Cloud' : selected_word_clouds.push(item); break;
            case 'R Output' : selected_r_outputs.push(item); break;
        }
    });
    var selected_items = selected_raw.filter(function(item) { return ['Table', 'Plot', 'WordCloud', 'R Output'].includes(item.type); });

    var implicitly_selected_tables = [];
    var implicitly_selected_plots = [];
    var implicitly_selected_word_clouds = [];
    var implicitly_selected_r_outputs = [];
    implicitly_selected_items.forEach(function(item) {
        switch (item.type) {
            case 'Table' : implicitly_selected_tables.push(item); break;
            case 'Plot' : implicitly_selected_plots.push(item); break;
            case 'Word Cloud' : implicitly_selected_word_clouds.push(item); break;
            case 'R Output' : implicitly_selected_r_outputs.push(item); break;
        }
    })

    var selected_top_level_pages = selected_raw.filter(function (item) { return item.type == 'ReportGroup'; });

    // Collect all subpages of top level selected pages
    var selected_pages = [];
    var selected_page_guids = [];

    function getSubPages(page, known_pages, known_page_guids) {
        let current_guid = page.guid;
        if (known_page_guids.indexOf(current_guid) == -1) {
            known_pages.push(page);
            known_page_guids.push(current_guid);
            let subpages = page.subItems.filter(function (item) { return item.type == 'ReportGroup'; });
            subpages.forEach(function (page) { getSubPages(page, known_pages, known_page_guids); } );
        }
    }
    selected_top_level_pages.forEach(function (page) { getSubPages(page, selected_pages, selected_page_guids); });





    // Identify questions that are selected implicitly within the selected outputs.

    // Questions in tables
    var questions_in_selected_tables = [];
    var questions_in_rows_of_selected_tables = [];
    var questions_in_columns_of_selected_tables = [];
    selected_tables.forEach(function (table) {
        if (table.primary != null ) {
            questions_in_selected_tables.push(table.primary);
            questions_in_rows_of_selected_tables.push(table.primary);
        }
        if (table.secondary != null && typeof table.secondary != 'string') {
            questions_in_selected_tables.push(table.secondary);
            questions_in_columns_of_selected_tables.push(table.secondary);
        }
    });

    // Questions in plots
    var questions_in_selected_plots = [];
    var variables_in_selected_plots = [];
    selected_plots.forEach(function (plot) {
        if (plot.primary != null ) {
            questions_in_selected_plots.push(plot.primary);
        }
        if (plot.secondary != null && typeof plot.secondary != 'string') {
            questions_in_selected_plots.push(plot.secondary);
        }
        if (plot.tertiary != null && typeof plot.tertiary != 'string') {
            questions_in_selected_plots.push(plot.tertiary);
        }
    });

    // Remove from selected_questions things which are selected in tables or plots
    // The intention of selected_questions is to contain questions explicitly selected
    // by the user in Data Sets
    let implicitly_selected_questions = questions_in_selected_tables.concat(questions_in_selected_plots);
    let implicitly_selected_question_guids = implicitly_selected_questions.map(q => q.guid);
    if (is_displayr)
        selected_questions = selected_questions.filter(q => implicitly_selected_question_guids.indexOf(q.guid) == -1);

    // Sort questions into singles and multis
    let single_variable_questions_selected = selected_questions.filter(function (q) { return q.variables.length == 1; });
    let multiple_variable_questions_selected = selected_questions.filter(function (q) { return q.variables.length > 1; });
    let single_variable_questions_implicitly_selected = implicitly_selected_questions.filter(function (q) { return q.variables.length == 1; });
    let multiple_variable_questions_implicitly_selected = implicitly_selected_questions.filter(function (q) { return q.variables.length > 1; });


    // Check in Q for whether have selections in the variables
    // and questions tab.
    // These will be empty if the user has not made selections
    // outside of the outputs tab.
    let v_and_q_selected_questions = selected_questions.filter(q => implicitly_selected_question_guids.indexOf(q.guid) == -1);
    let implicitly_selected_variables = implicitly_selected_questions.map(q => q.variables).flat();
    let implicitly_selected_variables_guids = implicitly_selected_variables.map(v => v.guid);
    let v_and_q_selected_variables = selected_variables.filter(v => implicitly_selected_variables_guids.indexOf(v.guid) == -1);


    // bugdup!23045 selectedDataSets() not available in Q5.9-fixes (QStable at 2021-09-13)
    let selected_data_sets;
    if (project.report.selectedDataSets) {
        selected_data_sets = project.report.selectedDataSets();
    }else {
        selected_data_sets = selected_questions.concat(questions_in_selected_tables)
                              .concat(questions_in_selected_plots)
                              .map(q => q.dataFile);
        let file_names = selected_data_sets.map(data_file => data_file.name);
        selected_data_sets = selected_data_sets.filter((data_file, idx) => file_names.indexOf(data_file.name) === idx);
    }

    let current_page = project.currentPage ? project.currentPage() : null;

    return {selected_data_sets: selected_data_sets,
            implicitly_selected_items: implicitly_selected_items,
            selected_questions: selected_questions,
            selected_pages: selected_pages,
            selected_tables: selected_tables,
            selected_plots: selected_plots,
            selected_word_clouds: selected_word_clouds,
            selected_r_outputs: selected_r_outputs,
            selected_items: selected_items,
            selected_top_level_pages: selected_top_level_pages,
            selected_variables: selected_variables,
            single_variable_questions_selected: single_variable_questions_selected,
            multiple_variable_questions_selected: multiple_variable_questions_selected,
            implicitly_selected_tables: implicitly_selected_tables,
            implicitly_selected_plots: implicitly_selected_plots,
            implicitly_selected_word_clouds: implicitly_selected_word_clouds,
            implicitly_selected_r_outputs: implicitly_selected_r_outputs,
            implicitly_selected_questions: implicitly_selected_questions,
            single_variable_questions_implicitly_selected: single_variable_questions_implicitly_selected,
            multiple_variable_questions_implicitly_selected: multiple_variable_questions_implicitly_selected,
            questions_in_selected_tables: questions_in_selected_tables,
            questions_in_rows_of_selected_tables: questions_in_rows_of_selected_tables,
            questions_in_columns_of_selected_tables: questions_in_columns_of_selected_tables,
            questions_in_selected_plots: questions_in_selected_plots,
            current_page: current_page,
            v_and_q_selected_questions: v_and_q_selected_questions,
            v_and_q_selected_variables:v_and_q_selected_variables};
}

// Function to determine if at least one object of the
// desired type has been selected by the user
function oneOrMoreSelected(user_selections, type, implicitly_selected_allowed) {
    let us = user_selections;
    let isa = implicitly_selected_allowed;
    switch (type) {
        case 'Group' : return us.selected_pages.length > 0;
        case 'Page' : return us.selected_pages.length > 0;
        case 'Table' : return (us.selected_tables.length +
            (isa ? us.implicitly_selected_tables.length : 0)) > 0;
        case 'R Output' : return (us.selected_r_outputs.length +
            (isa ? us.implicitly_selected_r_outputs.length : 0)) > 0;
        case 'Plot' : return (us.selected_plots.length +
            (isa ? us.implicitly_selected_plots.length : 0)) > 0;
        case 'Question' : return us.selected_questions.length > 0;
        case 'Variable Set' : return us.selected_variables.length > 0;
        case 'Variable' : return us.selected_variables.length > 0;
        case 'WordCloud' : return (us.selected_word_clouds.length +
            (isa ? us.implicitly_selected_word_clouds.length : 0)) > 0;
        case 'Variable' : return us.selected_variables.length > 0;
    }
    return false;
}

// Function to determine if at least one object of the
// desired type have been directly selected by the user
function NOrMoreSelected(user_selections, type, n, implicitly_selected_allowed) {
    let us = user_selections;
    let isa = implicitly_selected_allowed;
    switch (type) {
        case 'Group' : return us.selected_pages.length >= n;
        case 'Page' : return us.selected_pages.length >= n;
        case 'Top Pages' : return us.selected_top_level_pages.length >= n;
        case 'Table' : return us.selected_tables.length +
            (isa ? us.implicitly_selected_tables.length : 0) >= n;
        case 'R Output' : return us.selected_r_outputs.length +
            (isa ? us.implicitly_selected_r_outputs.length : 0) >= n;
        case 'Plot' : return us.selected_plots.length +
            (isa ? us.implicitly_selected_plots.length : 0) >= n;
        case 'Question' : return us.selected_questions.length >= n;
        case 'Variable Set' : return us.selected_variables.length >= n;
        case 'Variable' : return us.selected_variables.length >= n;
        case 'WordCloud' : return us.selected_word_clouds.length +
            (isa ? us.implicitly_selected_word_clouds.length : 0) >= n;
        case 'Data Set' : return us.selected_data_sets.length >= n;
    }
    return false;
}

// Function to determine if exactly n items of the
// desired type have been directly selected by the user
function exactlyNSelected(user_selections, type, n, implicitly_selected_allowed) {
    let us = user_selections;
    let isa = implicitly_selected_allowed;
    switch (type) {
        case 'Group' : return us.selected_pages.length == n;
        case 'Page' : return us.selected_pages.length == n;
        case 'Top Pages' : return us.selected_top_level_pages.length == n;
        case 'Table' : return us.selected_tables.length +
            (isa ? us.implicitly_selected_tables.length : 0) == n;
        case 'R Output' : return us.selected_r_outputs.length +
            (isa ? us.implicitly_selected_r_outputs.length : 0) == n;
        case 'Plot' : return us.selected_plots.length +
            (isa ? us.implicitly_selected_plots.length : 0) == n;
        case 'Question' : return us.selected_questions.length == n;
        case 'Variable Set' : return us.selected_variables.length == n;
        case 'Variable' : return us.selected_variables.length == n;
        case 'WordCloud' : return us.selected_word_clouds.length +
            (isa ? us.implicitly_selected_word_clouds.length : 0) == n;
        case 'Data Set' : return us.selected_data_sets.length == n;
    }
    return false;
}

// Returns true if all top-level selected pages are in the same folder, otherwise false.
function selectedPagesAreInSameFolder(user_selections) {
    var selected_top_level_pages = user_selections.selected_top_level_pages;
    var parents = selected_top_level_pages.map(function (page) {
        return page.parentGroup();
    });
    return parents.every(function (grp) { return parents[0].equals(grp)})
}


// This function asks the user to select questions as input for the Qscript
// It can be used in either Q or Displayr. The allowed structures should be
// specified as VariableSetStructures. In Q, the error message will automatically
// convert this to QuestionTypes.
// In Displayr the function will identify the selected questions and will stop if
// those questions are not appropriate. In Q, the function will prompt the user
// to guide them to an appropriate selection if their initial selection is not
// appropriate.
// If the user is only allowed to select one question, set 'single' to true.

function selectInputQuestions(allowed_structures, single, select_scale_questions, add_levels) {

    if (typeof single == 'undefined')
        single = false;
    if (typeof select_scale_questions == 'undefined')
        select_scale_questions = false;
    if (typeof add_levels == 'undefined')
        add_levels = false;

    if (!requireDataFile())
        return false;
    const is_displayr = inDisplayr();
    let structure_name = is_displayr ? "variable set" : "question";
    if (!single)
        structure_name = structure_name + "s";
    const user_selections = getAllUserSelections();
    let selected_questions = user_selections.selected_questions;

    if (selected_questions.length > 0) {
        // Remove and silently ignore questions that do not have allowed structures
        let sorted_selection = splitArrayIntoApplicableAndNotApplicable(selected_questions,
            function (q) { return allowed_structures.indexOf(q.variableSetStructure) != -1 && !q.isBanner; });
        selected_questions = sorted_selection.applicable;
    }
    if (single && selected_questions.length == 1)
        return (selected_questions[0]);


    if (selected_questions.length == 0 || single) {
        // No pop-ups in Displayr, just tell them what to do and then exit
        if (is_displayr) {
            log("Select " + (single ? "exactly one" : "one or more ") + printTypesString(allowed_structures) + " " + structure_name + ".");
            return false;
        }

        if (fileFormatVersion() < 13.05) {
            log("This QScript is not supported in this version of Q. Please use release version 5.4.1.0 or later to use this QScript.");
            return false;
        }
        let data_file = requestOneDataFileFromProject();
        let candidate_questions = getAllQuestionsByStructures([data_file], allowed_structures);
        if (candidate_questions.length === 0) {
            if (!is_displayr)
                convertStructureToType(allowed_structures);
            log("No appropriate " + printTypesString(allowed_structures) + " " + structure_name + " found in the data file. " +
            "Please choose another data file or create " + structure_name + " with this structure.");
            return false;
        }
        // This section gives the user the option of only showing scale identified questions
        // so the user can have a pre-filtered selection. Used for the topAndBottomBoxNetCreator function
        if (select_scale_questions) {
            let question_looks_like_scale = getAllScaleQuestions([data_file]);
            if (question_looks_like_scale.length > 0) {
                if (askYesNo("You will now be shown a list of " + structure_name + " to choose from. Would you like to be shown only " + structure_name + " that look like scales?"))
                    candidate_questions = question_looks_like_scale;
            }
        }
        // This section adds the lowest and highest value and labels to the question label
        // so the user can inspect it during selection. Used for the topAndBottomBoxNetCreator function
        if (add_levels) {
            let question_label_strings = candidate_questions.map(function (q) {
            let 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 " + structure_name + " you want to add NETs to:\r\n(highest and lowest value labels are shown in brackets)", question_label_strings);
            selected_questions = getElementsOfArrayBySelectedIndices(candidate_questions, selected_indices);
        } else {
            if (single)
                selected_questions = selectOneQuestion("Select " + structure_name + ":", candidate_questions, true);
            else
                selected_questions = selectManyQuestions("Select " + structure_name + ":", candidate_questions, true).questions;
        }
    }
    if (selected_questions.length == 0)
        return false;
    return(selected_questions);
}

// Function below to be used as a filter for R outputs that are considered Tables
const VALID_ROUTPUT_FOR_TABLE_CLASSES = ['matrix', 'array', 'data.frame', 'table',
                                         'integer', 'numeric', 'character', 'factor', 'logical'];
validTableOrTableLikeROutput = function (item) {
    return (item.type === 'Table' && item.primary !== null) ||
           (item.type === 'R Output' && item.error === null &&
            item.outputClasses.filter(c => VALID_ROUTPUT_FOR_TABLE_CLASSES.includes(c)).length > 0);
}

function requireOneValidROutputByClass(required_classes)
{
    let selections = getAllUserSelections();
    let invalid_input_msg = "Please select a single output from " +
        correctTerminology("Anything > Advanced Analysis > ") +
        anythingMenuLocation(required_classes) +
        " and then rerun this feature.";
    if (!exactlyNSelected(selections, "R Output", 1, false)) {
        throw new UserError(invalid_input_msg);
    }
    let r_output = selections.selected_r_outputs.length > 0 ?
        selections.selected_r_outputs[0] : selections.implicitly_selected_r_outputs[0];
    if (!checkROutputIsValid(r_output))
    {
        throw new UserError("There is an error with your selected model. " +
            "Please fix the issue in your selection before rerunning this feature.");
    }else if (required_classes.length > 0) {
        if(!r_output.outputClasses.some(cname => required_classes.includes(cname)) &&
           !validOldClassFormatOutput(r_output, required_classes)) {
            throw new UserError(invalid_input_msg);
        }
    }
    return r_output;
}

// extra checks to support older Choice Model, MaxDiff, and Regression
// outputs that only appended a single class (e.g. "FitChoice") to their outputs
function validOldClassFormatOutput(routput, required_classes) {
    let output_class = routput.outputClasses;
    if (output_class.length !== 1) // All newly created AA outputs have multiple classes
        return false;
    output_class = output_class[0];
    let algorithm;
    if (output_class === "FitMaxDiff" && /^FitMaxDiff/.test(required_classes[0])) {
        algorithm = routput.data.get(["algorithm"])[0];
        if (required_classes.some(cls => /HB$/.test(cls)) && /^HB/.test(algorithm)) {
            return true
        }else if (required_classes.some(cls => /LCA$|MNL$/.test(cls)) &&
                  algorithm === "Default") {
            return true;
        }
    }else if (output_class === "FitChoice" && /^FitChoice/.test(required_classes[0])) {
        algorithm = routput.data.get(["algorithm"])[0];
        if (required_classes.some(cls => /HB$/.test(cls)) && /^HB/.test(algorithm)) {
            return true
        }else if (required_classes.some(cls => /LCA$|MNL$/.test(cls)) &&
                  algorithm === "LCA") {
            return true;
        }
    }else if (output_class === "Regression") {
        let rtype = routput.data.get(["type"])[0];
        if (required_classes[0] === "LinearRegression" && rtype === "Linear") {
            return true
        }else if (required_classes[0] === "Regression") {
            return true;
        }
    }
    return false;
}

function anythingMenuLocation(classes) {
  switch(classes[0]) {
        case "ChoiceModelDesign" : return "Choice Modeling > Experimental Design";
        case "FitChoiceLCA": return "Choice Modeling > Latent Class Analysis";
        case "FitChoiceHB": return "Choice Modeling > Hierarchical Bayes";
        case "FitChoice": return "Choice Modeling (e.g. Hierarchical Bayes)";
        case "FitMaxDiffLCA": return "Marketing > MaxDiff > Latent Class Analysis";
        case "FitMaxDiffHB": return "Marketing > MaxDiff > Hierarchical Bayes";
        case "FitMaxDiff": return "Marketing > MaxDiff (e.g. Hierarchical Bayes)";
        case "CorrespondenceAnalysis": return "Dimension Reduction > " +
                "Correspondence Analysis of a Table";
        case "LDA": return "Machine Learning > Linear Discriminant Analysis";
        case "MachineLearning": return "Machine Learning (e.g. Random Forest)";
        case "LinearRegression" : return "Regression > Linear Regression";
        case "Regression" : return "Regression (e.g. Linear Regression)";
    }
}

See also