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.
var __webpack_modules__ = ({});
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
exports: {}
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// webpack/runtime/rspack_version
(() => {
__webpack_require__.rv = () => ("1.7.2")
})();
// webpack/runtime/rspack_unique_id
(() => {
__webpack_require__.ruid = "bundler=rspack@1.7.2";
})();
includeWeb('Table JavaScript Utility Functions');
includeWeb('JavaScript Text Analysis 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.