Table JavaScript Functions for Selecting Matching Rows or Columns
Jump to navigation
Jump to search
This page contains functions to help with writing Table JavaScript for Rules.
includeWeb('Table JavaScript Utility Functions');
// Duplicated and modified from src/Standard R/Select Rows and Columns from Table.js
// Unfortunately there is currently no way to share code with Standard R
function getInputNames(input, dim = 0) {
const MULTI_QUESTIONTYPES = [
'Text - Multi',
'Pick One - Multi',
'Pick Any - Compact',
'Pick Any - Grid',
'Number - Grid'
];
const VECTOR_TYPES = ['numeric', 'character', 'logical', 'integer', 'factor'];
if (input.type === 'R Output') {
const classes = input.outputClasses;
const input_data = input.data;
if (!input_data || !classes) {
return null;
}
const is_vector = classes.some(c => VECTOR_TYPES.includes(c));
try { // Attempt to call getAttribute, which errors if attribute is not present
if ((is_vector && dim === 0) || (classes.includes('data.frame') && dim === 1)) {
return input_data.getAttribute([], 'names');
}
if (classes.includes('data.frame') && dim === 0) {
var names = input_data.getAttribute([], 'row.names');
// integer/number row names only occur if they have not been
// changed from their defaults in R (i.e. there are no rownames),
// return empty array so selection mode defaults to range/text box
return !names || (names.length > 0 && typeof names[0] === 'number') ? null : names;
}
const dimnames = input_data.getAttribute([], 'dimnames');
if (classes.includes('QTable') && dimnames.length === 1 && dim === 1) {
return input_data.getAttribute([], 'statistic');
}
return dimnames && dim < dimnames.length && dimnames[dim] != null ? dimnames[dim] : null;
}
catch (e) {
if (e.message.includes('attribute')) {
return null;
}
throw e; // re-throw the error if it's not about missing attributes
}
}
if (input.type === 'Table') {
const table_output = input.calculateOutput();
const primary = input.primary;
const secondary = input.secondary;
if (!table_output || !primary || !secondary) {
return null;
}
const is_crosstab_or_multi_or_raw = secondary.type === 'Question'
|| MULTI_QUESTIONTYPES.includes(primary.questionType)
|| secondary === 'RAW DATA';
if (primary.isBanner && secondary === 'SUMMARY') {
is_crosstab_or_multi_or_raw = false;
}
if (dim === 0) {
return table_output.rowLabels;
}
if (dim === 1 && is_crosstab_or_multi_or_raw) { // get column labels from a crosstab
return table_output.columnLabels;
}
return table_output.statistics;
}
return null;
}
function isTable(obj) {
if (obj.type === 'R Output') {
const obj_classes = obj.outputClasses;
return obj_classes.includes('matrix') || obj_classes.includes('array') || obj_classes.includes('data.frame');
}
else if (obj.type === 'Table') {
return true;
}
else {
return false;
}
}
function selectUsingLabels(labels, get_original_labels, get_count, delete_method, sort_method, use_order_from_input) {
const labels_in_lower_case = labels.map(label => normalizeLabel(label));
const labels_set = new Set(labels_in_lower_case);
const indices_to_remove = get_original_labels().map((label, index) => labels_set.has(normalizeLabel(label)) ? -1 : index).filter(index => index !== -1);
if (indices_to_remove.length > 0) {
delete_method(indices_to_remove);
}
if (use_order_from_input && get_count() > 0) {
const index_infos = get_original_labels().map((label, index) => ({ originalIndex: index, indexInValues: labels_in_lower_case.indexOf(normalizeLabel(label)) }));
// Sort by the index of label in the values
index_infos.sort((a, b) => a.indexInValues - b.indexInValues);
const sorted_indices = index_infos.map(info => info.originalIndex);
sort_method(sorted_indices);
}
}
function selectUsingIndices(indices, get_count, delete_method, sort_method, use_order_from_input, is_row) {
// filter out NaN values
indices = indices.filter(x => !isNaN(x));
if (indices.length <= 0) {
form.ruleNotApplicable('no valid values found from the selected input');
}
if (indices.some(x => !Number.isInteger(x))) {
form.ruleNotApplicable('the selected input contains non-integer values');
}
// check if the indices are within the range
// (indices are 1-based, so we need to subtract 1)
if (indices.some(x => x - 1 < 0 || x - 1 >= get_count())) {
form.ruleNotApplicable(`the selected input contains indices outside the range of the number of ${is_row ? 'rows' : 'columns'}`);
}
const zero_based_indices = indices.filter(x => !isNaN(x)).map(x => x - 1);
const indices_to_remove = [];
const indices_to_keep = [];
for (let i = 0; i < get_count(); i++) {
if (zero_based_indices.includes(i)) {
indices_to_keep.push(i);
}
else {
indices_to_remove.push(i);
}
}
if (indices_to_remove.length > 0) {
delete_method(indices_to_remove);
}
if (use_order_from_input && get_count() > 0) {
const index_infos = indices_to_keep.map((index_before_deletion, index_after_deletion) => ({ originalIndex: index_after_deletion, indexInValues: zero_based_indices.indexOf(index_before_deletion) }));
// Sort by the index of original index in the values
index_infos.sort((a, b) => a.indexInValues - b.indexInValues);
const sorted_indices = index_infos.map(info => info.originalIndex);
sort_method(sorted_indices);
}
}
function selectMatchingRowsOrColumns(is_row) {
form.setHeading(`Select Matching ${is_row ? 'Rows' : 'Columns'}`);
form.setSummary(`Select ${is_row ? 'rows' : 'columns'} matching values from another input or item`);
const description_first_part = form.newLabel(`Select ${is_row ? 'rows' : 'columns'} using`);
const dropbox = form.newDropBox('data_source', ['Control:combobox,listbox,textbox', 'RItem:integer,numeric,character,matrix,array,data.frame', 'Table'], false);
dropbox.lineBreakAfter = true;
const controls = [description_first_part, dropbox];
const dropbox_value = dropbox.getValue();
if (!dropbox_value) {
form.setInputControls(controls);
form.ruleNotApplicable('no data selected');
}
const referenced_obj = form.resolve(dropbox_value.guid);
if (!referenced_obj) {
form.setInputControls(controls);
form.ruleNotApplicable('the selected input cannot be resolved');
}
let is_table = false;
try {
is_table = isTable(referenced_obj);
}
catch (err) {
// Accessing 'outputClasses' in isTable() can throw an error when there is a circular reference
form.setInputControls(controls);
form.ruleNotApplicable(err.message || 'the selected input is in error');
}
let use_rows_from_input = null;
if (is_table) {
const use_label = form.newLabel('Use selected input\'s');
const use_rows_or_columns_from_input_combobox = form.newComboBox('use_rows_or_columns_from_input', ['rows', 'columns']);
const use_label_2 = form.newLabel('for matching');
use_label_2.lineBreakAfter = true;
use_rows_or_columns_from_input_combobox.setDefault('rows');
controls.push(use_label);
controls.push(use_rows_or_columns_from_input_combobox);
controls.push(use_label_2);
use_rows_from_input = use_rows_or_columns_from_input_combobox.getValue() === 'rows';
}
const order_label = form.newLabel(`Order ${is_row ? 'rows' : 'columns'} based on`);
const order_combobox = form.newComboBox('match_order', ['selected input', 'original order']);
order_combobox.setDefault('selected input');
controls.push(order_label);
controls.push(order_combobox);
const use_order_from_input = order_combobox.getValue() === 'selected input';
form.setInputControls(controls);
const delete_method = (indices) => is_row ? table.deleteRows(indices) : table.deleteColumns(indices);
const sort_method = (indices) => is_row ? table.sortRows(indices) : table.sortColumns(indices);
const get_original_labels = () => is_row ? table.rowLabels : table.columnLabels;
const get_count = () => is_row ? table.numberRows : table.numberColumns;
if (is_table) {
const row_labels = getInputNames(referenced_obj, 0);
const column_labels = getInputNames(referenced_obj, 1);
const has_row_labels = row_labels && row_labels.length > 0;
const has_column_labels = column_labels && column_labels.length > 0;
if (use_rows_from_input) {
if (has_row_labels) {
selectUsingLabels(row_labels, get_original_labels, get_count, delete_method, sort_method, use_order_from_input);
}
else {
form.ruleNotApplicable('the selected input does not have row labels');
}
}
else {
if (has_column_labels) {
selectUsingLabels(column_labels, get_original_labels, get_count, delete_method, sort_method, use_order_from_input);
}
else {
form.ruleNotApplicable('the selected input does not have column labels');
}
}
return;
}
const labels = getInputNames(referenced_obj, 0);
if (labels && labels.length > 0) {
selectUsingLabels(labels, get_original_labels, get_count, delete_method, sort_method, use_order_from_input);
return;
}
let values = [];
switch (referenced_obj.type) {
case 'Control':
values = referenced_obj.data.get([]);
break;
case 'R Output':
values = referenced_obj.data.get([]);
break;
// Omit 'Table' case as it is handled above as is_table is true
default:
form.ruleNotApplicable('selected input is not supported');
}
values = values ? values.filter(x => x !== null && x !== undefined) : null;
if (!values || values.length <= 0) {
form.ruleNotApplicable('no valid values found from the selected input');
}
const is_string = values.every(x => typeof x === 'string');
const is_number = values.every(x => typeof x === 'number');
if (is_string) {
selectUsingLabels(values, get_original_labels, get_count, delete_method, sort_method, use_order_from_input);
}
else if (is_number) {
selectUsingIndices(values, get_count, delete_method, sort_method, use_order_from_input, is_row);
}
else {
form.ruleNotApplicable('the selected input needs to contain either text or numeric values');
}
}
To make these functions available when writing a QScript or Rule see JavaScript Reference.